// 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/autofill_driver_ios.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"
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;
// Status of form extraction for a given frame.
enum class FormExtractionStatus {
kNotRequested = 0,
kRequested = 1,
kCompleted = 2
};
@protocol FillDataProvider <NSObject>
// True if suggestions are available for the field in form.
- (bool)
areSuggestionsAvailableForFrameId:(NSString*)frameId
formRendererId:(autofill::FormRendererId)formRendererId
fieldRendererId:(autofill::FieldRendererId)fieldRendererId
isPasswordField:(bool)isPasswordField;
@end
// Represents a pending form query to be completed later with
// -runCompletion.
@interface PendingFormQuery : NSObject
// ID of the frame targeted by the query.
@property(nonatomic, strong, readonly) NSString* frameId;
// Initializes the object with a `query` to complete with `completion` for
// frame with id `frameId`.
- (instancetype)initWithQuery:(FormSuggestionProviderQuery*)query
completion:(SuggestionsAvailableCompletion)completion
fillDataProvider:(id<FillDataProvider>)fillDataProvider
isPasswordField:(BOOL)isPasswordField;
// Runs the completion callback with the available fill data. This can only be
// done once in the lifetime of the query object.
- (void)runCompletion;
@end
@implementation PendingFormQuery {
FormSuggestionProviderQuery* _query;
SuggestionsAvailableCompletion _completion;
id<FillDataProvider> _fillDataProvider;
BOOL _isPasswordField;
}
- (instancetype)initWithQuery:(FormSuggestionProviderQuery*)query
completion:(SuggestionsAvailableCompletion)completion
fillDataProvider:(id<FillDataProvider>)fillDataProvider
isPasswordField:(BOOL)isPasswordField {
self = [super init];
if (self) {
_query = query;
_completion = completion;
_fillDataProvider = fillDataProvider;
_frameId = query.frameID;
_isPasswordField = isPasswordField;
}
return self;
}
- (void)runCompletion {
// Check that the completion was never run as -runCompletion
// can only be called once.
CHECK(_completion);
_completion([_fillDataProvider
areSuggestionsAvailableForFrameId:self.frameId
formRendererId:_query.formRendererID
fieldRendererId:_query.fieldRendererID
isPasswordField:_isPasswordField]);
_completion = nil;
}
@end
@interface PasswordSuggestionHelper () <FillDataProvider>
@end
@implementation PasswordSuggestionHelper {
base::WeakPtr<web::WebState> _webState;
// Fill data keyed by frame id for the frames' forms in the webstate.
base::flat_map<std::string, std::unique_ptr<AccountSelectFillData>>
_fillDataMap;
// Pending form queries that are waiting for forms extraction results.
NSMutableArray<PendingFormQuery*>* _pendingFormQueries;
// Map of frame ids to the form extraction status for that frame.
std::map<std::string, FormExtractionStatus> _framesFormExtractionStatus;
}
#pragma mark - Initialization
- (instancetype)initWithWebState:(web::WebState*)webState {
self = [super init];
if (self) {
_webState = webState->GetWeakPtr();
_pendingFormQueries = [NSMutableArray array];
}
return self;
}
#pragma mark - Public methods
- (NSArray<FormSuggestion*>*)retrieveSuggestionsWithForm:
(FormSuggestionProviderQuery*)formQuery {
const std::string frameId = SysNSStringToUTF8(formQuery.frameID);
AccountSelectFillData* fillData = [self fillDataForFrameId:frameId];
BOOL isPasswordField =
[self isPasswordFieldOnForm:formQuery
webFrame:[self frameWithId:frameId]];
NSMutableArray<FormSuggestion*>* results = [NSMutableArray array];
if (fillData->IsSuggestionsAvailable(formQuery.formRendererID,
formQuery.fieldRendererID,
isPasswordField)) {
const password_manager::FormInfo* formInfo = fillData->GetFormInfo(
formQuery.formRendererID, formQuery.fieldRendererID, isPasswordField);
bool is_single_username_form = formInfo && formInfo->username_element_id &&
!formInfo->password_element_id;
std::vector<password_manager::UsernameAndRealm> usernameAndRealms =
fillData->RetrieveSuggestions(formQuery.formRendererID,
formQuery.fieldRendererID,
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));
}
FormSuggestionMetadata metadata;
metadata.is_single_username_form = is_single_username_form;
[results
addObject:[FormSuggestion
suggestionWithValue:username
displayDescription:realm
icon:nil
type:autofill::SuggestionType::
kPasswordEntry
backendIdentifier:nil
requiresReauth:YES
acceptanceA11yAnnouncement:nil
metadata:std::move(metadata)]];
}
}
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());
const std::string frame_id = SysNSStringToUTF8(formQuery.frameID);
web::WebFrame* frame = [self frameWithId:frame_id];
DCHECK(frame);
BOOL isPasswordField = [self isPasswordFieldOnForm:formQuery webFrame:frame];
PendingFormQuery* query =
[[PendingFormQuery alloc] initWithQuery:formQuery
completion:completion
fillDataProvider:self
isPasswordField:isPasswordField];
AccountSelectFillData* fillData = [self fillDataForFrameId:frame_id];
if (![formQuery hasFocusType] || !fillData->Empty() ||
_framesFormExtractionStatus[frame_id] ==
FormExtractionStatus::kCompleted) {
// If the query isn't triggered by focusing on the form or there is fill
// data available, complete the check immediately with the available fill
// data. If there is fill data, it doesn't mean that there are suggestions
// for the form targeted by the query, but at least there are some chances
// that suggestions will be available. If the extraction status is complete,
// it means we already know whether or not suggestions are available and
// there's no point in attempting form extraction again, so we can run the
// completion block right away and exit early.
[query runCompletion];
return;
}
// Queue the form query until the fill data is processed. The queue can handle
// concurent calls to -checkIfSuggestionsAvailableForForm, which may happen
// when there is more than one consumer of suggestions.
[_pendingFormQueries addObject:query];
// Try to extract password forms from the frame's renderer content
// because there is no knowledge of any extraction done yet. If
// -checkIfSuggestionsAvailableForForm is called before the first forms
// are extracted, this may result in extracting the forms twice, which
// is fine.
//
// It is important to always call -suggestionHelperShouldTriggerFormExtraction
// when there is a new query queued to make sure that the pending query is
// completed when processing the form extraction results. Leaving a query
// uncompleted may result in the caller waiting forever for query results
// (e.g. having the keyboard input accessory not showing any suggestion
// because the pipeline is blocked by an hanging request).
[self.delegate suggestionHelperShouldTriggerFormExtraction:self
inFrame:frame];
_framesFormExtractionStatus[frame_id] = FormExtractionStatus::kRequested;
}
- (std::unique_ptr<password_manager::FillData>)
passwordFillDataForUsername:(NSString*)username
forFrameId:(const std::string&)frameId {
return [self fillDataForFrameId:frameId]->GetFillData(
SysNSStringToUTF16(username));
}
- (void)resetForNewPage {
_fillDataMap.clear();
[_pendingFormQueries removeAllObjects];
_framesFormExtractionStatus.clear();
}
- (void)processWithPasswordFormFillData:(const PasswordFormFillData&)formData
forFrameId:(const std::string&)frameId
isMainFrame:(BOOL)isMainFrame
forSecurityOrigin:(const GURL&)origin {
DCHECK(_webState.get());
[self fillDataForFrameId:frameId]->Add(
formData, [self shouldAlwaysPopulateRealmForFrame:frameId
isMainFrame:isMainFrame
forSecurityOrigin: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 forFrameId:frameId];
[self completePendingFormQueriesForFrameId:frameId];
}
- (void)processWithNoSavedCredentialsWithFrameId:(const std::string&)frameId {
[self completePendingFormQueriesForFrameId:frameId];
}
- (BOOL)isPasswordFieldOnForm:(FormSuggestionProviderQuery*)formQuery
webFrame:(web::WebFrame*)webFrame {
if (![formQuery.fieldType isEqual:kObfuscatedFieldType]) {
return NO;
}
if (!_webState.get() || !webFrame) {
return YES;
}
auto* driver = autofill::AutofillDriverIOS::FromWebStateAndWebFrame(
_webState.get(), webFrame);
if (!driver) {
return YES;
}
autofill::FormStructure* form_structure =
driver->GetAutofillManager().FindCachedFormById(
{driver->GetFrameToken(), formQuery.formRendererID});
if (!form_structure) {
return YES;
}
const auto& fields = form_structure->fields();
auto itEnd = fields.end();
auto it = std::find_if(fields.begin(), itEnd, [&](auto& field) {
return formQuery.fieldRendererID == field->renderer_id();
});
if (it == itEnd) {
return YES;
}
autofill::FieldType fieldType = (*it)->Type().GetStorableType();
switch (GroupTypeOfFieldType(fieldType)) {
case autofill::FieldTypeGroup::kPasswordField:
case autofill::FieldTypeGroup::kNoGroup:
return YES; // May be a password field.
default:
return NO; // Not a password field.
}
}
#pragma mark - FillDataProvider
- (bool)
areSuggestionsAvailableForFrameId:(NSString*)frameId
formRendererId:(autofill::FormRendererId)formRendererId
fieldRendererId:(autofill::FieldRendererId)fieldRendererId
isPasswordField:(bool)isPasswordField {
return [self fillDataForFrameId:SysNSStringToUTF8(frameId)]
->IsSuggestionsAvailable(formRendererId, fieldRendererId,
isPasswordField);
}
#pragma mark - Private
- (web::WebFrame*)frameWithId:(const std::string&)frameId {
password_manager::PasswordManagerJavaScriptFeature* feature =
password_manager::PasswordManagerJavaScriptFeature::GetInstance();
return feature->GetWebFramesManager(_webState.get())->GetFrameWithId(frameId);
}
// Returns whether to add the form's url as the Credential's realm if the realm
// is not specified.
- (bool)shouldAlwaysPopulateRealmForFrame:(const std::string&)frameId
isMainFrame:(BOOL)isMainFrame
forSecurityOrigin:(const GURL&)origin {
CHECK(_webState.get());
if (IsCrossOriginIframe(_webState.get(), isMainFrame, origin)) {
return true;
}
web::WebFrame* frame = [self frameWithId:frameId];
if (!frame) {
return false;
}
auto* driver = autofill::AutofillDriverIOS::FromWebStateAndWebFrame(
_webState.get(), frame);
if (!driver) {
return false;
}
return driver->GetAutofillClient().ShouldFormatForLargeKeyboardAccessory();
}
// Completes all the pending form queries that were queued for the frame that
// corresponds to `frameId`. The fill data may not be the freshest if there are
// still other outgoing forms extractions queries pending for the frame, but at
// least something will be provided and the queries completed (avoiding the
// query caller waiting indefinitely for a callback).
- (void)completePendingFormQueriesForFrameId:(const std::string&)frameId {
NSMutableArray<PendingFormQuery*>* remainingQueries = [NSMutableArray array];
for (PendingFormQuery* query in _pendingFormQueries) {
if ([query.frameId isEqualToString:SysUTF8ToNSString(frameId)]) {
[query runCompletion];
} else {
[remainingQueries addObject:query];
}
}
_pendingFormQueries = remainingQueries;
// Only if the form extraction request has been made from
// PasswordSuggestionHelper do we set the extraction status' value to
// completed. Otherwise, the request could have happened too early and not yet
// contain the information we are interested in.
if (_framesFormExtractionStatus[frameId] ==
FormExtractionStatus::kRequested) {
_framesFormExtractionStatus[frameId] = FormExtractionStatus::kCompleted;
}
}
- (AccountSelectFillData*)fillDataForFrameId:(const std::string&)frameId {
// Create empty AccountSelectFillData for the frame if it doesn't exist.
return _fillDataMap
.insert(
std::make_pair(frameId, std::make_unique<AccountSelectFillData>()))
.first->second.get();
}
@end