blob: 51a08fc1351d12b8ef9defa9ee952d9e0004725e [file] [log] [blame]
// Copyright 2024 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/enterprise/data_controls/desktop_data_controls_dialog.h"
#include "chrome/grit/generated_resources.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/guest_view/browser/guest_view_base.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/gfx/color_palette.h"
#include "ui/views/border.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout_view.h"
namespace data_controls {
namespace {
constexpr int kSpacingBetweenIconAndMessage = 8;
constexpr int kBusinessIconSize = 16;
DesktopDataControlsDialog::TestObserver* observer_for_testing_ = nullptr;
std::unique_ptr<views::View> CreateEnterpriseIcon() {
auto enterprise_icon = std::make_unique<views::ImageView>();
enterprise_icon->SetImage(ui::ImageModel::FromVectorIcon(
vector_icons::kBusinessIcon, ui::kColorSysOnSurfaceSubtle,
kBusinessIconSize));
return enterprise_icon;
}
class DataControlsDialogDelegate : public views::DialogDelegate {
public:
explicit DataControlsDialogDelegate(DataControlsDialog::Type type,
DesktopDataControlsDialog* dialog)
: type_(type), dialog_(dialog) {
set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH));
// TODO(crbug.com/351342878): Move shared logic for dialog button styling to
// `DataControlsDialog`.
// For warning dialogs, "cancel" means "ignore the warning and bypass" and
// "accept" means "accept the warning and stop copying/pasting".
switch (type_) {
case DataControlsDialog::Type::kClipboardPasteBlock:
case DataControlsDialog::Type::kClipboardCopyBlock:
SetButtons(static_cast<int>(ui::mojom::DialogButton::kOk));
SetButtonLabel(ui::mojom::DialogButton::kOk,
l10n_util::GetStringUTF16(IDS_OK));
break;
case DataControlsDialog::Type::kClipboardPasteWarn:
SetButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
SetButtonLabel(ui::mojom::DialogButton::kOk,
l10n_util::GetStringUTF16(
IDS_DATA_CONTROLS_PASTE_WARN_CANCEL_BUTTON));
SetButtonStyle(ui::mojom::DialogButton::kCancel,
ui::ButtonStyle::kTonal);
SetButtonLabel(ui::mojom::DialogButton::kCancel,
l10n_util::GetStringUTF16(
IDS_DATA_CONTROLS_PASTE_WARN_CONTINUE_BUTTON));
break;
case DataControlsDialog::Type::kClipboardCopyWarn:
SetButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
SetButtonLabel(ui::mojom::DialogButton::kOk,
l10n_util::GetStringUTF16(
IDS_DATA_CONTROLS_COPY_WARN_CANCEL_BUTTON));
SetButtonStyle(ui::mojom::DialogButton::kCancel,
ui::ButtonStyle::kTonal);
SetButtonLabel(ui::mojom::DialogButton::kCancel,
l10n_util::GetStringUTF16(
IDS_DATA_CONTROLS_COPY_WARN_CONTINUE_BUTTON));
break;
case DataControlsDialog::Type::kClipboardShareWarn:
case DataControlsDialog::Type::kClipboardActionWarn:
case DataControlsDialog::Type::kClipboardShareBlock:
case DataControlsDialog::Type::kClipboardActionBlock:
// These flows are exclusive to mobile.
NOTREACHED();
}
SetButtonStyle(ui::mojom::DialogButton::kOk, ui::ButtonStyle::kProminent);
SetDefaultButton(static_cast<int>(ui::mojom::DialogButton::kOk));
if (observer_for_testing_) {
observer_for_testing_->OnConstructed(dialog_, this);
}
}
~DataControlsDialogDelegate() override {}
std::u16string GetWindowTitle() const override {
// TODO(crbug.com/351342878): Move this title string selection logic to
// common code as needed.
int id;
switch (type_) {
case DataControlsDialog::Type::kClipboardPasteBlock:
id = IDS_DATA_CONTROLS_CLIPBOARD_PASTE_BLOCK_TITLE;
break;
case DataControlsDialog::Type::kClipboardCopyBlock:
id = IDS_DATA_CONTROLS_CLIPBOARD_COPY_BLOCK_TITLE;
break;
case DataControlsDialog::Type::kClipboardPasteWarn:
id = IDS_DATA_CONTROLS_CLIPBOARD_PASTE_WARN_TITLE;
break;
case DataControlsDialog::Type::kClipboardCopyWarn:
id = IDS_DATA_CONTROLS_CLIPBOARD_COPY_WARN_TITLE;
break;
case DataControlsDialog::Type::kClipboardShareWarn:
case DataControlsDialog::Type::kClipboardActionWarn:
case DataControlsDialog::Type::kClipboardShareBlock:
case DataControlsDialog::Type::kClipboardActionBlock:
// These flows are exclusive to mobile.
NOTREACHED();
id = IDS_POLICY_ACTION_BLOCKED_BY_ORGANIZATION;
break;
}
return l10n_util::GetStringUTF16(id);
}
std::unique_ptr<views::Label> CreateMessage() const {
int id;
switch (type_) {
case DataControlsDialog::Type::kClipboardPasteBlock:
case DataControlsDialog::Type::kClipboardCopyBlock:
case DataControlsDialog::Type::kClipboardShareBlock:
case DataControlsDialog::Type::kClipboardActionBlock:
id = IDS_DATA_CONTROLS_BLOCKED_LABEL;
break;
case DataControlsDialog::Type::kClipboardPasteWarn:
case DataControlsDialog::Type::kClipboardCopyWarn:
case DataControlsDialog::Type::kClipboardShareWarn:
case DataControlsDialog::Type::kClipboardActionWarn:
id = IDS_DATA_CONTROLS_WARNED_LABEL;
break;
}
return std::make_unique<views::Label>(l10n_util::GetStringUTF16(id));
}
views::View* GetContentsView() override {
if (!contents_view_) {
contents_view_ = new views::BoxLayoutView(); // Owned by caller
contents_view_->SetOrientation(
views::BoxLayout::Orientation::kHorizontal);
contents_view_->SetMainAxisAlignment(
views::BoxLayout::MainAxisAlignment::kStart);
contents_view_->SetCrossAxisAlignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
contents_view_->SetBorder(views::CreateEmptyBorder(
views::LayoutProvider::Get()->GetInsetsMetric(views::INSETS_DIALOG)));
contents_view_->SetBetweenChildSpacing(kSpacingBetweenIconAndMessage);
contents_view_->AddChildView(CreateEnterpriseIcon());
contents_view_->AddChildView(CreateMessage());
}
return contents_view_;
}
ui::mojom::ModalType GetModalType() const override {
return ui::mojom::ModalType::kChild;
}
bool ShouldShowCloseButton() const override { return false; }
void OnWidgetInitialized() override {
if (observer_for_testing_) {
observer_for_testing_->OnWidgetInitialized(dialog_, this);
}
}
// Resets internal members to avoid dangling pointers. Only call this when the
// owning widget is about to be destroyed.
void Shutdown() {
contents_view_ = nullptr;
dialog_ = nullptr;
}
private:
DataControlsDialog::Type type_;
raw_ptr<views::BoxLayoutView> contents_view_ = nullptr;
raw_ptr<DesktopDataControlsDialog> dialog_ = nullptr;
};
} // namespace
DesktopDataControlsDialog::TestObserver::TestObserver() {
DesktopDataControlsDialog::SetObserverForTesting(this);
}
DesktopDataControlsDialog::TestObserver::~TestObserver() {
DesktopDataControlsDialog::SetObserverForTesting(nullptr);
}
// static
void DesktopDataControlsDialog::SetObserverForTesting(TestObserver* observer) {
// These checks add safety that tests are only setting one observer at a time.
if (observer_for_testing_) {
DCHECK_EQ(observer, nullptr);
} else {
DCHECK_NE(observer, nullptr);
}
observer_for_testing_ = observer;
}
void DesktopDataControlsDialog::Show(base::OnceClosure on_destructed) {
on_destructed_ = std::move(on_destructed);
content::WebContents* top_web_contents =
guest_view::GuestViewBase::GetTopLevelWebContents(web_contents());
dialog_delegate_ = std::make_unique<DataControlsDialogDelegate>(type_, this);
dialog_delegate_->SetOwnershipOfNewWidget(
views::Widget::InitParams::CLIENT_OWNS_WIDGET);
widget_ = constrained_window::ShowWebModalDialogViewsOwned(
dialog_delegate_.get(), top_web_contents,
views::Widget::InitParams::CLIENT_OWNS_WIDGET);
widget_->MakeCloseSynchronous(base::BindOnce(
&DesktopDataControlsDialog::CloseDialog, base::Unretained(this)));
}
void DesktopDataControlsDialog::CloseDialog(
views::Widget::ClosedReason reason) {
if (reason == views::Widget::ClosedReason::kAcceptButtonClicked) {
OnDialogButtonClicked(/*bypassed=*/false);
}
if (reason == views::Widget::ClosedReason::kCancelButtonClicked) {
OnDialogButtonClicked(/*bypassed=*/true);
}
static_cast<DataControlsDialogDelegate*>(dialog_delegate_.get())->Shutdown();
// The existing pattern is self-owned via
// SetOwnedByWidget(OwnedByWidgetPassKey());, since the previous code was a
// DialogDelegate owned by the widget.
// In the new pattern, deleting this implicitly deletes all the scopers,
// including the widget and the WebContents::ScopedIgnoreInputEvents.
delete this;
}
DesktopDataControlsDialog::~DesktopDataControlsDialog() {
if (on_destructed_) {
std::move(on_destructed_).Run();
}
if (observer_for_testing_) {
observer_for_testing_->OnDestructed(this);
}
}
void DesktopDataControlsDialog::WebContentsDestroyed() {
// If the WebContents the dialog is showing on gets destroyed, then the dialog
// was neither bypassed or accepted so it should close without calling
// any callback.
ClearCallbacks();
CloseDialog(views::Widget::ClosedReason::kAcceptButtonClicked);
}
void DesktopDataControlsDialog::PrimaryPageChanged(content::Page& page) {
// If the primary page is changed, the triggered Data Controls rules that lead
// to this current dialog showing are not necessarily still applicable. Data
// shouldn't be allowed through since there might be higher severity rules
// that trigger on the new page, so callbacks must be cleared before closing
// the dialog.
ClearCallbacks();
CloseDialog(views::Widget::ClosedReason::kAcceptButtonClicked);
}
DesktopDataControlsDialog::DesktopDataControlsDialog(
Type type,
content::WebContents* contents,
base::OnceCallback<void(bool bypassed)> callback)
: DataControlsDialog(type, std::move(callback)),
content::WebContentsObserver(contents) {
}
} // namespace data_controls