| // 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/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_->SetAccessibleName(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_->SetAccessibleName(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::DIALOG_BUTTON_OK, |
| 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_.SetWidgetOwnsNativeWidget(); |
| |
| dialog_delegate_.SetModalType(ui::MODAL_TYPE_CHILD); |
| 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); |
| |
| 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 |