chromium/chrome/browser/ash/policy/dlp/dialogs/files_policy_warn_dialog.cc

// Copyright 2023 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/ash/policy/dlp/dialogs/files_policy_warn_dialog.h"

#include <optional>
#include <string>

#include "ash/public/cpp/style/color_provider.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/typography.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/ash/policy/dlp/dialogs/files_policy_dialog.h"
#include "chrome/browser/ash/policy/dlp/dialogs/files_policy_dialog_utils.h"
#include "chrome/browser/ash/policy/dlp/files_policy_string_util.h"
#include "chrome/browser/chromeos/policy/dlp/dialogs/policy_dialog_base.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_file_destination.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_files_controller.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_files_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "components/enterprise/data_controls/core/browser/component.h"
#include "components/enterprise/data_controls/core/browser/dlp_histogram_helper.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/chromeos/strings/grit/ui_chromeos_strings.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/textarea/textarea.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "url/gurl.h"

namespace policy {
namespace {
// Maximum number of characters a user can write in the justification text area.
constexpr int kMaxBypassJustificationLength = 280;

// Returns the domain of the |destination|'s |url| if it can be
// obtained, or the full value otherwise, converted to u16string. Fails if
// |url| is empty.
std::u16string GetDestinationURL(DlpFileDestination destination) {
  DCHECK(destination.url().has_value());
  DCHECK(destination.url()->is_valid());
  GURL gurl = *destination.url();
  if (gurl.has_host()) {
    return base::UTF8ToUTF16(gurl.host());
  }
  return base::UTF8ToUTF16(gurl.spec());
}

// Returns the u16string formatted name for |destination|'s |component|. Fails
// if |component| is empty.
const std::u16string GetDestinationComponent(DlpFileDestination destination) {
  DCHECK(destination.component().has_value());
  switch (destination.component().value()) {
    case data_controls::Component::kArc:
      return l10n_util::GetStringUTF16(
          IDS_FILE_BROWSER_ANDROID_FILES_ROOT_LABEL);
    case data_controls::Component::kCrostini:
      return l10n_util::GetStringUTF16(IDS_FILE_BROWSER_LINUX_FILES_ROOT_LABEL);
    case data_controls::Component::kPluginVm:
      return l10n_util::GetStringUTF16(
          IDS_FILE_BROWSER_PLUGIN_VM_DIRECTORY_LABEL);
    case data_controls::Component::kUsb:
      return l10n_util::GetStringUTF16(
          IDS_POLICY_DLP_FILES_DESTINATION_REMOVABLE_STORAGE);
    case data_controls::Component::kDrive:
      return l10n_util::GetStringUTF16(IDS_FILE_BROWSER_DRIVE_DIRECTORY_LABEL);
    case data_controls::Component::kOneDrive:
      return l10n_util::GetStringUTF16(
          IDS_FILE_BROWSER_DLP_COMPONENT_MICROSOFT_ONEDRIVE);
    case data_controls::Component::kUnknownComponent:
      NOTREACHED_IN_MIGRATION();
      return u"";
  }
}

// Returns the u16string formatted |destination|. Fails if both |component| and
// |url| are empty (the destination is a local file/directory). Returns the
// |component| if both are non-empty.
const std::u16string GetDestination(DlpFileDestination destination) {
  return destination.component().has_value()
             ? GetDestinationComponent(destination)
             : GetDestinationURL(destination);
}

const std::u16string GetJustificationLabelText(dlp::FileAction action) {
  switch (action) {
    case dlp::FileAction::kDownload:
      return l10n_util::GetStringUTF16(
          IDS_POLICY_DLP_FILES_DOWNLOAD_JUSTIFICATION_LABEL);
    case dlp::FileAction::kUpload:
      return l10n_util::GetStringUTF16(
          IDS_POLICY_DLP_FILES_UPLOAD_JUSTIFICATION_LABEL);
    case dlp::FileAction::kCopy:
      return l10n_util::GetStringUTF16(
          IDS_POLICY_DLP_FILES_COPY_JUSTIFICATION_LABEL);
    case dlp::FileAction::kMove:
      return l10n_util::GetStringUTF16(
          IDS_POLICY_DLP_FILES_MOVE_JUSTIFICATION_LABEL);
    case dlp::FileAction::kOpen:
    case dlp::FileAction::kShare:
      return l10n_util::GetStringUTF16(
          IDS_POLICY_DLP_FILES_OPEN_JUSTIFICATION_LABEL);
    case dlp::FileAction::kTransfer:
    case dlp::FileAction::kUnknown:
      return l10n_util::GetStringUTF16(
          IDS_POLICY_DLP_FILES_TRANSFER_JUSTIFICATION_LABEL);
  }
}

}  // namespace

FilesPolicyWarnDialog::FilesPolicyWarnDialog(
    WarningWithJustificationCallback callback,
    dlp::FileAction action,
    gfx::NativeWindow modal_parent,
    std::optional<DlpFileDestination> destination,
    Info dialog_info)
    : FilesPolicyDialog(dialog_info.GetFiles().size(), action, modal_parent),
      destination_(destination),
      dialog_info_(dialog_info) {
  auto split = base::SplitOnceCallback(std::move(callback));
  SetAcceptCallback(base::BindOnce(&FilesPolicyWarnDialog::ProceedWarning,
                                   weak_ptr_factory_.GetWeakPtr(),
                                   std::move(split.first)));
  SetCancelCallback(base::BindOnce(&FilesPolicyWarnDialog::CancelWarning,
                                   weak_ptr_factory_.GetWeakPtr(),
                                   std::move(split.second)));
  SetButtonLabel(ui::mojom::DialogButton::kOk, GetOkButton());
  SetButtonLabel(ui::mojom::DialogButton::kCancel, GetCancelButton());

  AddGeneralInformation();
  if (dialog_info_.GetLearnMoreURL().has_value()) {
    files_dialog_utils::AddLearnMoreLink(
        l10n_util::GetStringUTF16(IDS_LEARN_MORE),
        dialog_info.GetAccessibleLearnMoreLinkName(),
        dialog_info_.GetLearnMoreURL().value(), upper_panel_);
  }
  MaybeAddConfidentialRows();
  MaybeAddJustificationPanel();

  data_controls::DlpHistogramEnumeration(
      data_controls::dlp::kFileActionWarnReviewedUMA, action);
}

FilesPolicyWarnDialog::~FilesPolicyWarnDialog() = default;

size_t FilesPolicyWarnDialog::GetMaxBypassJustificationLengthForTesting()
    const {
  return kMaxBypassJustificationLength;
}

void FilesPolicyWarnDialog::MaybeAddConfidentialRows() {
  if (action_ == dlp::FileAction::kDownload ||
      dialog_info_.GetFiles().empty()) {
    return;
  }

  SetupScrollView();
  for (const auto& file : dialog_info_.GetFiles()) {
    AddConfidentialRow(file.icon, file.title);
  }
}

std::u16string FilesPolicyWarnDialog::GetOkButton() {
  return policy::files_string_util::GetContinueAnywayButton(action_);
}

std::u16string FilesPolicyWarnDialog::GetCancelButton() {
  return l10n_util::GetStringUTF16(IDS_POLICY_DLP_WARN_CANCEL_BUTTON);
}

std::u16string FilesPolicyWarnDialog::GetTitle() {
  if (base::FeatureList::IsEnabled(features::kNewFilesPolicyUX)) {
    switch (action_) {
      case dlp::FileAction::kDownload:
        return l10n_util::GetStringUTF16(
            IDS_POLICY_DLP_FILES_DOWNLOAD_REVIEW_TITLE);
      case dlp::FileAction::kUpload:
        return l10n_util::GetStringUTF16(
            IDS_POLICY_DLP_FILES_UPLOAD_REVIEW_TITLE);
      case dlp::FileAction::kCopy:
        return l10n_util::GetStringUTF16(
            IDS_POLICY_DLP_FILES_COPY_REVIEW_TITLE);
      case dlp::FileAction::kMove:
        return l10n_util::GetStringUTF16(
            IDS_POLICY_DLP_FILES_MOVE_REVIEW_TITLE);
      case dlp::FileAction::kOpen:
      case dlp::FileAction::kShare:
        return l10n_util::GetStringUTF16(
            IDS_POLICY_DLP_FILES_OPEN_REVIEW_TITLE);
      case dlp::FileAction::kTransfer:
      case dlp::FileAction::kUnknown:  // TODO(crbug.com/40238129)
                                       // Set proper text when file
                                       // action is unknown
        return l10n_util::GetStringUTF16(
            IDS_POLICY_DLP_FILES_TRANSFER_REVIEW_TITLE);
    }
  }
  switch (action_) {
    case dlp::FileAction::kDownload:
      return l10n_util::GetPluralStringFUTF16(
          // Download action is only allowed for one file.
          IDS_POLICY_DLP_FILES_DOWNLOAD_WARN_TITLE, 1);
    case dlp::FileAction::kUpload:
      return l10n_util::GetPluralStringFUTF16(
          IDS_POLICY_DLP_FILES_UPLOAD_WARN_TITLE, file_count_);
    case dlp::FileAction::kCopy:
      return l10n_util::GetPluralStringFUTF16(
          IDS_POLICY_DLP_FILES_COPY_WARN_TITLE, file_count_);
    case dlp::FileAction::kMove:
      return l10n_util::GetPluralStringFUTF16(
          IDS_POLICY_DLP_FILES_MOVE_WARN_TITLE, file_count_);
    case dlp::FileAction::kOpen:
    case dlp::FileAction::kShare:
      return l10n_util::GetPluralStringFUTF16(
          IDS_POLICY_DLP_FILES_OPEN_WARN_TITLE, file_count_);
    case dlp::FileAction::kTransfer:
    case dlp::FileAction::kUnknown:  // TODO(crbug.com/40238129)
                                     // Set proper text when file
                                     // action is unknown
      return l10n_util::GetPluralStringFUTF16(
          IDS_POLICY_DLP_FILES_TRANSFER_WARN_TITLE, file_count_);
  }
}

std::u16string FilesPolicyWarnDialog::GetMessage() {
  if (base::FeatureList::IsEnabled(features::kNewFilesPolicyUX)) {
    return dialog_info_.GetMessage();
  }
  CHECK(destination_.has_value());
  DlpFileDestination destination_val = destination_.value();
  std::u16string destination_str;
  int message_id;
  switch (action_) {
    case dlp::FileAction::kDownload:
      destination_str = GetDestinationComponent(destination_val);
      // Download action is only allowed for one file.
      return base::ReplaceStringPlaceholders(
          l10n_util::GetPluralStringFUTF16(
              IDS_POLICY_DLP_FILES_DOWNLOAD_WARN_MESSAGE, 1),
          destination_str,
          /*offset=*/nullptr);
    case dlp::FileAction::kUpload:
      destination_str = GetDestinationURL(destination_val);
      message_id = IDS_POLICY_DLP_FILES_UPLOAD_WARN_MESSAGE;
      break;
    case dlp::FileAction::kCopy:
      destination_str = GetDestination(destination_val);
      message_id = IDS_POLICY_DLP_FILES_COPY_WARN_MESSAGE;
      break;
    case dlp::FileAction::kMove:
      destination_str = GetDestination(destination_val);
      message_id = IDS_POLICY_DLP_FILES_MOVE_WARN_MESSAGE;
      break;
    case dlp::FileAction::kOpen:
    case dlp::FileAction::kShare:
      destination_str = GetDestination(destination_val);
      message_id = IDS_POLICY_DLP_FILES_OPEN_WARN_MESSAGE;
      break;
    case dlp::FileAction::kTransfer:
    case dlp::FileAction::kUnknown:
      // kUnknown is used for internal checks - treat as kTransfer.
      destination_str = GetDestination(destination_val);
      message_id = IDS_POLICY_DLP_FILES_TRANSFER_WARN_MESSAGE;
      break;
  }
  return base::ReplaceStringPlaceholders(
      l10n_util::GetPluralStringFUTF16(message_id, file_count_),
      destination_str,
      /*offset=*/nullptr);
}

void FilesPolicyWarnDialog::ProceedWarning(
    WarningWithJustificationCallback callback) {
  std::optional<std::u16string> user_justification;
  if (justification_field_) {
    user_justification = justification_field_->GetText();
  }
  std::move(callback).Run(user_justification,
                          /*should_proceed=*/true);
}

void FilesPolicyWarnDialog::CancelWarning(
    WarningWithJustificationCallback callback) {
  std::move(callback).Run(/*user_justification=*/std::nullopt,
                          /*should_proceed=*/false);
}

void FilesPolicyWarnDialog::MaybeAddJustificationPanel() {
  if (!dialog_info_.DoesBypassRequireJustification()) {
    return;
  }

  // Disable the proceed button until some text is entered.
  DialogDelegate::SetButtonEnabled(ui::mojom::DialogButton::kOk, false);

  views::View* justification_panel =
      AddChildView(std::make_unique<views::View>());
  justification_panel->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical, gfx::Insets::TLBR(8, 24, 8, 24),
      /*between_child_spacing=*/8));

  const std::u16string justification_label_text =
      GetJustificationLabelText(action_);

  views::Label* justification_field_label = justification_panel->AddChildView(
      std::make_unique<views::Label>(justification_label_text));
  justification_field_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  justification_field_label->SetAllowCharacterBreak(true);
  justification_field_label->SetFontList(
      ash::TypographyProvider::Get()->ResolveTypographyToken(
          ash::TypographyToken::kCrosLabel1));
  justification_field_label->SetEnabledColor(
      ash::ColorProvider::Get()->GetContentLayerColor(
          ash::ColorProvider::ContentLayerType::kTextColorPrimary));

  // Setting a themed rounded background does not work for text areas. As a
  // workaround we set it for an external container and set the text area
  // background to transparent.
  views::View* justification_field_container =
      justification_panel->AddChildView(std::make_unique<views::View>());
  justification_field_container->SetLayoutManager(
      std::make_unique<views::FillLayout>());
  justification_field_container->SetBackground(
      views::CreateThemedRoundedRectBackground(
          ash::kColorAshControlBackgroundColorInactive, 8, 0));

  justification_field_ = justification_field_container->AddChildView(
      std::make_unique<views::Textarea>());
  justification_field_->SetID(
      PolicyDialogBase::kEnterpriseConnectorsJustificationTextareaId);
  justification_field_->GetViewAccessibility().SetName(
      justification_label_text);
  justification_field_->GetViewAccessibility().SetDescription(
      l10n_util::GetStringFUTF16(
          IDS_POLICY_DLP_FILES_JUSTIFICATION_TEXTAREA_ACCESSIBLE_DESCRIPTION,
          base::NumberToString16(0),
          base::NumberToString16(kMaxBypassJustificationLength)));
  justification_field_->SetController(this);
  justification_field_->SetBackgroundColor(SK_ColorTRANSPARENT);
  justification_field_->SetPreferredSize(
      gfx::Size(justification_field_->GetPreferredSize().width(), 140));
  justification_field_->SetBorder(
      views::CreateEmptyBorder(gfx::Insets::TLBR(8, 16, 8, 16)));
  justification_field_->SetFontList(
      ash::TypographyProvider::Get()->ResolveTypographyToken(
          ash::TypographyToken::kCrosBody2));

  justification_field_length_label_ =
      justification_panel->AddChildView(std::make_unique<views::Label>());
  justification_field_length_label_->SetHorizontalAlignment(gfx::ALIGN_RIGHT);
  justification_field_length_label_->SetText(l10n_util::GetStringFUTF16(
      IDS_DEEP_SCANNING_DIALOG_BYPASS_JUSTIFICATION_TEXT_LIMIT_LABEL,
      base::NumberToString16(0),
      base::NumberToString16(kMaxBypassJustificationLength)));
  justification_field_length_label_->SetFontList(
      ash::TypographyProvider::Get()->ResolveTypographyToken(
          ash::TypographyToken::kCrosLabel2));
}

void FilesPolicyWarnDialog::ContentsChanged(
    views::Textfield* sender,
    const std::u16string& new_contents) {
  if (justification_field_length_label_) {
    justification_field_length_label_->SetText(l10n_util::GetStringFUTF16(
        IDS_DEEP_SCANNING_DIALOG_BYPASS_JUSTIFICATION_TEXT_LIMIT_LABEL,
        base::NumberToString16(new_contents.size()),
        base::NumberToString16(kMaxBypassJustificationLength)));
    justification_field_->GetViewAccessibility().SetDescription(
        l10n_util::GetStringFUTF16(
            IDS_POLICY_DLP_FILES_JUSTIFICATION_TEXTAREA_ACCESSIBLE_DESCRIPTION,
            base::NumberToString16(new_contents.size()),
            base::NumberToString16(kMaxBypassJustificationLength)));
  }

  if (new_contents.size() == 0 ||
      new_contents.size() > kMaxBypassJustificationLength) {
    DialogDelegate::SetButtonEnabled(ui::mojom::DialogButton::kOk, false);
  } else {
    DialogDelegate::SetButtonEnabled(ui::mojom::DialogButton::kOk, true);
  }
}

BEGIN_METADATA(FilesPolicyWarnDialog)
END_METADATA

}  // namespace policy