blob: 94987d2a2c5e4a5cd94a05ae018c79e415042441 [file] [log] [blame]
// Copyright 2025 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/ssl/ask_before_http_dialog_controller.h"
#include <memory>
#include "base/memory/weak_ptr.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/https_only_mode_tab_helper.h"
#include "chrome/browser/ssl/https_upgrades_util.h"
#include "chrome/browser/ssl/stateful_ssl_host_state_delegate_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tabs/public/tab_dialog_manager.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/common/webui_url_constants.h"
#include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h"
#include "components/security_interstitials/core/https_only_mode_metrics.h"
#include "components/security_interstitials/core/metrics_helper.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/class_property.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
#include "ui/base/models/dialog_model_field.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "ui/base/window_open_disposition_utils.h"
#include "ui/views/bubble/bubble_dialog_model_host.h"
#include "ui/views/controls/bulleted_label_list/bulleted_label_list_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/style/typography.h"
using HttpWarningReason =
security_interstitials::https_only_mode::InterstitialReason;
namespace {
inline constexpr char kLearnMoreLink[] =
"https://support.google.com/chrome?p=first_mode";
// Helper to create the prompt body view based on the Ask-before-HTTP warning
// type.
void AddAskBeforeHttpDialogText(ui::DialogModel& dialog_model,
HttpWarningReason warning_reason,
ui::DialogModelLabel::TextReplacement link) {
// Build the dialog text view based on the warning type.
if (warning_reason == HttpWarningReason::kSiteEngagementHeuristic) {
auto description_text = ui::DialogModelLabel::CreateWithReplacement(
IDS_ABH_PROMPT_SITE_ENGAGEMENT_PRIMARY_PARAGRAPH, link);
dialog_model.AddParagraph(
description_text, /*header=*/u"",
AskBeforeHttpDialogController::kDescriptionTextId);
return;
} else if (warning_reason == HttpWarningReason::kAdvancedProtection) {
// TODO(crbug.com/351990829): Android text is slightly different.
auto description_text = ui::DialogModelLabel::CreateWithReplacement(
IDS_ABH_PROMPT_ADVANCED_PROTECTION_PRIMARY_PARAGRAPH, link);
dialog_model.AddParagraph(
description_text, /*header=*/u"",
AskBeforeHttpDialogController::kDescriptionTextId);
return;
} else if (warning_reason ==
HttpWarningReason::kTypicallySecureUserHeuristic) {
auto description_text = ui::DialogModelLabel::CreateWithReplacement(
IDS_ABH_PROMPT_TYPICALLY_SECURE_BROWSING_PRIMARY_PARAGRAPH, link);
dialog_model.AddParagraph(
description_text, /*header=*/u"",
AskBeforeHttpDialogController::kDescriptionTextId);
return;
} else if (warning_reason == HttpWarningReason::kIncognito) {
auto description_text = ui::DialogModelLabel::CreateWithReplacement(
IDS_ABH_PROMPT_INCOGNITO_PRIMARY_PARAGRAPH, link);
dialog_model.AddParagraph(
description_text, /*header=*/u"",
AskBeforeHttpDialogController::kDescriptionTextId);
return;
} else if (warning_reason == HttpWarningReason::kPref ||
warning_reason == HttpWarningReason::kBalanced) {
// Default text includes parts as a bulleted list.
// TODO(crbug.com/351990829): Replace this with a custom implementation.
// The existing BulletedLabelListView is pretty minimal and doesn't allow
// much flexibility. Having our own would allow more control over margins,
// line heights, text context/styling, and may make it easier to bold
// certain parts (i.e., uses StyledLabel and exposes them for
// customization).
// TODO(crbug.com/351990829): On Android, we just want to add each bit of
// text as a separate paragraph, so the dialog bridge can port it
// directly. We also want to have the "Continue" button be the "cancel"
// button.
auto bullet_list_view = std::make_unique<views::BulletedLabelListView>(
std::vector<std::u16string>(
{l10n_util::GetStringUTF16(
IDS_ABH_PROMPT_BALANCED_MODE_FIRST_ITEM_TEXT),
l10n_util::GetStringUTF16(
IDS_ABH_PROMPT_BALANCED_MODE_SECOND_ITEM_TEXT)}),
views::style::TextStyle::STYLE_BODY_4);
dialog_model.AddCustomField(
std::make_unique<views::BubbleDialogModelHost::CustomView>(
std::move(bullet_list_view),
views::BubbleDialogModelHost::FieldType::kText));
auto description_text = ui::DialogModelLabel::CreateWithReplacement(
IDS_ABH_PROMPT_SECONDARY_TEXT, link);
dialog_model.AddParagraph(
description_text, /*header=*/u"",
AskBeforeHttpDialogController::kDescriptionTextId);
return;
}
}
} // namespace
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(AskBeforeHttpDialogController,
kGoBackButtonId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(AskBeforeHttpDialogController,
kContinueButtonId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(AskBeforeHttpDialogController,
kDescriptionTextId);
AskBeforeHttpDialogController::AskBeforeHttpDialogController(
tabs::TabInterface* tab_interface)
: tab_interface_(tab_interface) {
tab_will_detach_subscription_ = tab_interface_->RegisterWillDetach(
base::BindRepeating(&AskBeforeHttpDialogController::TabWillDetach,
base::Unretained(this)));
}
AskBeforeHttpDialogController::~AskBeforeHttpDialogController() {
if (HasOpenDialogWidget()) {
CloseDialogWidget(views::Widget::ClosedReason::kUnspecified);
}
}
void AskBeforeHttpDialogController::ShowDialog(
content::WebContents* web_contents,
const GURL& request_url,
ukm::SourceId navigation_source_id) {
// If we are triggering a new dialog, that means another navigation
// finished, so we should prefer to replace the dialog and cancel
// the old one. TabDialogManager handles this for us.
// The widget will own `model_host` through DialogDelegate.
views::BubbleDialogModelHost* model_host =
views::BubbleDialogModelHost::CreateModal(CreateDialogModel(request_url),
ui::mojom::ModalType::kChild)
.release();
model_host->SetOwnershipOfNewWidget(
views::Widget::InitParams::CLIENT_OWNS_WIDGET);
model_host->set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_LARGE_MODAL_DIALOG_PREFERRED_WIDTH));
auto tab_dialog_params = std::make_unique<tabs::TabDialogManager::Params>();
// NOTE: This param *only* applies to cross-site navigations, and interacts
// poorly with the ordering of when the ABH dialog is triggered (during
// navigation completion). If this is set to true, the dialog gets shown
// and then immediately closed when doing a cross-site navigation. We
// instead want to observe *all* navigations and close the dialog ourselves
// if it was present when a new navigation *starts*, which is handled by
// HttpsOnlyModeTabHelper::DidStartNavigation().
tab_dialog_params->close_on_navigate = false;
// If for whatever reason a new ABH dialog is triggered, we should prefer
// showing that as it is the one that is relevant for the _current_
// navigation. This will cause any existing dialog to be dismissed.
// TODO(crbug.com/351990829): Write a test for the current behavior that the
// dialog being dismissed for this reason doesn't trigger a "back to
// safety" action.
tab_dialog_params->block_new_modal = false;
// Track the source ID for the navigation that triggered the dialog.
navigation_source_id_ = navigation_source_id;
// Configure the metrics helper for this instance of the warning dialog.
security_interstitials::MetricsHelper::ReportDetails settings;
settings.metric_prefix = "https_first_mode";
// TODO(crbug.com/351990829): Consider if we want to record repeated
// visit metrics (for both the new dialog UI and for the old interstitial UI).
metrics_helper_ = std::make_unique<security_interstitials::MetricsHelper>(
request_url, settings, nullptr);
metrics_helper_->RecordUserDecision(
security_interstitials::MetricsHelper::SHOW);
metrics_helper_->RecordUserInteraction(
security_interstitials::MetricsHelper::TOTAL_VISITS);
dialog_widget_ =
tab_interface_->GetTabFeatures()
->tab_dialog_manager()
->CreateAndShowDialog(model_host, std::move(tab_dialog_params));
dialog_widget_->MakeCloseSynchronous(
base::BindOnce(&AskBeforeHttpDialogController::CloseDialogWidget,
weak_ptr_factory_.GetWeakPtr()));
}
bool AskBeforeHttpDialogController::HasOpenDialogWidget() const {
return dialog_widget_ && !dialog_widget_->IsClosed();
}
void AskBeforeHttpDialogController::CloseDialogWidget(
views::Widget::ClosedReason reason) {
// NOTE: Losing focus (e.g., switching away from the tab with the dialog)
// does not cause the widget to be closed.
if (reason == views::Widget::ClosedReason::kCancelButtonClicked) {
// User pressed the "Continue to site" button.
RecordHttpsFirstModeUKM(navigation_source_id_,
security_interstitials::https_only_mode::
BlockingResult::kInterstitialProceed);
metrics_helper_->RecordUserDecision(
security_interstitials::MetricsHelper::PROCEED);
} else {
// All other cases are the user not proceeding (either actively clicking "Go
// back", or dismissing the warning for some other reason like closing the
// tab).
RecordHttpsFirstModeUKM(navigation_source_id_,
security_interstitials::https_only_mode::
BlockingResult::kInterstitialDontProceed);
metrics_helper_->RecordUserDecision(
security_interstitials::MetricsHelper::DONT_PROCEED);
}
navigation_source_id_ = ukm::kInvalidSourceId;
metrics_helper_.reset();
dialog_widget_.reset();
}
std::unique_ptr<ui::DialogModel>
AskBeforeHttpDialogController::CreateDialogModel(const GURL& request_url) {
auto dialog_model =
ui::DialogModel::Builder()
.SetInternalName(kAskBeforeHttpDialogName)
.SetTitle(l10n_util::GetStringUTF16(IDS_ABH_PROMPT_TITLE))
// TODO(crbug.com/351990829): On Android this should just use
// AddCancelButton().
.AddExtraButton(
base::BindRepeating(
&AskBeforeHttpDialogController::OnContinueButtonClicked,
weak_ptr_factory_.GetWeakPtr(), request_url),
ui::DialogModel::Button::Params()
.SetLabel(l10n_util::GetStringUTF16(
IDS_HTTPS_ONLY_MODE_SUBMIT_BUTTON))
.SetStyle(ui::ButtonStyle::kDefault)
.SetId(kContinueButtonId))
.AddOkButton(
base::BindOnce(
&AskBeforeHttpDialogController::OnGoBackButtonClicked,
weak_ptr_factory_.GetWeakPtr()),
ui::DialogModel::Button::Params()
.SetLabel(l10n_util::GetStringUTF16(
IDS_HTTPS_ONLY_MODE_BACK_BUTTON))
.SetStyle(ui::ButtonStyle::kProminent)
.SetId(kGoBackButtonId))
.OverrideDefaultButton(ui::mojom::DialogButton::kNone)
.SetInitiallyFocusedField(kGoBackButtonId)
.Build();
// We separately add on the main warning text, including the learn more link.
ui::DialogModelLabel::TextReplacement link = ui::DialogModelLabel::CreateLink(
IDS_ABH_PROMPT_LEARN_MORE_LINK,
base::BindRepeating(
&AskBeforeHttpDialogController::OnHelpCenterLinkClicked,
weak_ptr_factory_.GetWeakPtr()));
security_interstitials::https_only_mode::HttpInterstitialState
interstitial_state =
ComputeInterstitialState(tab_interface_->GetContents(), request_url);
AddAskBeforeHttpDialogText(
*dialog_model,
security_interstitials::https_only_mode::GetInterstitialReason(
interstitial_state),
link);
return dialog_model;
}
void AskBeforeHttpDialogController::OnHelpCenterLinkClicked(
const ui::Event& event) {
metrics_helper_->RecordUserInteraction(
security_interstitials::MetricsHelper::SHOW_LEARN_MORE);
tab_interface_->GetBrowserWindowInterface()->OpenGURL(
GURL(kLearnMoreLink),
ui::DispositionFromEventFlags(event.flags(),
WindowOpenDisposition::NEW_FOREGROUND_TAB));
}
void AskBeforeHttpDialogController::OnGoBackButtonClicked() {
if (HasOpenDialogWidget()) {
CloseDialogWidget(views::Widget::ClosedReason::kAcceptButtonClicked);
}
// LINT.IfChange(HttpsFirstModeGoBackLogic)
auto& controller = tab_interface_->GetContents()->GetController();
if (controller.CanGoBack()) {
controller.GoBack();
} else {
controller.LoadURL(GURL(chrome::kChromeUINewTabURL), content::Referrer(),
ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string());
}
// LINT.ThenChange(components/security_interstitials/content/security_interstitial_controller_client.cc:InterstitialGoBackLogic)
}
void AskBeforeHttpDialogController::OnContinueButtonClicked(
const GURL& request_url,
const ui::Event& event) {
if (HasOpenDialogWidget()) {
CloseDialogWidget(views::Widget::ClosedReason::kCancelButtonClicked);
}
// LINT.IfChange(HttpsFirstModeProceedLogic)
content::WebContents* web_contents = tab_interface_->GetContents();
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
StatefulSSLHostStateDelegate* state =
static_cast<StatefulSSLHostStateDelegate*>(
profile->GetSSLHostStateDelegate());
// StatefulSSLHostStateDelegate can be null during tests.
if (state) {
// Notifies the browser process when a HTTP exception is allowed in
// HTTPS-First Mode.
web_contents->SetAlwaysSendSubresourceNotifications();
state->AllowHttpForHost(
request_url.host(),
web_contents->GetPrimaryMainFrame()->GetStoragePartition());
}
auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(web_contents);
tab_helper->set_is_navigation_upgraded(false);
tab_helper->set_is_navigation_fallback(false);
web_contents->GetController().Reload(content::ReloadType::NORMAL, false);
// The failed https navigation will remain as a forward entry, so it needs to
// be removed.
web_contents->GetController().PruneForwardEntries();
// LINT.ThenChange(chrome/browser/ssl/https_only_mode_controller_client.cc:HttpsFirstModeProceedLogic)
}
void AskBeforeHttpDialogController::TabWillDetach(
tabs::TabInterface* tab,
tabs::TabInterface::DetachReason reason) {
if (reason == tabs::TabInterface::DetachReason::kDelete &&
HasOpenDialogWidget()) {
// TODO(crbug.com/351990829): Consider adding a new `ClosedReason`
// value for this case.
CloseDialogWidget(views::Widget::ClosedReason::kUnspecified);
}
}