1 // Copyright 2013 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/passwords/password_items_view.h"
6 
7 #include <algorithm>
8 #include <memory>
9 #include <numeric>
10 #include <utility>
11 
12 #include "base/macros.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/util/type_safety/strong_alias.h"
15 #include "build/branding_buildflags.h"
16 #include "chrome/app/vector_icons/vector_icons.h"
17 #include "chrome/browser/ui/passwords/bubble_controllers/items_bubble_controller.h"
18 #include "chrome/browser/ui/passwords/manage_passwords_view_utils.h"
19 #include "chrome/browser/ui/passwords/passwords_model_delegate.h"
20 #include "chrome/browser/ui/views/chrome_layout_provider.h"
21 #include "chrome/grit/generated_resources.h"
22 #include "components/password_manager/core/browser/password_form.h"
23 #include "components/password_manager/core/common/password_manager_ui.h"
24 #include "components/vector_icons/vector_icons.h"
25 #include "ui/base/l10n/l10n_util.h"
26 #include "ui/base/models/simple_combobox_model.h"
27 #include "ui/base/resource/resource_bundle.h"
28 #include "ui/gfx/favicon_size.h"
29 #include "ui/gfx/image/image_skia.h"
30 #include "ui/gfx/paint_vector_icon.h"
31 #include "ui/gfx/range/range.h"
32 #include "ui/gfx/vector_icon_types.h"
33 #include "ui/resources/grit/ui_resources.h"
34 #include "ui/views/controls/button/button.h"
35 #include "ui/views/controls/button/image_button.h"
36 #include "ui/views/controls/button/image_button_factory.h"
37 #include "ui/views/controls/button/md_text_button.h"
38 #include "ui/views/controls/color_tracking_icon_view.h"
39 #include "ui/views/controls/image_view.h"
40 #include "ui/views/controls/label.h"
41 #include "ui/views/controls/link.h"
42 #include "ui/views/controls/separator.h"
43 #include "ui/views/layout/fill_layout.h"
44 #include "ui/views/layout/grid_layout.h"
45 #include "ui/views/style/typography.h"
46 
47 namespace {
48 
49 // Column set identifiers for displaying or undoing removal of credentials.
50 // All of them allocate space differently.
51 enum PasswordItemsViewColumnSetType {
52   // Contains three columns for credential pair and a delete button.
53   PASSWORD_COLUMN_SET,
54   // Like PASSWORD_COLUMN_SET plus a column for an icon indicating the store,
55   // and a vertical bar before the delete button.
56   MULTI_STORE_PASSWORD_COLUMN_SET,
57   // Contains two columns for text and an undo button.
58   UNDO_COLUMN_SET
59 };
60 
InferColumnSetTypeFromCredentials(const std::vector<password_manager::PasswordForm> & credentials)61 PasswordItemsViewColumnSetType InferColumnSetTypeFromCredentials(
62     const std::vector<password_manager::PasswordForm>& credentials) {
63   if (std::any_of(credentials.begin(), credentials.end(),
64                   [](const password_manager::PasswordForm& form) {
65                     return form.in_store ==
66                            password_manager::PasswordForm::Store::kAccountStore;
67                   })) {
68     return MULTI_STORE_PASSWORD_COLUMN_SET;
69   }
70   return PASSWORD_COLUMN_SET;
71 }
72 
BuildColumnSet(views::GridLayout * layout,PasswordItemsViewColumnSetType type_id)73 void BuildColumnSet(views::GridLayout* layout,
74                     PasswordItemsViewColumnSetType type_id) {
75   DCHECK(!layout->GetColumnSet(type_id));
76   views::ColumnSet* column_set = layout->AddColumnSet(type_id);
77   // Passwords are split 60/40 (6:4) as the username is more important
78   // than obscured password digits. Otherwise two columns are 50/50 (1:1).
79   constexpr float kFirstColumnWeight = 60.0f;
80   constexpr float kSecondColumnWeight = 40.0f;
81   const int between_column_padding =
82       ChromeLayoutProvider::Get()->GetDistanceMetric(
83           views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
84   // Add favicon column
85   if (type_id == PASSWORD_COLUMN_SET ||
86       type_id == MULTI_STORE_PASSWORD_COLUMN_SET) {
87     column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
88                           views::GridLayout::kFixedSize,
89                           views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
90     column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
91                                  between_column_padding);
92   }
93 
94   column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
95                         kFirstColumnWeight,
96                         views::GridLayout::ColumnSize::kFixed, 0, 0);
97 
98   if (type_id == PASSWORD_COLUMN_SET ||
99       type_id == MULTI_STORE_PASSWORD_COLUMN_SET) {
100     column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
101                                  between_column_padding);
102     column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
103                           kSecondColumnWeight,
104                           views::GridLayout::ColumnSize::kFixed, 0, 0);
105   }
106   if (type_id == MULTI_STORE_PASSWORD_COLUMN_SET) {
107     // All rows show a store indicator or leave the space blank.
108     column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
109                                  between_column_padding);
110     column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
111                           views::GridLayout::kFixedSize,
112                           views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
113     // Add a column for the vertical bar.
114     column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
115                                  between_column_padding);
116     column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER,
117                           views::GridLayout::kFixedSize,
118                           views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
119   }
120   // All rows end with a trailing column for the undo/trash button.
121   column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
122                                between_column_padding);
123   column_set->AddColumn(views::GridLayout::TRAILING, views::GridLayout::FILL,
124                         views::GridLayout::kFixedSize,
125                         views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
126 }
127 
StartRow(views::GridLayout * layout,PasswordItemsViewColumnSetType type_id)128 void StartRow(views::GridLayout* layout,
129               PasswordItemsViewColumnSetType type_id) {
130   if (!layout->GetColumnSet(type_id))
131     BuildColumnSet(layout, type_id);
132   layout->StartRow(views::GridLayout::kFixedSize, type_id);
133 }
134 
135 }  // namespace
136 
137 // An entry for each credential. Relays delete/undo actions associated with
138 // this password row to parent dialog.
139 class PasswordItemsView::PasswordRow {
140  public:
141   PasswordRow(PasswordItemsView* parent,
142               const password_manager::PasswordForm* password_form);
143 
144   void AddToLayout(views::GridLayout* layout,
145                    PasswordItemsViewColumnSetType type_id);
146 
147  private:
148   void AddUndoRow(views::GridLayout* layout);
149   void AddPasswordRow(views::GridLayout* layout,
150                       PasswordItemsViewColumnSetType type_id);
151 
152   void DeleteButtonPressed();
153   void UndoButtonPressed();
154 
155   PasswordItemsView* const parent_;
156   const password_manager::PasswordForm* const password_form_;
157   bool deleted_ = false;
158 
159   DISALLOW_COPY_AND_ASSIGN(PasswordRow);
160 };
161 
PasswordRow(PasswordItemsView * parent,const password_manager::PasswordForm * password_form)162 PasswordItemsView::PasswordRow::PasswordRow(
163     PasswordItemsView* parent,
164     const password_manager::PasswordForm* password_form)
165     : parent_(parent), password_form_(password_form) {}
166 
AddToLayout(views::GridLayout * layout,PasswordItemsViewColumnSetType type_id)167 void PasswordItemsView::PasswordRow::AddToLayout(
168     views::GridLayout* layout,
169     PasswordItemsViewColumnSetType type_id) {
170   if (deleted_)
171     AddUndoRow(layout);
172   else
173     AddPasswordRow(layout, type_id);
174 }
175 
AddUndoRow(views::GridLayout * layout)176 void PasswordItemsView::PasswordRow::AddUndoRow(views::GridLayout* layout) {
177   StartRow(layout, UNDO_COLUMN_SET);
178   layout
179       ->AddView(std::make_unique<views::Label>(
180           l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_DELETED),
181           views::style::CONTEXT_DIALOG_BODY_TEXT))
182       ->SetHorizontalAlignment(gfx::ALIGN_LEFT);
183   auto* undo_button = layout->AddView(std::make_unique<views::MdTextButton>(
184       base::BindRepeating(&PasswordRow::UndoButtonPressed,
185                           base::Unretained(this)),
186       l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_UNDO)));
187   undo_button->SetTooltipText(l10n_util::GetStringFUTF16(
188       IDS_MANAGE_PASSWORDS_UNDO_TOOLTIP, GetDisplayUsername(*password_form_)));
189 }
190 
AddPasswordRow(views::GridLayout * layout,PasswordItemsViewColumnSetType type_id)191 void PasswordItemsView::PasswordRow::AddPasswordRow(
192     views::GridLayout* layout,
193     PasswordItemsViewColumnSetType type_id) {
194   StartRow(layout, type_id);
195 
196   if (parent_->favicon_.IsEmpty()) {
197     // Use a globe fallback until the actual favicon is loaded.
198     layout->AddView(std::make_unique<views::ColorTrackingIconView>(
199         kGlobeIcon, gfx::kFaviconSize));
200   } else {
201     layout->AddView(std::make_unique<views::ImageView>())
202         ->SetImage(parent_->favicon_.AsImageSkia());
203   }
204 
205   layout->AddView(CreateUsernameLabel(*password_form_));
206   layout->AddView(CreatePasswordLabel(*password_form_));
207 
208   if (type_id == MULTI_STORE_PASSWORD_COLUMN_SET) {
209     if (password_form_->in_store ==
210         password_manager::PasswordForm::Store::kAccountStore) {
211       auto* image_view = layout->AddView(std::make_unique<views::ImageView>());
212       image_view->SetImage(gfx::CreateVectorIcon(
213 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
214           kGoogleGLogoIcon,
215 #else
216           vector_icons::kSyncIcon,
217 #endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
218           gfx::kFaviconSize, gfx::kPlaceholderColor));
219       image_view->SetAccessibleName(l10n_util::GetStringUTF16(
220           IDS_MANAGE_PASSWORDS_ACCOUNT_STORE_ICON_DESCRIPTION));
221     } else {
222       layout->SkipColumns(1);
223     }
224 
225     auto* separator = layout->AddView(std::make_unique<views::Separator>());
226     separator->SetFocusBehavior(
227         LocationBarBubbleDelegateView::FocusBehavior::NEVER);
228     separator->SetPreferredHeight(views::style::GetLineHeight(
229         views::style::CONTEXT_MENU, views::style::STYLE_SECONDARY));
230     separator->SetCanProcessEventsWithinSubtree(false);
231   }
232 
233   auto* delete_button =
234       layout->AddView(views::CreateVectorImageButtonWithNativeTheme(
235           base::BindRepeating(&PasswordRow::DeleteButtonPressed,
236                               base::Unretained(this)),
237           kTrashCanIcon));
238   delete_button->SetTooltipText(l10n_util::GetStringFUTF16(
239       IDS_MANAGE_PASSWORDS_DELETE, GetDisplayUsername(*password_form_)));
240 }
241 
DeleteButtonPressed()242 void PasswordItemsView::PasswordRow::DeleteButtonPressed() {
243   deleted_ = true;
244   parent_->NotifyPasswordFormAction(
245       *password_form_,
246       PasswordBubbleControllerBase::PasswordAction::kRemovePassword);
247 }
248 
UndoButtonPressed()249 void PasswordItemsView::PasswordRow::UndoButtonPressed() {
250   deleted_ = false;
251   parent_->NotifyPasswordFormAction(
252       *password_form_,
253       PasswordBubbleControllerBase::PasswordAction::kAddPassword);
254 }
255 
PasswordItemsView(content::WebContents * web_contents,views::View * anchor_view)256 PasswordItemsView::PasswordItemsView(content::WebContents* web_contents,
257                                      views::View* anchor_view)
258     : PasswordBubbleViewBase(web_contents,
259                              anchor_view,
260                              /*easily_dismissable=*/true),
261       controller_(PasswordsModelDelegateFromWebContents(web_contents)) {
262   SetButtons(ui::DIALOG_BUTTON_OK);
263   SetExtraView(std::make_unique<views::MdTextButton>(
264       base::BindRepeating(
265           [](PasswordItemsView* items) {
266             items->controller_.OnManageClicked(
267                 password_manager::ManagePasswordsReferrer::
268                     kManagePasswordsBubble);
269             items->CloseBubble();
270           },
271           base::Unretained(this)),
272       l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_MANAGE_PASSWORDS_BUTTON)));
273 
274   if (controller_.local_credentials().empty()) {
275     // A LayoutManager is required for GetHeightForWidth() even without
276     // content.
277     SetLayoutManager(std::make_unique<views::FillLayout>());
278   } else {
279     // The request is cancelled when the |controller_| is destructed.
280     // |controller_| has the same life time as |this| and hence it's safe to use
281     // base::Unretained(this).
282     controller_.RequestFavicon(base::BindOnce(
283         &PasswordItemsView::OnFaviconReady, base::Unretained(this)));
284     for (auto& password_form : controller_.local_credentials()) {
285       password_rows_.push_back(
286           std::make_unique<PasswordRow>(this, &password_form));
287     }
288 
289     RecreateLayout();
290   }
291 }
292 
293 PasswordItemsView::~PasswordItemsView() = default;
294 
GetController()295 PasswordBubbleControllerBase* PasswordItemsView::GetController() {
296   return &controller_;
297 }
298 
GetController() const299 const PasswordBubbleControllerBase* PasswordItemsView::GetController() const {
300   return &controller_;
301 }
302 
RecreateLayout()303 void PasswordItemsView::RecreateLayout() {
304   // This method should only be used when we have password rows, otherwise the
305   // dialog should only show the no-passwords title and doesn't need to be
306   // recreated.
307   DCHECK(!controller_.local_credentials().empty());
308 
309   RemoveAllChildViews(true);
310 
311   views::GridLayout* grid_layout =
312       SetLayoutManager(std::make_unique<views::GridLayout>());
313 
314   const int vertical_padding = ChromeLayoutProvider::Get()->GetDistanceMetric(
315       DISTANCE_CONTROL_LIST_VERTICAL);
316   bool first_row = true;
317   PasswordItemsViewColumnSetType row_column_set_type =
318       InferColumnSetTypeFromCredentials(controller_.local_credentials());
319   for (auto& row : password_rows_) {
320     if (!first_row)
321       grid_layout->AddPaddingRow(views::GridLayout::kFixedSize,
322                                  vertical_padding);
323 
324     row->AddToLayout(grid_layout, row_column_set_type);
325     first_row = false;
326   }
327 
328   PreferredSizeChanged();
329   if (GetBubbleFrameView())
330     SizeToContents();
331 }
332 
NotifyPasswordFormAction(const password_manager::PasswordForm & password_form,PasswordBubbleControllerBase::PasswordAction action)333 void PasswordItemsView::NotifyPasswordFormAction(
334     const password_manager::PasswordForm& password_form,
335     PasswordBubbleControllerBase::PasswordAction action) {
336   RecreateLayout();
337   // After the view is consistent, notify the model that the password needs to
338   // be updated (either removed or put back into the store, as appropriate.
339   controller_.OnPasswordAction(password_form, action);
340 }
341 
OnFaviconReady(const gfx::Image & favicon)342 void PasswordItemsView::OnFaviconReady(const gfx::Image& favicon) {
343   if (!favicon.IsEmpty()) {
344     favicon_ = favicon;
345     RecreateLayout();
346   }
347 }
348