// 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/http_auth_dialog.h"
#include <vector>
#include "base/no_destructor.h"
#include "base/observer_list.h"
#include "base/task/sequenced_task_runner.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/elide_url.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/browser_thread.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/layout/table_layout_view.h"
namespace ash {
namespace {
// This dialog is used in place of the browser http auth dialog when
// `g_enable_count` >= 1. Once Lacros ships, this feature can be enabled always.
static int g_enable_count = 0;
// The distance between vertical controls.
constexpr int kDistanceControlListVertical = 12;
// All HttpAuthDialogs should be tracked in this global singleton for testing.
using HttpAuthDialogVector = std::vector<HttpAuthDialog*>;
HttpAuthDialogVector& GetAllDialogs() {
static base::NoDestructor<HttpAuthDialogVector> instance;
return *instance;
}
// All observers should be tracked in this global singleton.
using Observers = base::ObserverList<HttpAuthDialog::Observer>;
Observers& GetObservers() {
static base::NoDestructor<Observers> instance;
return *instance;
}
// Computes `authority` and `explanation`.
void GetDialogStrings(const GURL& request_url,
const net::AuthChallengeInfo& auth_info,
std::u16string* authority,
std::u16string* explanation) {
GURL authority_url;
if (auth_info.is_proxy) {
*authority = l10n_util::GetStringFUTF16(
IDS_LOGIN_DIALOG_PROXY_AUTHORITY,
url_formatter::FormatUrlForSecurityDisplay(
auth_info.challenger.GetURL(), url_formatter::SchemeDisplay::SHOW));
authority_url = auth_info.challenger.GetURL();
} else {
*authority = url_formatter::FormatUrlForSecurityDisplay(request_url);
authority_url = request_url;
}
if (!network::IsUrlPotentiallyTrustworthy(authority_url)) {
*explanation = l10n_util::GetStringUTF16(IDS_LOGIN_DIALOG_NOT_PRIVATE);
} else {
explanation->clear();
}
}
} // namespace
HttpAuthDialog::~HttpAuthDialog() {
// Book-keeping for test-only data-structures.
auto& dialogs = GetAllDialogs();
auto it = std::find(dialogs.begin(), dialogs.end(), this);
DCHECK(it != dialogs.end());
dialogs.erase(it);
// The widget will be destroyed soon, so we must first clear raw_ptrs owned by
// the widget.
dialog_view_ = nullptr;
}
HttpAuthDialog::ScopedEnabler::ScopedEnabler() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
++g_enable_count;
}
HttpAuthDialog::ScopedEnabler::~ScopedEnabler() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
--g_enable_count;
}
std::unique_ptr<HttpAuthDialog::ScopedEnabler> HttpAuthDialog::Enable() {
return std::make_unique<ScopedEnabler>();
}
// static
bool HttpAuthDialog::IsEnabled() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return g_enable_count >= 1;
}
// static
std::unique_ptr<HttpAuthDialog> HttpAuthDialog::Create(
const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents,
const GURL& url,
LoginAuthRequiredCallback auth_required_callback) {
// This class cannot handle UI-less auth dialog requests. Once Lacros ships,
// this should no longer be possible and this can become a CHECK.
if (!web_contents) {
return nullptr;
}
// Anchor to the outermost WebContents, for e.g. embedded <webview>s.
web_contents = web_contents->GetOutermostWebContents();
// Skip if the WebContents instance is not prepared to show a dialog.
if (!web_modal::WebContentsModalDialogManager::FromWebContents(
web_contents)) {
LOG(ERROR) << "Skipping HttpAuthDialog, url=" << url.possibly_invalid_spec()
<< ", web_contents?" << !!web_contents;
base::debug::DumpWithoutCrashing();
return nullptr;
}
// The constructor is private. There is no portable way to expose the
// constructor to std::make_unique.
return base::WrapUnique(new HttpAuthDialog(
auth_info, web_contents, url, std::move(auth_required_callback)));
}
// static
void HttpAuthDialog::AddObserver(Observer* observer) {
GetObservers().AddObserver(observer);
}
// static
void HttpAuthDialog::RemoveObserver(Observer* observer) {
GetObservers().RemoveObserver(observer);
}
// static
std::vector<HttpAuthDialog*> HttpAuthDialog::GetAllDialogsForTest() {
return GetAllDialogs();
}
void HttpAuthDialog::SupplyCredentialsForTest(std::u16string_view username,
std::u16string_view password) {
dialog_view_->SetCredentialsForTest(std::move(username), std::move(password));
dialog_delegate_.AcceptDialog();
}
void HttpAuthDialog::CancelForTest() {
dialog_delegate_.CancelDialog();
}
HttpAuthDialog::DialogView::DialogView(std::u16string_view authority,
std::u16string_view explanation) {
std::u16string authority_string(authority);
std::u16string explanation_string(explanation);
views::LayoutProvider* provider = views::LayoutProvider::Get();
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
provider->GetDialogInsetsForContentType(
views::DialogContentType::kText, views::DialogContentType::kControl),
provider->GetDistanceMetric(views::DISTANCE_UNRELATED_CONTROL_VERTICAL)));
auto* authority_container =
AddChildView(std::make_unique<views::BoxLayoutView>());
authority_container->SetOrientation(views::BoxLayout::Orientation::kVertical);
auto* authority_label =
authority_container->AddChildView(std::make_unique<views::Label>(
authority_string, views::style::CONTEXT_LABEL,
views::style::STYLE_PRIMARY));
authority_label->SetMultiLine(true);
constexpr int kMessageWidth = 320;
authority_label->SetMaximumWidth(kMessageWidth);
authority_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
authority_label->SetAllowCharacterBreak(true);
if (!explanation_string.empty()) {
auto* explanation_label =
authority_container->AddChildView(std::make_unique<views::Label>(
explanation_string, views::style::CONTEXT_LABEL,
views::style::STYLE_SECONDARY));
explanation_label->SetMultiLine(true);
explanation_label->SetMaximumWidth(kMessageWidth);
explanation_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
}
auto* fields_container =
AddChildView(std::make_unique<views::TableLayoutView>());
fields_container
->AddColumn(views::LayoutAlignment::kStart,
views::LayoutAlignment::kCenter,
views::TableLayout::kFixedSize,
views::TableLayout::ColumnSize::kUsePreferred, 0, 0)
.AddPaddingColumn(
views::TableLayout::kFixedSize,
provider->GetDistanceMetric(views::DISTANCE_RELATED_LABEL_HORIZONTAL))
.AddColumn(views::LayoutAlignment::kStretch,
views::LayoutAlignment::kStretch, 1.0,
views::TableLayout::ColumnSize::kFixed, 0, 0)
.AddRows(1, views::TableLayout::kFixedSize)
.AddPaddingRow(views::TableLayout::kFixedSize,
kDistanceControlListVertical)
.AddRows(1, views::TableLayout::kFixedSize);
auto* username_label =
fields_container->AddChildView(std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_LOGIN_DIALOG_USERNAME_FIELD),
views::style::CONTEXT_LABEL, views::style::STYLE_PRIMARY));
username_field_ =
fields_container->AddChildView(std::make_unique<views::Textfield>());
username_field_->GetViewAccessibility().SetName(*username_label);
auto* password_label =
fields_container->AddChildView(std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_LOGIN_DIALOG_PASSWORD_FIELD),
views::style::CONTEXT_LABEL, views::style::STYLE_PRIMARY));
password_field_ =
fields_container->AddChildView(std::make_unique<views::Textfield>());
password_field_->GetViewAccessibility().SetName(*password_label);
password_field_->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD);
}
HttpAuthDialog::DialogView::~DialogView() = default;
// Access the data in the username/password text fields.
std::u16string HttpAuthDialog::DialogView::GetUsername() const {
return username_field_->GetText();
}
std::u16string HttpAuthDialog::DialogView::GetPassword() const {
return password_field_->GetText();
}
void HttpAuthDialog::DialogView::SetCredentialsForTest(
std::u16string_view username,
std::u16string_view password) {
std::u16string username_string(username);
std::u16string password_string(password);
username_field_->SetText(username_string);
password_field_->SetText(password_string);
}
views::View* HttpAuthDialog::DialogView::GetInitiallyFocusedView() {
return username_field_;
}
HttpAuthDialog::HttpAuthDialog(const net::AuthChallengeInfo& auth_info,
content::WebContents* web_contents,
const GURL& url,
LoginAuthRequiredCallback auth_required_callback)
: callback_(std::move(auth_required_callback)),
web_contents_(web_contents) {
CHECK(!callback_.is_null());
GetAllDialogs().push_back(this);
dialog_delegate_.SetButtonLabel(
ui::mojom::DialogButton::kOk,
l10n_util::GetStringUTF16(IDS_LOGIN_DIALOG_OK_BUTTON_LABEL));
dialog_delegate_.SetAcceptCallback(base::BindOnce(
[](base::WeakPtr<HttpAuthDialog> dialog) {
if (!dialog) {
return;
}
dialog->SupplyCredentials(dialog->dialog_view_->GetUsername(),
dialog->dialog_view_->GetPassword());
},
weak_factory_.GetWeakPtr()));
auto close_callback =
base::BindOnce(&HttpAuthDialog::Cancel, weak_factory_.GetWeakPtr());
// WindowClosing callback is guaranteed to be called regardless of whether the
// dialog is closed by the user or the OS.
dialog_delegate_.RegisterWindowClosingCallback(std::move(close_callback));
dialog_delegate_.SetOwnershipOfNewWidget(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
dialog_delegate_.SetModalType(ui::mojom::ModalType::kChild);
dialog_delegate_.SetShowCloseButton(false);
dialog_delegate_.SetTitle(l10n_util::GetStringUTF16(IDS_LOGIN_DIALOG_TITLE));
std::u16string authority;
std::u16string explanation;
GetDialogStrings(url, auth_info, &authority, &explanation);
dialog_view_ = dialog_delegate_.SetContentsView(
std::make_unique<DialogView>(authority, explanation));
dialog_delegate_.SetInitiallyFocusedView(
dialog_view_->GetInitiallyFocusedView());
dialog_widget_ = constrained_window::ShowWebModalDialogViewsOwned(
&dialog_delegate_, web_contents,
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
NotifyShownAsync(web_contents_);
}
void HttpAuthDialog::SupplyCredentials(std::u16string_view username,
std::u16string_view password) {
std::u16string username_string(username);
std::u16string password_string(password);
net::AuthCredentials credentials(username_string, password_string);
CHECK(!callback_.is_null());
NotifySuppliedAsync(web_contents_);
// Running `callback_` can result in synchronous destruction of this object.
// We dispatch the call to avoid re-entrancy, as this method itself can be
// synchronously invoked as a callback.
auto run_callback = base::BindOnce(
[](base::WeakPtr<HttpAuthDialog> dialog,
LoginAuthRequiredCallback callback, net::AuthCredentials credentials) {
if (dialog) {
std::move(callback).Run(std::move(credentials));
}
},
weak_factory_.GetWeakPtr(), std::move(callback_), std::move(credentials));
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(run_callback));
}
void HttpAuthDialog::Cancel() {
NotifyCancelledAsync(web_contents_);
// Running `callback_` can result in synchronous destruction of this object.
// We dispatch the call to avoid re-entrancy, as this method itself can be
// synchronously invoked as a callback.
auto run_callback = base::BindOnce(
[](base::WeakPtr<HttpAuthDialog> dialog,
LoginAuthRequiredCallback callback) {
if (dialog) {
std::move(callback).Run(std::nullopt);
}
},
weak_factory_.GetWeakPtr(), std::move(callback_));
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(run_callback));
}
// static
void HttpAuthDialog::NotifyShownAsync(content::WebContents* web_contents) {
auto callback = base::BindOnce(
[](base::WeakPtr<content::WebContents> web_contents) {
for (auto& observer : GetObservers()) {
observer.HttpAuthDialogShown(web_contents ? web_contents.get()
: nullptr);
}
},
web_contents->GetWeakPtr());
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
std::move(callback));
}
// static
void HttpAuthDialog::NotifySuppliedAsync(content::WebContents* web_contents) {
auto callback = base::BindOnce(
[](base::WeakPtr<content::WebContents> web_contents) {
for (auto& observer : GetObservers()) {
observer.HttpAuthDialogSupplied(web_contents ? web_contents.get()
: nullptr);
}
},
web_contents->GetWeakPtr());
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
std::move(callback));
}
// static
void HttpAuthDialog::NotifyCancelledAsync(content::WebContents* web_contents) {
auto callback = base::BindOnce(
[](base::WeakPtr<content::WebContents> web_contents) {
for (auto& observer : GetObservers()) {
observer.HttpAuthDialogCancelled(web_contents ? web_contents.get()
: nullptr);
}
},
web_contents->GetWeakPtr());
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
std::move(callback));
}
} // namespace ash