Commit 3b105fc4 authored by Viktor Semeniuk's avatar Viktor Semeniuk Committed by Commit Bot

[iOS][Password Check] Copy website, username, password

This change adds possibility to copy website, username and password to
the pasteboard. Before copying password reauth is required.

Bug: 1122993
Change-Id: Ib41c1bb77b335a91ca54d0d6dada850a727fcb79
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2379739
Commit-Queue: Viktor Semeniuk <vsemeniuk@google.com>
Reviewed-by: default avatarGauthier Ambard <gambard@chromium.org>
Cr-Commit-Position: refs/heads/master@{#804740}
parent a26a8be6
......@@ -26,6 +26,7 @@ source_set("password_details") {
"//ios/chrome/browser/ui/commands",
"//ios/chrome/browser/ui/coordinators:chrome_coordinators",
"//ios/chrome/browser/ui/table_view",
"//ios/chrome/browser/ui/util",
"//ios/chrome/common/ui/colors",
"//ios/chrome/common/ui/reauthentication",
"//ios/chrome/common/ui/util",
......@@ -41,6 +42,8 @@ source_set("password_details_ui") {
"password_details.mm",
"password_details_consumer.h",
"password_details_handler.h",
"password_details_menu_item.h",
"password_details_menu_item.mm",
"password_details_table_view_constants.h",
"password_details_table_view_constants.mm",
"password_details_table_view_controller.h",
......@@ -92,6 +95,7 @@ source_set("unit_tests") {
"//ios/chrome/browser/browser_state:test_support",
"//ios/chrome/browser/main:test_support",
"//ios/chrome/browser/passwords",
"//ios/chrome/browser/ui/commands",
"//ios/chrome/browser/ui/settings/cells",
"//ios/chrome/browser/ui/table_view:test_support",
"//ios/chrome/browser/ui/table_view/cells",
......
......@@ -35,9 +35,6 @@ struct PasswordForm;
// Delegate.
@property(nonatomic, weak) id<PasswordDetailsCoordinatorDelegate> delegate;
// Dispatcher.
@property(nonatomic, weak) id<ApplicationCommands> dispatcher;
@end
#endif // IOS_CHROME_BROWSER_UI_SETTINGS_PASSWORD_PASSWORD_DETAILS_PASSWORD_DETAILS_COORDINATOR_H_
......@@ -14,6 +14,7 @@
#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h"
#import "ios/chrome/browser/ui/alert_coordinator/alert_coordinator.h"
#import "ios/chrome/browser/ui/commands/application_commands.h"
#import "ios/chrome/browser/ui/commands/command_dispatcher.h"
#import "ios/chrome/browser/ui/commands/open_new_tab_command.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_consumer.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_coordinator_delegate.h"
......@@ -52,6 +53,9 @@
// The action sheet coordinator, if one is currently being shown.
@property(nonatomic, strong) ActionSheetCoordinator* actionSheetCoordinator;
// Dispatcher.
@property(nonatomic, weak) id<ApplicationCommands, BrowserCommands> dispatcher;
@end
@implementation PasswordDetailsCoordinator
......@@ -75,6 +79,8 @@
_password = password;
_manager = manager;
_reauthenticationModule = reauthModule;
_dispatcher = static_cast<id<BrowserCommands, ApplicationCommands>>(
browser->GetCommandDispatcher());
}
return self;
}
......@@ -92,7 +98,7 @@
self.mediator.consumer = self.viewController;
self.viewController.handler = self;
self.viewController.delegate = self.mediator;
self.viewController.commandsDispatcher = self.dispatcher;
self.viewController.commandsHandler = self.dispatcher;
self.viewController.reauthModule = self.reauthenticationModule;
[self.baseNavigationController pushViewController:self.viewController
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_BROWSER_UI_SETTINGS_PASSWORD_PASSWORD_DETAILS_PASSWORD_DETAILS_MENU_ITEM_H_
#define IOS_CHROME_BROWSER_UI_SETTINGS_PASSWORD_PASSWORD_DETAILS_PASSWORD_DETAILS_MENU_ITEM_H_
#import <UIKit/UIKit.h>
// Menu item which holds item type. Possible types |website|, |username| or
// |password|.
@interface PasswordDetailsMenuItem : UIMenuItem
@property(nonatomic, assign) NSInteger itemType;
@end
#endif // IOS_CHROME_BROWSER_UI_SETTINGS_PASSWORD_PASSWORD_DETAILS_PASSWORD_DETAILS_MENU_ITEM_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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_details/password_details_menu_item.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@implementation PasswordDetailsMenuItem
@end
......@@ -25,7 +25,8 @@
delegate;
// Dispatcher for this ViewController.
@property(nonatomic, weak) id<ApplicationCommands> commandsDispatcher;
@property(nonatomic, weak) id<ApplicationCommands, BrowserCommands>
commandsHandler;
// Module containing the reauthentication mechanism for interactions
// with password.
......
......@@ -11,11 +11,13 @@
#include "base/strings/sys_string_conversions.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "ios/chrome/browser/ui/commands/application_commands.h"
#import "ios/chrome/browser/ui/commands/browser_commands.h"
#import "ios/chrome/browser/ui/commands/open_new_tab_command.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_item.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_consumer.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_handler.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_menu_item.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_constants.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_controller_delegate.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_cells_constants.h"
......@@ -240,6 +242,10 @@ typedef NS_ENUM(NSInteger, ReauthenticationReason) {
switch (itemType) {
case ItemTypeWebsite:
case ItemTypeUsername:
[self ensureContextMenuShownForItemType:itemType
tableView:tableView
atIndexPath:indexPath];
break;
case ItemTypeChangePasswordRecommendation:
break;
case ItemTypePassword: {
......@@ -249,18 +255,22 @@ typedef NS_ENUM(NSInteger, ReauthenticationReason) {
TableViewTextEditCell* textFieldCell =
base::mac::ObjCCastStrict<TableViewTextEditCell>(cell);
[textFieldCell.textField becomeFirstResponder];
} else {
[self ensureContextMenuShownForItemType:itemType
tableView:tableView
atIndexPath:indexPath];
}
break;
}
case ItemTypeChangePasswordButton:
if (!self.tableView.editing) {
DCHECK(self.commandsDispatcher);
DCHECK(self.commandsHandler);
DCHECK(self.password.changePasswordURL.is_valid());
OpenNewTabCommand* command = [OpenNewTabCommand
commandWithURLFromChrome:self.password.changePasswordURL];
UmaHistogramEnumeration("PasswordManager.BulkCheck.UserAction",
PasswordCheckInteraction::kChangePassword);
[self.commandsDispatcher closeSettingsUIAndOpenURL:command];
[self.commandsHandler closeSettingsUIAndOpenURL:command];
}
break;
}
......@@ -276,6 +286,27 @@ typedef NS_ENUM(NSInteger, ReauthenticationReason) {
return NO;
}
// If the context menu is not shown for a given item type, constructs that
// menu and shows it. This method should only be called for item types
// representing the cells with the site, username and password.
- (void)ensureContextMenuShownForItemType:(NSInteger)itemType
tableView:(UITableView*)tableView
atIndexPath:(NSIndexPath*)indexPath {
UIMenuController* menu = [UIMenuController sharedMenuController];
if (![menu isMenuVisible]) {
menu.menuItems = [self getMenuItemsFor:itemType];
if (@available(iOS 13, *)) {
[menu showMenuFromView:tableView
rect:[tableView rectForRowAtIndexPath:indexPath]];
} else {
[menu setTargetRect:[tableView rectForRowAtIndexPath:indexPath]
inView:tableView];
[menu setMenuVisible:YES animated:YES];
}
}
}
#pragma mark - UITableViewDataSource
- (UITableViewCell*)tableView:(UITableView*)tableView
......@@ -284,8 +315,9 @@ typedef NS_ENUM(NSInteger, ReauthenticationReason) {
cellForRowAtIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
cell.tag = itemType;
switch (itemType) {
case ItemTypePassword: {
TableViewTextEditCell* textFieldCell =
......@@ -379,8 +411,16 @@ typedef NS_ENUM(NSInteger, ReauthenticationReason) {
return;
[strongSelf logPasswordSettingsReauthResult:result];
if (result == ReauthenticationResult::kFailure)
if (result == ReauthenticationResult::kFailure) {
if (reason == ReauthenticationReasonCopy) {
[strongSelf
showToast:
l10n_util::GetNSString(
IDS_IOS_SETTINGS_PASSWORD_WAS_NOT_COPIED_MESSAGE)
forSuccess:NO];
}
return;
}
[strongSelf showPasswordFor:reason];
};
......@@ -407,9 +447,14 @@ typedef NS_ENUM(NSInteger, ReauthenticationReason) {
UmaHistogramEnumeration("PasswordManager.BulkCheck.UserAction",
PasswordCheckInteraction::kShowPassword);
break;
case ReauthenticationReasonCopy:
// TODO:(crbug.com/1075494) - Implement copy password functionality.
case ReauthenticationReasonCopy: {
UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
generalPasteboard.string = self.password.password;
[self showToast:l10n_util::GetNSString(
IDS_IOS_SETTINGS_PASSWORD_WAS_COPIED_MESSAGE)
forSuccess:YES];
break;
}
case ReauthenticationReasonEdit:
// Called super because we want to update only |tableView.editing|.
[super editButtonPressed];
......@@ -434,6 +479,18 @@ typedef NS_ENUM(NSInteger, ReauthenticationReason) {
}
}
// Shows a snack bar with |message| and provides haptic feedback. The haptic
// feedback is either for success or for error, depending on |success|.
- (void)showToast:(NSString*)message forSuccess:(BOOL)success {
TriggerHapticFeedbackForNotification(success
? UINotificationFeedbackTypeSuccess
: UINotificationFeedbackTypeError);
[self.commandsHandler showSnackbarWithMessage:message
buttonText:nil
messageAction:nil
completionAction:nil];
}
- (void)passwordEditingConfirmed {
self.password.password = self.passwordTextItem.textFieldValue;
[self.delegate passwordDetailsViewController:self
......@@ -473,6 +530,58 @@ typedef NS_ENUM(NSInteger, ReauthenticationReason) {
}
}
// Returns an array of UIMenuItems to display in a context menu on the site
// cell.
- (NSArray*)getMenuItemsFor:(NSInteger)itemType {
PasswordDetailsMenuItem* copyOption = [[PasswordDetailsMenuItem alloc]
initWithTitle:l10n_util::GetNSString(IDS_IOS_SETTINGS_SITE_COPY_MENU_ITEM)
action:@selector(copyPasswordDetails:)];
copyOption.itemType = itemType;
return @[ copyOption ];
}
// Copies the password information to system pasteboard and shows a toast of
// success/failure.
- (void)copyPasswordDetails:(id)sender {
UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
UIMenuController* menu = base::mac::ObjCCastStrict<UIMenuController>(sender);
PasswordDetailsMenuItem* menuItem =
base::mac::ObjCCastStrict<PasswordDetailsMenuItem>(
menu.menuItems.firstObject);
NSString* message = nil;
switch (menuItem.itemType) {
case ItemTypeWebsite:
generalPasteboard.string = self.password.website;
message =
l10n_util::GetNSString(IDS_IOS_SETTINGS_SITE_WAS_COPIED_MESSAGE);
break;
case ItemTypeUsername:
generalPasteboard.string = self.password.username;
message =
l10n_util::GetNSString(IDS_IOS_SETTINGS_USERNAME_WAS_COPIED_MESSAGE);
break;
case ItemTypePassword:
[self attemptToShowPasswordFor:ReauthenticationReasonCopy];
return;
}
[self showToast:message forSuccess:YES];
}
#pragma mark - UIResponder
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (action == @selector(copyPasswordDetails:)) {
return YES;
}
return NO;
}
#pragma mark - Metrics
// Logs metrics for the given reauthentication |result| (success, failure or
......
......@@ -11,6 +11,7 @@
#include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/common/password_form.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
#import "ios/chrome/browser/ui/commands/snackbar_commands.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_item.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_consumer.h"
......@@ -39,6 +40,10 @@ constexpr char kUsername[] = "test@egmail.com";
constexpr char kPassword[] = "test";
}
@interface PasswordDetailsTableViewController (Test)
- (void)copyPasswordDetails:(id)sender;
@end
// Test class that conforms to PasswordDetailsHanler in order to test the
// presenter methods are called correctly.
@interface FakePasswordDetailsHandler : NSObject <PasswordDetailsHandler>
......@@ -86,6 +91,30 @@ constexpr char kPassword[] = "test";
@end
@interface FakeSnackbarImplementation : NSObject <SnackbarCommands>
@property(nonatomic, assign) NSString* snackbarMessage;
@end
@implementation FakeSnackbarImplementation
- (void)showSnackbarMessage:(MDCSnackbarMessage*)message {
}
- (void)showSnackbarMessage:(MDCSnackbarMessage*)message
bottomOffset:(CGFloat)offset {
}
- (void)showSnackbarWithMessage:(NSString*)messageText
buttonText:(NSString*)buttonText
messageAction:(void (^)(void))messageAction
completionAction:(void (^)(BOOL))completionAction {
self.snackbarMessage = messageText;
}
@end
// Unit tests for PasswordIssuesTableViewController.
class PasswordDetailsTableViewControllerTest
: public ChromeTableViewControllerTest {
......@@ -95,6 +124,7 @@ class PasswordDetailsTableViewControllerTest
delegate_ = [[FakePasswordDetailsDelegate alloc] init];
reauthentication_module_ = [[MockReauthenticationModule alloc] init];
reauthentication_module_.expectedResult = ReauthenticationResult::kSuccess;
snack_bar_ = [[FakeSnackbarImplementation alloc] init];
}
ChromeTableViewController* InstantiateController() override {
......@@ -104,6 +134,7 @@ class PasswordDetailsTableViewControllerTest
controller.handler = handler_;
controller.delegate = delegate_;
controller.reauthModule = reauthentication_module_;
controller.commandsHandler = snack_bar_;
return controller;
}
......@@ -174,8 +205,12 @@ class PasswordDetailsTableViewControllerTest
FakePasswordDetailsHandler* handler() { return handler_; }
FakePasswordDetailsDelegate* delegate() { return delegate_; }
MockReauthenticationModule* reauth() { return reauthentication_module_; }
FakeSnackbarImplementation* snack_bar() {
return (FakeSnackbarImplementation*)snack_bar_;
}
private:
id snack_bar_;
FakePasswordDetailsHandler* handler_;
FakePasswordDetailsDelegate* delegate_;
MockReauthenticationModule* reauthentication_module_;
......@@ -396,3 +431,89 @@ TEST_F(PasswordDetailsTableViewControllerTest, TestBlockedOrigin) {
[passwordDetails editButtonPressed];
EXPECT_TRUE(passwordDetails.tableView.editing);
}
// Tests copy website works as intended.
TEST_F(PasswordDetailsTableViewControllerTest, CopySite) {
SetPassword();
PasswordDetailsTableViewController* passwordDetails =
base::mac::ObjCCastStrict<PasswordDetailsTableViewController>(
controller());
[passwordDetails tableView:passwordDetails.tableView
didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
UIMenuController* menu = [UIMenuController sharedMenuController];
EXPECT_EQ(1u, menu.menuItems.count);
[passwordDetails copyPasswordDetails:menu];
UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
EXPECT_NSEQ(@"http://www.example.com/", generalPasteboard.string);
EXPECT_NSEQ(l10n_util::GetNSString(IDS_IOS_SETTINGS_SITE_WAS_COPIED_MESSAGE),
snack_bar().snackbarMessage);
}
// Tests copy username works as intended.
TEST_F(PasswordDetailsTableViewControllerTest, CopyUsername) {
SetPassword();
PasswordDetailsTableViewController* passwordDetails =
base::mac::ObjCCastStrict<PasswordDetailsTableViewController>(
controller());
[passwordDetails tableView:passwordDetails.tableView
didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]];
UIMenuController* menu = [UIMenuController sharedMenuController];
EXPECT_EQ(1u, menu.menuItems.count);
[passwordDetails copyPasswordDetails:menu];
UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
EXPECT_NSEQ(@"test@egmail.com", generalPasteboard.string);
EXPECT_NSEQ(
l10n_util::GetNSString(IDS_IOS_SETTINGS_USERNAME_WAS_COPIED_MESSAGE),
snack_bar().snackbarMessage);
}
// Tests copy password works as intended when reauth was successful.
TEST_F(PasswordDetailsTableViewControllerTest, CopyPasswordSuccess) {
SetPassword();
PasswordDetailsTableViewController* passwordDetails =
base::mac::ObjCCastStrict<PasswordDetailsTableViewController>(
controller());
[passwordDetails tableView:passwordDetails.tableView
didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]];
UIMenuController* menu = [UIMenuController sharedMenuController];
EXPECT_EQ(1u, menu.menuItems.count);
[passwordDetails copyPasswordDetails:menu];
UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
EXPECT_NSEQ(@"test", generalPasteboard.string);
EXPECT_NSEQ(
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_REAUTH_REASON_COPY),
reauth().localizedReasonForAuthentication);
EXPECT_NSEQ(
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_WAS_COPIED_MESSAGE),
snack_bar().snackbarMessage);
}
// Tests copy password works as intended.
TEST_F(PasswordDetailsTableViewControllerTest, CopyPasswordFail) {
SetPassword();
PasswordDetailsTableViewController* passwordDetails =
base::mac::ObjCCastStrict<PasswordDetailsTableViewController>(
controller());
reauth().expectedResult = ReauthenticationResult::kFailure;
[passwordDetails tableView:passwordDetails.tableView
didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]];
UIMenuController* menu = [UIMenuController sharedMenuController];
EXPECT_EQ(1u, menu.menuItems.count);
[passwordDetails copyPasswordDetails:menu];
EXPECT_NSEQ(
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_WAS_NOT_COPIED_MESSAGE),
snack_bar().snackbarMessage);
}
......@@ -114,7 +114,6 @@
password:form
reauthModule:self.reauthModule
passwordCheckManager:_manager];
self.passwordDetails.dispatcher = self.dispatcher;
self.passwordDetails.delegate = self;
[self.passwordDetails start];
}
......
......@@ -1312,7 +1312,6 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf(
password:form
reauthModule:_reauthenticationModule
passwordCheckManager:_passwordCheck.get()];
self.passwordDetailsCoordinator.dispatcher = self.dispatcher;
self.passwordDetailsCoordinator.delegate = self;
[self.passwordDetailsCoordinator start];
} else {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment