// Copyright 2012 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/ui/views/external_protocol_dialog.h"

#include <utility>

#include "base/strings/string_util.h"
#include "base/types/optional_util.h"
#include "build/build_config.h"
#include "chrome/browser/external_protocol/external_protocol_handler.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_occlusion_observer.h"
#include "chrome/browser/picture_in_picture/scoped_picture_in_picture_occlusion_observation.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/shell_integration.h"
#include "chrome/browser/tab_contents/tab_util.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/prefs/pref_service.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/weak_document_ptr.h"
#include "content/public/browser/web_contents.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/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/gfx/text_elider.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/message_box_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_client_view.h"

using content::WebContents;

namespace {

std::u16string GetMessageTextForOrigin(
    const std::optional<url::Origin>& origin) {
  if (!origin || origin->opaque()) {
    return l10n_util::GetStringUTF16(IDS_EXTERNAL_PROTOCOL_MESSAGE);
  }
  return l10n_util::GetStringFUTF16(
      IDS_EXTERNAL_PROTOCOL_MESSAGE_WITH_INITIATING_ORIGIN,
      url_formatter::FormatOriginForSecurityDisplay(*origin));
}

}  // namespace

class ExternalProtocolDialog::PictureInPictureWatcher
    : public PictureInPictureOcclusionObserver {
 public:
  explicit PictureInPictureWatcher(ExternalProtocolDialog* dialog)
      : dialog_(dialog) {
    CHECK(dialog_);
    occlusion_observation_.Observe(dialog_->GetWidget());
  }

  PictureInPictureWatcher(const PictureInPictureWatcher&) = delete;
  PictureInPictureWatcher& operator=(const PictureInPictureWatcher&) = delete;
  ~PictureInPictureWatcher() override = default;

  // Returns true whenever `dialog_` is occluded by Picture-in-Picture windows,
  // false otherwise.
  bool OccludedByPictureInPicture() { return occluded_by_picture_in_picture_; }

  // Simulates Picture-in-Picture occlussion changed for testing.
  void SimulateOcclusionStateChangedForTesting(bool occluded) {
    OnOcclusionStateChanged(occluded);
  }

 private:
  // PictureInPictureOcclusionObserver:
  void OnOcclusionStateChanged(bool occluded) override {
    // Protect from immediate input if the dialog has just become unoccluded.
    if (occluded_by_picture_in_picture_ && !occluded) {
      dialog_->TriggerInputProtection();
    }

    occluded_by_picture_in_picture_ = occluded;
  }

  ScopedPictureInPictureOcclusionObservation occlusion_observation_{this};
  bool occluded_by_picture_in_picture_ = false;
  const raw_ptr<ExternalProtocolDialog> dialog_;
};

#if !BUILDFLAG(IS_CHROMEOS)
// static
void ExternalProtocolHandler::RunExternalProtocolDialog(
    const GURL& url,
    WebContents* web_contents,
    ui::PageTransition ignored_page_transition,
    bool ignored_has_user_gesture,
    bool ignored_is_in_fenced_frame_tree,
    const std::optional<url::Origin>& initiating_origin,
    content::WeakDocumentPtr initiator_document,
    const std::u16string& program_name) {
  DCHECK(web_contents);

  if (program_name.empty()) {
    // ShellExecute won't do anything. Don't bother warning the user.
    return;
  }

  // Windowing system takes ownership.
  new ExternalProtocolDialog(web_contents, url, program_name, initiating_origin,
                             std::move(initiator_document));
}
#endif  // !BUILDFLAG(IS_CHROMEOS)

ExternalProtocolDialog::ExternalProtocolDialog(
    WebContents* web_contents,
    const GURL& url,
    const std::u16string& program_name,
    const std::optional<url::Origin>& initiating_origin,
    content::WeakDocumentPtr initiator_document)
    : web_contents_(web_contents->GetWeakPtr()),
      url_(url),
      program_name_(program_name),
      initiating_origin_(initiating_origin),
      initiator_document_(std::move(initiator_document)) {
  SetDefaultButton(static_cast<int>(ui::mojom::DialogButton::kCancel));
  SetButtonLabel(ui::mojom::DialogButton::kOk,
                 l10n_util::GetStringFUTF16(
                     IDS_EXTERNAL_PROTOCOL_OK_BUTTON_TEXT, program_name_));
  SetButtonLabel(
      ui::mojom::DialogButton::kCancel,
      l10n_util::GetStringUTF16(IDS_EXTERNAL_PROTOCOL_CANCEL_BUTTON_TEXT));

  SetAcceptCallback(base::BindOnce(&ExternalProtocolDialog::OnDialogAccepted,
                                   base::Unretained(this)));
  SetCancelCallback(base::BindOnce(
      &ExternalProtocolHandler::RecordHandleStateMetrics,
      false /* checkbox_selected */, ExternalProtocolHandler::BLOCK));
  SetCloseCallback(base::BindOnce(
      &ExternalProtocolHandler::RecordHandleStateMetrics,
      false /* checkbox_selected */, ExternalProtocolHandler::BLOCK));
  SetModalType(ui::mojom::ModalType::kChild);

  message_box_view_ = AddChildView(std::make_unique<views::MessageBoxView>(
      GetMessageTextForOrigin(initiating_origin_)));

  ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
  gfx::Insets dialog_insets = provider->GetDialogInsetsForContentType(
      views::DialogContentType::kText, views::DialogContentType::kText);
  dialog_insets.set_left(0);
  set_margins(dialog_insets);

  SetUseDefaultFillLayout(true);

  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  // The checkbox allows the user to opt-in to relaxed security
  // (i.e. skipping future prompts) for the combination of the
  // protocol and the origin of the page initiating this external
  // protocol launch. The checkbox is offered so long as the
  // group policy to show the checkbox is not explicitly disabled
  // and there is a trustworthy initiating origin.
  bool show_remember_selection_checkbox =
      profile->GetPrefs()->GetBoolean(
          prefs::kExternalProtocolDialogShowAlwaysOpenCheckbox) &&
      ExternalProtocolHandler::MayRememberAllowDecisionsForThisOrigin(
          base::OptionalToPtr(initiating_origin_));

  if (show_remember_selection_checkbox) {
    message_box_view_->SetCheckBoxLabel(l10n_util::GetStringFUTF16(
        IDS_EXTERNAL_PROTOCOL_CHECKBOX_PER_ORIGIN_TEXT,
        url_formatter::FormatOriginForSecurityDisplay(
            initiating_origin_.value(),
            /*scheme_display = */ url_formatter::SchemeDisplay::
                OMIT_CRYPTOGRAPHIC)));
  }

  constrained_window::ShowWebModalDialogViews(this, web_contents);
  picture_in_picture_watcher_ = std::make_unique<PictureInPictureWatcher>(this);
}

ExternalProtocolDialog::~ExternalProtocolDialog() = default;

bool ExternalProtocolDialog::ShouldShowCloseButton() const {
  return false;
}

std::u16string ExternalProtocolDialog::GetWindowTitle() const {
  constexpr int kMaxCommandCharsToDisplay = 32;
  std::u16string elided;
  gfx::ElideString(program_name_, kMaxCommandCharsToDisplay, &elided);
  return l10n_util::GetStringFUTF16(IDS_EXTERNAL_PROTOCOL_TITLE, elided);
}

void ExternalProtocolDialog::OnDialogAccepted() {
  const bool remember = message_box_view_->IsCheckBoxSelected();
  ExternalProtocolHandler::RecordHandleStateMetrics(
      remember, ExternalProtocolHandler::DONT_BLOCK);

  if (!web_contents_) {
    // Dialog outlasted the WebContents.
    return;
  }

  if (remember) {
    DCHECK(initiating_origin_);
    Profile* profile =
        Profile::FromBrowserContext(web_contents_->GetBrowserContext());

    ExternalProtocolHandler::SetBlockState(
        url_.GetScheme(), *initiating_origin_,
        ExternalProtocolHandler::DONT_BLOCK, profile);
  }

  ExternalProtocolHandler::LaunchUrlWithoutSecurityCheck(
      url_, web_contents_.get(), initiator_document_);
}

void ExternalProtocolDialog::TriggerInputProtection() {
  GetDialogClientView()->TriggerInputProtection();
}

bool ExternalProtocolDialog::ShouldIgnoreButtonPressedEventHandling(
    View* button,
    const ui::Event& event) const {
  // Ignore button pressed events whenever we are occluded by a
  // Picture-in-Picture window.
  return picture_in_picture_watcher_
             ? picture_in_picture_watcher_->OccludedByPictureInPicture()
             : false;
}

bool ExternalProtocolDialog::ShouldAllowKeyEventsDuringInputProtection() const {
  return false;
}

void ExternalProtocolDialog::SimulateOcclusionStateChangedForTesting(
    bool occluded) {
  CHECK(picture_in_picture_watcher_);
  picture_in_picture_watcher_
      ->SimulateOcclusionStateChangedForTesting(  // IN-TEST
          occluded);
}

void ExternalProtocolDialog::SetRememberSelectionCheckboxCheckedForTesting(
    bool checked) {
  message_box_view_->SetCheckBoxSelected(checked);
}

BEGIN_METADATA(ExternalProtocolDialog)
END_METADATA
