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