| // 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/branded_strings.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_.scheme(), *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 |