| // 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.uniqueFormID |
| fieldRendererId:_query.uniqueFieldID |
| 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.uniqueFormID, formQuery.uniqueFieldID, isPasswordField)) { |
| const password_manager::FormInfo* formInfo = fillData->GetFormInfo( |
| formQuery.uniqueFormID, formQuery.uniqueFieldID, isPasswordField); |
| bool is_single_username_form = formInfo && formInfo->username_element_id && |
| !formInfo->password_element_id; |
| |
| std::vector<password_manager::UsernameAndRealm> usernameAndRealms = |
| fillData->RetrieveSuggestions(formQuery.uniqueFormID, |
| formQuery.uniqueFieldID, 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 |
| popupItemId:autofill::PopupItemId:: |
| kAutocompleteEntry |
| 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.uniqueFormID}); |
| 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.uniqueFieldID == 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 |