blob: f96db355a01e547dce7cb43982109a16f9c027c9 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "components/password_manager/ios/password_suggestion_helper.h"
#include "base/strings/sys_string_conversions.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/password_form_fill_data.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#include "components/password_manager/core/browser/password_ui_utils.h"
#include "components/password_manager/ios/account_select_fill_data.h"
#import "components/password_manager/ios/password_manager_ios_util.h"
#import "components/password_manager/ios/password_manager_java_script_feature.h"
#include "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/web_state.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using autofill::FieldRendererId;
using autofill::FormData;
using autofill::FormRendererId;
using autofill::PasswordFormFillData;
using base::SysNSStringToUTF16;
using base::SysNSStringToUTF8;
using base::SysUTF16ToNSString;
using base::SysUTF8ToNSString;
using password_manager::AccountSelectFillData;
using password_manager::FillData;
using password_manager::IsCrossOriginIframe;
typedef void (^PasswordSuggestionsAvailableCompletion)(
const password_manager::AccountSelectFillData* __nullable);
@implementation PasswordSuggestionHelper {
base::WeakPtr<web::WebState> _webState;
// The value of the map is a C++ interface to cache and retrieve password
// suggestions. The interfaces are grouped by the frame of the form.
base::flat_map<web::WebFrame*, std::unique_ptr<AccountSelectFillData>>
_fillDataMap;
// YES indicates that extracted password form has been sent to the password
// manager.
BOOL _sentPasswordFormToPasswordManager;
// YES indicates that suggestions from the password manager have been
// processed.
BOOL _processedPasswordSuggestions;
// The completion to inform the caller of -checkIfSuggestionsAvailableForForm:
// that suggestions are available for a given form and field.
PasswordSuggestionsAvailableCompletion _suggestionsAvailableCompletion;
}
#pragma mark - Initialization
- (instancetype)initWithWebState:(web::WebState*)webState {
self = [super init];
if (self) {
_webState = webState->GetWeakPtr();
_sentPasswordFormToPasswordManager = NO;
}
return self;
}
#pragma mark - Public methods
- (NSArray<FormSuggestion*>*)
retrieveSuggestionsWithFormID:(FormRendererId)formIdentifier
fieldIdentifier:(FieldRendererId)fieldIdentifier
inFrame:(web::WebFrame*)frame
fieldType:(NSString*)fieldType {
AccountSelectFillData* fillData = [self getFillDataFromFrame:frame];
BOOL isPasswordField = [fieldType isEqual:kPasswordFieldType];
NSMutableArray<FormSuggestion*>* results = [NSMutableArray array];
if (fillData && fillData->IsSuggestionsAvailable(
formIdentifier, fieldIdentifier, isPasswordField)) {
std::vector<password_manager::UsernameAndRealm> usernameAndRealms =
fillData->RetrieveSuggestions(formIdentifier, fieldIdentifier,
isPasswordField);
for (const auto& usernameAndRealm : usernameAndRealms) {
NSString* username = SysUTF16ToNSString(usernameAndRealm.username);
NSString* realm = nil;
if (!usernameAndRealm.realm.empty()) {
url::Origin origin = url::Origin::Create(GURL(usernameAndRealm.realm));
realm = SysUTF8ToNSString(password_manager::GetShownOrigin(origin));
}
[results addObject:[FormSuggestion suggestionWithValue:username
displayDescription:realm
icon:nil
identifier:0
requiresReauth:YES]];
}
}
return [results copy];
}
- (void)checkIfSuggestionsAvailableForForm:
(FormSuggestionProviderQuery*)formQuery
completionHandler:
(SuggestionsAvailableCompletion)completion {
// When password controller's -processWithPasswordFormFillData: is already
// called, |completion| will be called immediately and |triggerFormExtraction|
// will be skipped.
// Otherwise, -suggestionHelperShouldTriggerFormExtraction: will be called
// and |completion| will not be called until
// -processWithPasswordFormFillData: is called.
DCHECK(_webState.get());
password_manager::PasswordManagerJavaScriptFeature* feature =
password_manager::PasswordManagerJavaScriptFeature::GetInstance();
web::WebFrame* frame =
feature->GetWebFramesManager(_webState.get())
->GetFrameWithId(SysNSStringToUTF8(formQuery.frameID));
DCHECK(frame);
BOOL isPasswordField = [formQuery isOnPasswordField];
if ([formQuery hasFocusType] &&
(!_sentPasswordFormToPasswordManager || !_processedPasswordSuggestions)) {
// Save the callback until fill data is ready.
_suggestionsAvailableCompletion = ^(const AccountSelectFillData* fillData) {
completion(!fillData ? NO
: fillData->IsSuggestionsAvailable(
formQuery.uniqueFormID,
formQuery.uniqueFieldID, isPasswordField));
};
if (!_sentPasswordFormToPasswordManager) {
// Form extraction is required for this check.
[self.delegate suggestionHelperShouldTriggerFormExtraction:self
inFrame:frame];
}
return;
}
AccountSelectFillData* fillData = [self getFillDataFromFrame:frame];
completion(fillData && fillData->IsSuggestionsAvailable(
formQuery.uniqueFormID, formQuery.uniqueFieldID,
isPasswordField));
}
- (std::unique_ptr<password_manager::FillData>)
passwordFillDataForUsername:(NSString*)username
inFrame:(web::WebFrame*)frame {
AccountSelectFillData* fillData = [self getFillDataFromFrame:frame];
return fillData ? fillData->GetFillData(SysNSStringToUTF16(username))
: nullptr;
}
- (void)resetForNewPage {
_fillDataMap.clear();
_sentPasswordFormToPasswordManager = NO;
_processedPasswordSuggestions = NO;
_suggestionsAvailableCompletion = nil;
}
- (void)processWithPasswordFormFillData:(const PasswordFormFillData&)formData
inFrame:(web::WebFrame*)frame
isMainFrame:(BOOL)isMainFrame
forSecurityOrigin:(const GURL&)origin {
// TODO(crbug.com/1383214): Rewrite the code to eliminate the chance of using
// |frame| after its destruction.
AccountSelectFillData* fillData = [self getFillDataFromFrame:frame];
if (!fillData) {
auto it = _fillDataMap.insert(
std::make_pair(frame, std::make_unique<AccountSelectFillData>()));
fillData = it.first->second.get();
}
DCHECK(_webState.get());
fillData->Add(formData,
IsCrossOriginIframe(_webState.get(), isMainFrame, origin));
// "attachListenersForBottomSheet" is used to add event listeners
// to fields which must trigger a specific behavior. In this case,
// the username and password fields' renderer ids are sent through
// "attachListenersForBottomSheet" so that they may trigger the
// password bottom sheet on focus events for these specific fields.
std::vector<autofill::FieldRendererId> rendererIds(2);
rendererIds[0] = formData.username_element_renderer_id;
rendererIds[1] = formData.password_element_renderer_id;
[self.delegate attachListenersForBottomSheet:rendererIds inFrame:frame];
_processedPasswordSuggestions = YES;
if (_suggestionsAvailableCompletion) {
_suggestionsAvailableCompletion(fillData);
_suggestionsAvailableCompletion = nil;
}
}
- (void)processWithNoSavedCredentials {
// Only update |_processedPasswordSuggestions| if PasswordManager was
// queried for some forms. This is needed to protect against a case when
// there are no forms on the pageload and they are added dynamically.
if (_sentPasswordFormToPasswordManager) {
_processedPasswordSuggestions = YES;
}
if (_suggestionsAvailableCompletion) {
_suggestionsAvailableCompletion(nullptr);
}
_suggestionsAvailableCompletion = nil;
}
- (void)updateStateOnPasswordFormExtracted {
_sentPasswordFormToPasswordManager = YES;
}
#pragma mark - Private methods
- (AccountSelectFillData*)getFillDataFromFrame:(web::WebFrame*)frame {
auto it = _fillDataMap.find(frame);
if (it == _fillDataMap.end()) {
return nullptr;
}
return it->second.get();
}
@end