[go: nahoru, domu]

blob: 6b1d22d21956fd87b5cb10a7f638f98ac3e66823 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issues_table_view_controller.h"
#import <UIKit/UIKit.h>
#import "base/apple/foundation_util.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "ios/chrome/browser/passwords/password_checkup_metrics.h"
#import "ios/chrome/browser/passwords/password_checkup_utils.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_multi_detail_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_favicon_data_source.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issue_content_item.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issues_consumer.h"
#import "ios/chrome/browser/ui/settings/password/password_issues/password_issues_presenter.h"
#import "ios/chrome/browser/ui/settings/password/passwords_table_view_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
using password_manager::WarningType;
using password_manager::features::IsPasswordCheckupEnabled;
namespace {
// Vertical spacing between password issue items.
constexpr CGFloat kVerticalSpacingBetweenItems = 8;
typedef NS_ENUM(NSInteger, SectionIdentifier) {
SectionIdentifierHeader = kSectionIdentifierEnumZero,
SectionIdentifierContent,
SectionIdentifierDismissedCredentialsButton,
// Identifier of the section containing the first password issue when Password
// Checkup is enabled. Subsequent password issues use incremental section
// identifiers, as each issue goes in a separate section. To avoid section
// identifiers collisions, `SectionIdentifierFirstPasswordIssue` should be
// the last value in `SectionIdentifier` and any new identifiers must be
// defined above it.
SectionIdentifierFirstPasswordIssue,
// Do not add more values here. See comment above.
};
typedef NS_ENUM(NSInteger, ItemType) {
ItemTypeHeader = kItemTypeEnumZero,
ItemTypePassword, // This is a repeated item type.
ItemTypePasswordHeader, // This is a repeated item type.
ItemTypeChangePassword, // This is a repeated item type.
ItemTypeDismissedCredentialsButton,
};
} // namespace
@interface PasswordIssuesTableViewController () {
// Text of the header displayed on top of the page.
NSString* _headerText;
// URL of link in the page header. Nullable.
CrURL* _headerURL;
// Insecure password issues displayed in the tableView.
// Reused password issues are displayed in groups of same-password credentials
// with a text header on top of the first password issue in the group. All
// other types of issues are displayed in the same group without header.
NSArray<PasswordIssueGroup*>* _passwordGroups;
// Number in the button for presenting dismissed compromised
// credential warnings. When zero, no button is displayed.
NSInteger _dismissedWarningsCount;
// Type of insecure credentials displayed in the page.
WarningType _warningType;
}
@end
@implementation PasswordIssuesTableViewController
- (instancetype)initWithWarningType:(WarningType)warningType {
self = [super initWithStyle:ChromeTableViewStyle()];
if (self) {
_warningType = warningType;
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.accessibilityIdentifier = kPasswordIssuesTableViewId;
[self loadModel];
}
- (void)didMoveToParentViewController:(UIViewController*)parent {
[super didMoveToParentViewController:parent];
if (!parent) {
[self.presenter dismissPasswordIssuesTableViewController];
}
}
#pragma mark - ChromeTableViewController
- (void)loadModel {
[super loadModel];
if (!IsPasswordCheckupEnabled()) {
[self loadModelLegacy];
return;
}
TableViewModel* model = self.tableViewModel;
TableViewLinkHeaderFooterItem* headerItem = [self headerItem];
if (headerItem) {
[model addSectionWithIdentifier:SectionIdentifierHeader];
[model setHeader:headerItem
forSectionWithIdentifier:SectionIdentifierHeader];
}
// Add password issues to their own separate sections.
__block NSInteger nextPasswordIssueSectionIdentifier =
SectionIdentifierFirstPasswordIssue;
for (PasswordIssueGroup* issueGroup in _passwordGroups) {
[issueGroup.passwordIssues
enumerateObjectsUsingBlock:^(PasswordIssue* passwordIssue,
NSUInteger index, BOOL* stop) {
// Create section for next password issue.
[model addSectionWithIdentifier:nextPasswordIssueSectionIdentifier];
// Add header on top of first issue if the issue group has a header.
if (index == 0 && issueGroup.headerText) {
[model setHeader:[self passwordIssueGroupHeaderItemWithText:
issueGroup.headerText]
forSectionWithIdentifier:nextPasswordIssueSectionIdentifier];
}
// Add password issue.
[model addItem:[self passwordIssueItem:passwordIssue]
toSectionWithIdentifier:nextPasswordIssueSectionIdentifier];
if (passwordIssue.changePasswordURL.has_value()) {
// Add change password button below password issue.
[model addItem:[self changePasswordItem]
toSectionWithIdentifier:nextPasswordIssueSectionIdentifier];
}
// Increment section identifier for next password issue.
nextPasswordIssueSectionIdentifier++;
}];
}
TableViewMultiDetailTextItem* dismissedWarningsItem =
[self dismissedWarningsItem];
if (dismissedWarningsItem) {
[model
addSectionWithIdentifier:SectionIdentifierDismissedCredentialsButton];
[model addItem:dismissedWarningsItem
toSectionWithIdentifier:SectionIdentifierDismissedCredentialsButton];
}
}
// Legacy loadModel logic used when Password Checkup Feature is not enabled.
- (void)loadModelLegacy {
CHECK(!IsPasswordCheckupEnabled());
TableViewModel* model = self.tableViewModel;
[model addSectionWithIdentifier:SectionIdentifierContent];
TableViewLinkHeaderFooterItem* headerItem = [self headerItem];
if (headerItem) {
[model setHeader:headerItem
forSectionWithIdentifier:SectionIdentifierContent];
}
for (PasswordIssue* password in _passwordGroups.firstObject.passwordIssues) {
[model addItem:[self passwordIssueItem:password]
toSectionWithIdentifier:SectionIdentifierContent];
}
}
#pragma mark - Items
- (TableViewLinkHeaderFooterItem*)headerItem {
if (!_headerText) {
return nil;
}
TableViewLinkHeaderFooterItem* headerItem =
[[TableViewLinkHeaderFooterItem alloc] initWithType:ItemTypeHeader];
headerItem.text = _headerText;
if (_headerURL) {
headerItem.urls = @[ _headerURL ];
}
return headerItem;
}
- (PasswordIssueContentItem*)passwordIssueItem:(PasswordIssue*)password {
PasswordIssueContentItem* passwordItem =
[[PasswordIssueContentItem alloc] initWithType:ItemTypePassword];
passwordItem.password = password;
passwordItem.accessibilityTraits |= UIAccessibilityTraitButton;
passwordItem.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return passwordItem;
}
// Creates a header for displaying on top of a group of password issues.
- (TableViewLinkHeaderFooterItem*)passwordIssueGroupHeaderItemWithText:
(NSString*)headerText {
TableViewLinkHeaderFooterItem* groupHeaderItem =
[[TableViewLinkHeaderFooterItem alloc]
initWithType:ItemTypePasswordHeader];
groupHeaderItem.text = headerText;
return groupHeaderItem;
}
// Creates an item acting as a button for changing an insecure password in its
// corresponding website.
- (TableViewTextItem*)changePasswordItem {
TableViewTextItem* item =
[[TableViewTextItem alloc] initWithType:ItemTypeChangePassword];
item.text = l10n_util::GetNSString(IDS_IOS_CHANGE_COMPROMISED_PASSWORD);
item.textColor = [UIColor colorNamed:kBlueColor];
item.accessibilityTraits = UIAccessibilityTraitButton;
return item;
}
// Creates the item acting as a button for presenting dismissed compromised
// credential warnings. Returns nil when `_dismissedWarningsCount` is zero.
- (TableViewMultiDetailTextItem*)dismissedWarningsItem {
// The button is not visible either because there aren't dismissed compromised
// credentials or because the view controller is not showing compromised
// credentials.
if (_dismissedWarningsCount == 0) {
return nil;
}
TableViewMultiDetailTextItem* dismissedWarningsItem =
[[TableViewMultiDetailTextItem alloc]
initWithType:ItemTypeDismissedCredentialsButton];
dismissedWarningsItem.text = l10n_util::GetNSString(
IDS_IOS_COMPROMISED_PASSWORD_ISSUES_DISMISSED_WARNINGS_BUTTON_TITLE);
dismissedWarningsItem.trailingDetailText =
[@(_dismissedWarningsCount) stringValue];
dismissedWarningsItem.accessibilityTraits = UIAccessibilityTraitButton;
dismissedWarningsItem.accessoryType =
UITableViewCellAccessoryDisclosureIndicator;
dismissedWarningsItem.accessibilityIdentifier = kDismissedWarningsCellId;
return dismissedWarningsItem;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView*)tableView
didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
[super tableView:tableView didSelectRowAtIndexPath:indexPath];
TableViewModel* model = self.tableViewModel;
ItemType itemType =
static_cast<ItemType>([model itemTypeForIndexPath:indexPath]);
switch (itemType) {
case ItemTypeHeader:
case ItemTypePasswordHeader:
break;
case ItemTypePassword: {
PasswordIssueContentItem* passwordIssue =
base::apple::ObjCCastStrict<PasswordIssueContentItem>(
[model itemAtIndexPath:indexPath]);
[self.presenter presentPasswordIssueDetails:passwordIssue.password];
break;
}
case ItemTypeDismissedCredentialsButton:
password_manager::LogOpenPasswordIssuesList(
WarningType::kDismissedWarningsWarning);
[self.presenter presentDismissedCompromisedCredentials];
break;
case ItemTypeChangePassword:
password_manager::LogChangePasswordOnWebsite(_warningType);
CrURL* changePasswordURL =
[self changePasswordURLForPasswordInSection:indexPath.section];
[self.presenter dismissAndOpenURL:changePasswordURL];
break;
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
UITableViewCell* cell = [super tableView:tableView
cellForRowAtIndexPath:indexPath];
switch ([self.tableViewModel itemTypeForIndexPath:indexPath]) {
case ItemTypePassword: {
TableViewURLCell* urlCell =
base::apple::ObjCCastStrict<TableViewURLCell>(cell);
urlCell.textLabel.lineBreakMode = NSLineBreakByTruncatingHead;
// Load the favicon from cache.
[self loadFaviconAtIndexPath:indexPath forCell:cell];
break;
}
}
return cell;
}
- (UIView*)tableView:(UITableView*)tableView
viewForHeaderInSection:(NSInteger)section {
UIView* view = [super tableView:tableView viewForHeaderInSection:section];
if (section == 0 && [self.tableViewModel headerForSectionIndex:0]) {
// Attach self as delegate to handle clicks in page header.
TableViewLinkHeaderFooterView* headerView =
base::apple::ObjCCastStrict<TableViewLinkHeaderFooterView>(view);
headerView.delegate = self;
}
return view;
}
- (CGFloat)tableView:(UITableView*)tableView
heightForFooterInSection:(NSInteger)section {
TableViewModel* model = self.tableViewModel;
// Calculate the actual height of the footer view if there's one.
if ([model footerForSectionIndex:section]) {
return UITableViewAutomaticDimension;
}
NSInteger sectionIdentifier =
[model sectionIdentifierForSectionIndex:section];
switch (sectionIdentifier) {
case SectionIdentifierHeader:
// Add an empty footer of 8pt height so added up to the table view header
// bottom padding (8pt) and the first section's header (either an empty
// 8pt header or an actual header that has a 8pt top padding) achieves the
// desired 24pt spacing between the table view header and the next element
// below it.
return kVerticalSpacingBetweenItems;
case SectionIdentifierContent:
// Vertical spacing between the last item and its container in the legacy
// layout.
return kVerticalSpacingBetweenItems;
case SectionIdentifierDismissedCredentialsButton:
// Spacing between dismiss button and the bottom of the scrollable area.
return kVerticalSpacingBetweenItems;
default:
// Handle password issue sections.
// All other sections should be handled by now.
CHECK_GE(sectionIdentifier, SectionIdentifierFirstPasswordIssue);
if (section + 1 < model.numberOfSections) {
// When the next section doesn't have a header, the desired spacing is
// achieved via an empty header. If there's a header, it includes an 8pt
// top padding, so we add a 16pt empty footer to achieve the desired
// spacing of 24pt.
return [model headerForSectionIndex:section + 1]
? kVerticalSpacingBetweenItems * 2
: 0;
}
// Vertical spacing between the last item and its container.
return kVerticalSpacingBetweenItems;
}
}
- (CGFloat)tableView:(UITableView*)tableView
heightForHeaderInSection:(NSInteger)section {
TableViewModel* model = self.tableViewModel;
// Calculate the actual height of the header view if there's one.
if ([model headerForSectionIndex:section]) {
return UITableViewAutomaticDimension;
}
NSInteger sectionIdentifier =
[model sectionIdentifierForSectionIndex:section];
switch (sectionIdentifier) {
case SectionIdentifierHeader:
// This section always has a header.
NOTREACHED_NORETURN();
case SectionIdentifierContent:
// Keep legacy spacing when no header.
return [super tableView:tableView heightForHeaderInSection:section];
case SectionIdentifierDismissedCredentialsButton:
// Spacing to last password issue.
return 3 * kVerticalSpacingBetweenItems;
default:
// Handle password issue sections.
// All other sections should be handled by now.
CHECK_GE(sectionIdentifier, SectionIdentifierFirstPasswordIssue);
// Add an empty header to achieve the desired spacing to the element
// above.
return kVerticalSpacingBetweenItems;
}
}
// Asynchronously loads favicon for given index path. The loads are cancelled
// upon cell reuse automatically.
- (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath
forCell:(UITableViewCell*)cell {
TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
DCHECK(item);
DCHECK(cell);
TableViewURLItem* URLItem =
base::apple::ObjCCastStrict<TableViewURLItem>(item);
TableViewURLCell* URLCell =
base::apple::ObjCCastStrict<TableViewURLCell>(cell);
NSString* itemIdentifier = URLItem.uniqueIdentifier;
[self.imageDataSource
faviconForPageURL:URLItem.URL
completion:^(FaviconAttributes* attributes) {
// Only set favicon if the cell hasn't been reused.
if ([URLCell.cellUniqueIdentifier
isEqualToString:itemIdentifier]) {
DCHECK(attributes);
[URLCell.faviconView configureWithAttributes:attributes];
}
}];
}
#pragma mark - PasswordIssuesConsumer
- (void)setPasswordIssues:(NSArray<PasswordIssueGroup*>*)passwordGroups
dismissedWarningsCount:(NSInteger)dismissedWarnings {
_passwordGroups = passwordGroups;
_dismissedWarningsCount = dismissedWarnings;
[self reloadData];
// User removed/resolved all issues, dismiss the vc and go back to the
// previous screen.
if (IsPasswordCheckupEnabled() && passwordGroups.count == 0 &&
dismissedWarnings == 0) {
[self.presenter dismissAfterAllIssuesGone];
}
}
- (void)setNavigationBarTitle:(NSString*)title {
self.title = title;
}
- (void)setHeader:(NSString*)text URL:(CrURL*)URL {
_headerText = text;
_headerURL = URL;
[self reloadData];
}
#pragma mark - TableViewLinkHeaderFooterItemDelegate
- (void)view:(TableViewLinkHeaderFooterView*)view didTapLinkURL:(CrURL*)URL {
[self.presenter dismissAndOpenURL:URL];
}
#pragma mark - Private
// Helper for getting the url for changing the password of the password issue
// item in the given tableView section.
- (CrURL*)changePasswordURLForPasswordInSection:(NSInteger)section {
PasswordIssueContentItem* passwordIssueItem =
base::apple::ObjCCastStrict<PasswordIssueContentItem>([self.tableViewModel
itemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]]);
CHECK(passwordIssueItem.password.changePasswordURL.has_value());
return passwordIssueItem.password.changePasswordURL.value();
}
@end