1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/ui/views/webauthn/authenticator_request_dialog_view.h"
6
7 #include "base/logging.h"
8 #include "base/strings/string16.h"
9 #include "chrome/browser/ui/views/chrome_layout_provider.h"
10 #include "chrome/browser/ui/views/chrome_typography.h"
11 #include "chrome/browser/ui/views/md_text_button_with_down_arrow.h"
12 #include "chrome/browser/ui/views/webauthn/authenticator_request_sheet_view.h"
13 #include "chrome/browser/ui/views/webauthn/sheet_view_factory.h"
14 #include "chrome/browser/ui/webauthn/authenticator_request_sheet_model.h"
15 #include "chrome/grit/generated_resources.h"
16 #include "components/constrained_window/constrained_window_views.h"
17 #include "components/strings/grit/components_strings.h"
18 #include "components/web_modal/web_contents_modal_dialog_manager.h"
19 #include "components/web_modal/web_contents_modal_dialog_manager_delegate.h"
20 #include "ui/base/l10n/l10n_util.h"
21 #include "ui/gfx/color_utils.h"
22 #include "ui/gfx/geometry/insets.h"
23 #include "ui/gfx/paint_vector_icon.h"
24 #include "ui/views/border.h"
25 #include "ui/views/layout/fill_layout.h"
26 #include "ui/views/vector_icons.h"
27
28 // static
ShowAuthenticatorRequestDialog(content::WebContents * web_contents,std::unique_ptr<AuthenticatorRequestDialogModel> model)29 void ShowAuthenticatorRequestDialog(
30 content::WebContents* web_contents,
31 std::unique_ptr<AuthenticatorRequestDialogModel> model) {
32 // The authenticator request dialog will only be shown for common user-facing
33 // WebContents, which have a |manager|. Most other sources without managers,
34 // like service workers and extension background pages, do not allow WebAuthn
35 // requests to be issued in the first place.
36 // TODO(https://crbug.com/849323): There are some niche WebContents where the
37 // WebAuthn API is available, but there is no |manager| available. Currently,
38 // we will not be able to show a dialog, so the |model| will be immediately
39 // destroyed. The request may be able to still run to completion if it does
40 // not require any user input, otherise it will be blocked and time out. We
41 // should audit this.
42 auto* manager = web_modal::WebContentsModalDialogManager::FromWebContents(
43 constrained_window::GetTopLevelWebContents(web_contents));
44 if (!manager)
45 return;
46
47 new AuthenticatorRequestDialogView(web_contents, std::move(model));
48 }
49
~AuthenticatorRequestDialogView()50 AuthenticatorRequestDialogView::~AuthenticatorRequestDialogView() {
51 model_->RemoveObserver(this);
52
53 // AuthenticatorRequestDialogView is a WidgetDelegate, owned by views::Widget.
54 // It's only destroyed by Widget::OnNativeWidgetDestroyed() invoking
55 // DeleteDelegate(), and because WIDGET_OWNS_NATIVE_WIDGET, ~Widget() is
56 // invoked straight after, which destroys child views. views::View subclasses
57 // shouldn't be doing anything interesting in their destructors, so it should
58 // be okay to destroy the |sheet_| immediately after this line.
59 //
60 // However, as AuthenticatorRequestDialogModel is owned by |this|, and
61 // ObservableAuthenticatorList is owned by AuthenticatorRequestDialogModel,
62 // destroy all view components that might own models observing the list prior
63 // to destroying AuthenticatorRequestDialogModel.
64 RemoveAllChildViews(true /* delete_children */);
65 }
66
ReplaceCurrentSheetWith(std::unique_ptr<AuthenticatorRequestSheetView> new_sheet)67 void AuthenticatorRequestDialogView::ReplaceCurrentSheetWith(
68 std::unique_ptr<AuthenticatorRequestSheetView> new_sheet) {
69 DCHECK(new_sheet);
70
71 other_transports_menu_runner_.reset();
72
73 delete sheet_;
74 DCHECK(children().empty());
75
76 sheet_ = new_sheet.get();
77 AddChildView(new_sheet.release());
78
79 UpdateUIForCurrentSheet();
80 }
81
UpdateUIForCurrentSheet()82 void AuthenticatorRequestDialogView::UpdateUIForCurrentSheet() {
83 DCHECK(sheet_);
84
85 sheet_->ReInitChildViews();
86
87 int buttons = ui::DIALOG_BUTTON_NONE;
88 if (sheet()->model()->IsAcceptButtonVisible())
89 buttons |= ui::DIALOG_BUTTON_OK;
90 if (sheet()->model()->IsCancelButtonVisible())
91 buttons |= ui::DIALOG_BUTTON_CANCEL;
92 SetButtons(buttons);
93 SetDefaultButton((buttons & ui::DIALOG_BUTTON_OK) ? ui::DIALOG_BUTTON_OK
94 : ui::DIALOG_BUTTON_NONE);
95 SetButtonLabel(ui::DIALOG_BUTTON_OK, sheet_->model()->GetAcceptButtonLabel());
96 SetButtonLabel(ui::DIALOG_BUTTON_CANCEL,
97 sheet_->model()->GetCancelButtonLabel());
98
99 // Whether to show the `Choose another option` button, or other dialog
100 // configuration is delegated to the |sheet_|, and the new sheet likely wants
101 // to provide a new configuration.
102 ToggleOtherTransportsButtonVisibility();
103 DialogModelChanged();
104
105 // If the widget is not yet shown or already being torn down, we are done. In
106 // the former case, sizing/layout will happen once the dialog is visible.
107 if (!GetWidget())
108 return;
109
110 // Force re-layout of the entire dialog client view, which includes the sheet
111 // content as well as the button row on the bottom.
112 // TODO(ellyjones): Why is this necessary?
113 GetWidget()->GetRootView()->Layout();
114
115 // The accessibility title is also sourced from the |sheet_|'s step title.
116 GetWidget()->UpdateWindowTitle();
117
118 // TODO(https://crbug.com/849323): Investigate how a web-modal dialog's
119 // lifetime compares to that of the parent WebContents. Take a conservative
120 // approach for now.
121 if (!web_contents())
122 return;
123
124 // The |dialog_manager| might temporarily be unavailable while the tab is
125 // being dragged from one browser window to the other.
126 auto* dialog_manager =
127 web_modal::WebContentsModalDialogManager::FromWebContents(
128 constrained_window::GetTopLevelWebContents(web_contents()));
129 if (!dialog_manager)
130 return;
131
132 // Update the dialog size and position, as the preferred size of the sheet
133 // might have changed.
134 constrained_window::UpdateWebContentsModalDialogPosition(
135 GetWidget(), dialog_manager->delegate()->GetWebContentsModalDialogHost());
136
137 // Reset focus to the highest priority control on the new/updated sheet.
138 if (GetInitiallyFocusedView())
139 GetInitiallyFocusedView()->RequestFocus();
140 }
141
ToggleOtherTransportsButtonVisibility()142 void AuthenticatorRequestDialogView::ToggleOtherTransportsButtonVisibility() {
143 other_transports_button_->SetVisible(ShouldOtherTransportsButtonBeVisible());
144 }
145
ShouldOtherTransportsButtonBeVisible() const146 bool AuthenticatorRequestDialogView::ShouldOtherTransportsButtonBeVisible()
147 const {
148 return sheet_->model()->GetOtherTransportsMenuModel() &&
149 sheet_->model()->GetOtherTransportsMenuModel()->GetItemCount();
150 }
151
CalculatePreferredSize() const152 gfx::Size AuthenticatorRequestDialogView::CalculatePreferredSize() const {
153 const int width = ChromeLayoutProvider::Get()->GetDistanceMetric(
154 views::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH);
155 return gfx::Size(width, GetHeightForWidth(width));
156 }
157
Accept()158 bool AuthenticatorRequestDialogView::Accept() {
159 sheet()->model()->OnAccept();
160 return false;
161 }
162
Cancel()163 bool AuthenticatorRequestDialogView::Cancel() {
164 sheet()->model()->OnCancel();
165 return false;
166 }
167
IsDialogButtonEnabled(ui::DialogButton button) const168 bool AuthenticatorRequestDialogView::IsDialogButtonEnabled(
169 ui::DialogButton button) const {
170 switch (button) {
171 case ui::DIALOG_BUTTON_NONE:
172 break;
173 case ui::DIALOG_BUTTON_OK:
174 return sheet()->model()->IsAcceptButtonEnabled();
175 case ui::DIALOG_BUTTON_CANCEL:
176 return true; // Cancel is always enabled if visible.
177 }
178 NOTREACHED();
179 return false;
180 }
181
GetInitiallyFocusedView()182 views::View* AuthenticatorRequestDialogView::GetInitiallyFocusedView() {
183 // Need to provide a custom implementation, as most dialog sheets will not
184 // have a default button which gets initial focus. The focus priority is:
185 // 1. Step-specific content, e.g. transport selection list, if any.
186 // 2. Accept button, if visible and enabled.
187 // 3. Other transport selection button, if visible.
188 // 4. `Cancel` / `Close` button.
189
190 views::View* intially_focused_sheet_control =
191 sheet()->GetInitiallyFocusedView();
192 if (intially_focused_sheet_control)
193 return intially_focused_sheet_control;
194
195 if (sheet()->model()->IsAcceptButtonVisible() &&
196 sheet()->model()->IsAcceptButtonEnabled()) {
197 return GetOkButton();
198 }
199
200 if (ShouldOtherTransportsButtonBeVisible())
201 return other_transports_button_;
202
203 if (sheet()->model()->IsCancelButtonVisible())
204 return GetCancelButton();
205
206 return nullptr;
207 }
208
GetModalType() const209 ui::ModalType AuthenticatorRequestDialogView::GetModalType() const {
210 return ui::MODAL_TYPE_CHILD;
211 }
212
GetWindowTitle() const213 base::string16 AuthenticatorRequestDialogView::GetWindowTitle() const {
214 return sheet()->model()->GetStepTitle();
215 }
216
ShouldShowWindowTitle() const217 bool AuthenticatorRequestDialogView::ShouldShowWindowTitle() const {
218 return false;
219 }
220
ShouldShowCloseButton() const221 bool AuthenticatorRequestDialogView::ShouldShowCloseButton() const {
222 return false;
223 }
224
OnModelDestroyed()225 void AuthenticatorRequestDialogView::OnModelDestroyed() {
226 NOTREACHED();
227 }
228
OnStepTransition()229 void AuthenticatorRequestDialogView::OnStepTransition() {
230 if (model_->should_dialog_be_closed()) {
231 if (!first_shown_) {
232 // No widget has ever been created for this dialog, thus there will be no
233 // DeleteDelegate() call to delete this view.
234 DCHECK(!GetWidget());
235 delete this;
236 return;
237 }
238 if (GetWidget()) {
239 GetWidget()->Close(); // DeleteDelegate() will delete |this|.
240 }
241 return;
242 }
243 if (model_->should_dialog_be_hidden()) {
244 if (GetWidget()) {
245 GetWidget()->Hide();
246 }
247 return;
248 }
249
250 ReplaceCurrentSheetWith(CreateSheetViewForCurrentStepOf(model_.get()));
251 Show();
252 }
253
OnSheetModelChanged()254 void AuthenticatorRequestDialogView::OnSheetModelChanged() {
255 UpdateUIForCurrentSheet();
256 }
257
OnVisibilityChanged(content::Visibility visibility)258 void AuthenticatorRequestDialogView::OnVisibilityChanged(
259 content::Visibility visibility) {
260 const bool web_contents_was_hidden = web_contents_hidden_;
261 web_contents_hidden_ = visibility == content::Visibility::HIDDEN;
262
263 // Show() does not actually show the dialog while the parent WebContents are
264 // hidden. Instead, show it when the WebContents become visible again.
265 if (web_contents_was_hidden && !web_contents_hidden_ &&
266 !model_->should_dialog_be_hidden() && !GetWidget()->IsVisible()) {
267 GetWidget()->Show();
268 }
269 }
270
AuthenticatorRequestDialogView(content::WebContents * web_contents,std::unique_ptr<AuthenticatorRequestDialogModel> model)271 AuthenticatorRequestDialogView::AuthenticatorRequestDialogView(
272 content::WebContents* web_contents,
273 std::unique_ptr<AuthenticatorRequestDialogModel> model)
274 : content::WebContentsObserver(web_contents),
275 model_(std::move(model)),
276 sheet_(nullptr),
277 other_transports_button_(
278 SetExtraView(std::make_unique<views::MdTextButtonWithDownArrow>(
279 base::BindRepeating(
280 &AuthenticatorRequestDialogView::OtherTransportsButtonPressed,
281 base::Unretained(this)),
282 l10n_util::GetStringUTF16(IDS_WEBAUTHN_TRANSPORT_POPUP_LABEL)))),
283 web_contents_hidden_(web_contents->GetVisibility() ==
284 content::Visibility::HIDDEN) {
285 DCHECK(!model_->should_dialog_be_closed());
286 model_->AddObserver(this);
287
288 SetCloseCallback(
289 base::BindOnce(&AuthenticatorRequestDialogView::OnDialogClosing,
290 base::Unretained(this)));
291
292 // Currently, all sheets have a label on top and controls at the bottom.
293 // Consider moving this to AuthenticatorRequestSheetView if this changes.
294 SetLayoutManager(std::make_unique<views::FillLayout>());
295
296 OnStepTransition();
297 }
298
Show()299 void AuthenticatorRequestDialogView::Show() {
300 if (!first_shown_) {
301 constrained_window::ShowWebModalDialogViews(this, web_contents());
302 DCHECK(GetWidget());
303 first_shown_ = true;
304 return;
305 }
306
307 if (web_contents_hidden_) {
308 // Calling Widget::Show() while the tab is not in foreground shows the
309 // dialog on the foreground tab (https://crbug/969153). Instead, wait for
310 // OnVisibilityChanged() to signal the tab going into foreground again, and
311 // then show the widget.
312 return;
313 }
314
315 GetWidget()->Show();
316 }
317
OtherTransportsButtonPressed()318 void AuthenticatorRequestDialogView::OtherTransportsButtonPressed() {
319 auto* other_transports_menu_model =
320 sheet_->model()->GetOtherTransportsMenuModel();
321 DCHECK(other_transports_menu_model);
322 DCHECK_GE(other_transports_menu_model->GetItemCount(), 1);
323
324 other_transports_menu_runner_ = std::make_unique<views::MenuRunner>(
325 other_transports_menu_model, views::MenuRunner::COMBOBOX);
326
327 gfx::Rect anchor_bounds = other_transports_button_->GetBoundsInScreen();
328 other_transports_menu_runner_->RunMenuAt(
329 other_transports_button_->GetWidget(), nullptr /* MenuButtonController */,
330 anchor_bounds, views::MenuAnchorPosition::kTopLeft,
331 ui::MENU_SOURCE_MOUSE);
332 }
333
OnDialogClosing()334 void AuthenticatorRequestDialogView::OnDialogClosing() {
335 // To keep the UI responsive, always allow immediately closing the dialog when
336 // desired; but still trigger cancelling the AuthenticatorRequest unless it is
337 // already complete.
338 //
339 // Note that on most sheets, cancelling will immediately destroy the request,
340 // so this method will be re-entered like so:
341 //
342 // AuthenticatorRequestDialogView::Close()
343 // views::DialogClientView::CanClose()
344 // views::Widget::Close()
345 // AuthenticatorRequestDialogView::OnStepTransition()
346 // AuthenticatorRequestDialogModel::SetCurrentStep()
347 // AuthenticatorRequestDialogModel::OnRequestComplete()
348 // ChromeAuthenticatorRequestDelegate::~ChromeAuthenticatorRequestDelegate()
349 // content::AuthenticatorImpl::InvokeCallbackAndCleanup()
350 // content::AuthenticatorImpl::FailWithNotAllowedErrorAndCleanup()
351 // <<invoke callback>>
352 // ChromeAuthenticatorRequestDelegate::OnCancelRequest()
353 // AuthenticatorRequestDialogModel::Cancel()
354 // AuthenticatorRequestDialogView::Cancel()
355 // AuthenticatorRequestDialogView::Close() [initial call]
356 //
357 // This should not be a problem as the native widget will never synchronously
358 // close and hence not synchronously destroy the model while it's iterating
359 // over observers in SetCurrentStep().
360 if (!model_->should_dialog_be_closed())
361 Cancel();
362 }
363