[go: nahoru, domu]

blob: 105e3490e19620459438433d383aab61ba64ff4a [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/shared_password_controller.h"
#include <stddef.h>
#include <algorithm>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/apple/foundation_util.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "components/autofill/core/browser/ui/popup_item_ids.h"
#include "components/autofill/core/browser/ui/popup_types.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/password_form_fill_data.h"
#include "components/autofill/core/common/password_form_generation_data.h"
#include "components/autofill/core/common/password_generation_util.h"
#include "components/autofill/core/common/signatures.h"
#include "components/autofill/core/common/unique_ids.h"
#include "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/browser/form_suggestion_provider_query.h"
#import "components/autofill/ios/form_util/form_activity_observer_bridge.h"
#include "components/autofill/ios/form_util/form_activity_params.h"
#include "components/autofill/ios/form_util/unique_id_data_tab_helper.h"
#include "components/password_manager/core/browser/password_bubble_experiment.h"
#import "components/password_manager/core/browser/password_feature_manager.h"
#include "components/password_manager/core/browser/password_generation_frame_helper.h"
#include "components/password_manager/core/browser/password_manager_client.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/password_manager/ios/account_select_fill_data.h"
#import "components/password_manager/ios/ios_password_manager_driver_factory.h"
#include "components/password_manager/ios/password_manager_ios_util.h"
#import "components/password_manager/ios/password_manager_java_script_feature.h"
#import "components/password_manager/ios/shared_password_controller+private.h"
#include "components/strings/grit/components_strings.h"
#include "ios/web/common/url_scheme_util.h"
#include "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/js_messaging/web_frames_manager_observer_bridge.h"
#include "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/web_state.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "url/gurl.h"
using autofill::FieldDataManager;
using autofill::FieldRendererId;
using autofill::FormActivityObserverBridge;
using autofill::FormData;
using autofill::FormRendererId;
using autofill::PasswordFormGenerationData;
using autofill::password_generation::LogPasswordGenerationEvent;
using autofill::password_generation::PasswordGenerationType;
using base::SysNSStringToUTF16;
using base::SysNSStringToUTF8;
using base::SysUTF16ToNSString;
using base::SysUTF8ToNSString;
using l10n_util::GetNSString;
using l10n_util::GetNSStringF;
using password_manager::AccountSelectFillData;
using password_manager::FillData;
using password_manager::GetPageURLAndCheckTrustLevel;
using password_manager::IsCrossOriginIframe;
using password_manager::JsonStringToFormData;
using password_manager::PasswordFormManagerForUI;
using password_manager::PasswordGenerationFrameHelper;
using password_manager::PasswordManagerClient;
using password_manager::PasswordManagerDriver;
using password_manager::PasswordManagerInterface;
using password_manager::metrics_util::LogPasswordDropdownShown;
using password_manager::metrics_util::PasswordDropdownState;
namespace {
// Password is considered not generated when user edits it below 4 characters.
constexpr int kMinimumLengthForEditedPassword = 4;
} // namespace
NSString* const kPasswordFormSuggestionSuffix = @" ••••••••";
@interface SharedPasswordController ()
// Helper contains common password suggestion logic.
@property(nonatomic, readonly) PasswordSuggestionHelper* suggestionHelper;
// Tracks field when current password was generated.
@property(nonatomic) FieldRendererId passwordGeneratedIdentifier;
// Tracks current potential generated password until accepted or rejected.
@property(nonatomic, copy) NSString* generatedPotentialPassword;
- (BOOL)IsOffTheRecord;
@end
@implementation SharedPasswordController {
PasswordManagerInterface* _passwordManager;
// The WebState this instance is observing. Will be null after
// -webStateDestroyed: has been called.
web::WebState* _webState;
PasswordControllerDriverHelper* _driverHelper;
// Bridge to observe WebState from Objective-C.
std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
// Bridge to observe the web frames manager from Objective-C.
std::unique_ptr<web::WebFramesManagerObserverBridge>
_webFramesManagerObserverBridge;
// Bridge to observe form activity in |_webState|.
std::unique_ptr<FormActivityObserverBridge> _formActivityObserverBridge;
// Form data for password generation on this page.
std::map<FormRendererId, PasswordFormGenerationData> _formGenerationData;
// Identifier of the field that was last typed into.
FieldRendererId _lastTypedfieldIdentifier;
// The value that was last typed by the user.
NSString* _lastTypedValue;
// Identifier of the last focused form.
FormRendererId _lastFocusedFormIdentifier;
// Identifier of the last focused field.
FieldRendererId _lastFocusedFieldIdentifier;
// Identifier of the last focused frame.
web::WebFrame* _lastFocusedFrame;
// A refcounted object is stored here, because otherwise the driver can
// be deleted with the frame, and the driver needs to be alive after the
// frame deletion for submission detecting purposes.
scoped_refptr<IOSPasswordManagerDriver> _lastSubmittedPasswordManagerDriver;
}
- (instancetype)initWithWebState:(web::WebState*)webState
manager:(password_manager::PasswordManagerInterface*)
passwordManager
formHelper:(PasswordFormHelper*)formHelper
suggestionHelper:(PasswordSuggestionHelper*)suggestionHelper
driverHelper:(PasswordControllerDriverHelper*)driverHelper {
self = [super init];
if (self) {
DCHECK(webState);
IOSPasswordManagerDriverFactory::CreateForWebState(webState, self,
passwordManager);
_webState = webState;
_webStateObserverBridge =
std::make_unique<web::WebStateObserverBridge>(self);
_webState->AddObserver(_webStateObserverBridge.get());
_webFramesManagerObserverBridge =
std::make_unique<web::WebFramesManagerObserverBridge>(self);
web::WebFramesManager* framesManager =
password_manager::PasswordManagerJavaScriptFeature::GetInstance()
->GetWebFramesManager(_webState);
framesManager->AddObserver(_webFramesManagerObserverBridge.get());
_formActivityObserverBridge =
std::make_unique<FormActivityObserverBridge>(_webState, self);
_formHelper = formHelper;
_formHelper.delegate = self;
_suggestionHelper = suggestionHelper;
_suggestionHelper.delegate = self;
_passwordManager = passwordManager;
_driverHelper = driverHelper;
}
return self;
}
- (void)dealloc {
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
}
}
- (BOOL)IsOffTheRecord {
DCHECK(_delegate.passwordManagerClient);
return _delegate.passwordManagerClient->IsOffTheRecord();
}
#pragma mark - PasswordGenerationProvider
- (void)triggerPasswordGeneration {
if (!_lastFocusedFieldIdentifier) {
return;
}
LogPasswordGenerationEvent(
autofill::password_generation::PASSWORD_GENERATION_CONTEXT_MENU_PRESSED);
[self generatePasswordForFormId:_lastFocusedFormIdentifier
fieldIdentifier:_lastFocusedFieldIdentifier
inFrame:_lastFocusedFrame
isManuallyTriggered:YES];
}
#pragma mark - CRWWebStateObserver
- (void)webState:(web::WebState*)webState
didFinishNavigation:(web::NavigationContext*)navigation {
DCHECK_EQ(_webState, webState);
if (!navigation->HasCommitted() || navigation->IsSameDocument()) {
return;
}
if (!GetPageURLAndCheckTrustLevel(webState, nullptr)) {
return;
}
// Clear per-page state.
[self.suggestionHelper resetForNewPage];
auto fieldDataManager =
UniqueIDDataTabHelper::FromWebState(_webState)->GetFieldDataManager();
// This FieldDataManager info is for forms that were present on a page
// before navigation, therefore not the current driver is needed, but the
// last submitted one.
_passwordManager->PropagateFieldDataManagerInfo(
*fieldDataManager, _lastSubmittedPasswordManagerDriver.get());
// On non-iOS platforms navigations initiated by link click are excluded from
// navigations which might be form submssions. On iOS there is no easy way to
// check that the navigation is link initiated, so it is skipped. It should
// not be so important since it is unlikely that the user clicks on a link
// after filling password form w/o submitting it.
_passwordManager->DidNavigateMainFrame(
/*form_may_be_submitted=*/navigation->IsRendererInitiated());
fieldDataManager->ClearData();
}
- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success {
DCHECK_EQ(_webState, webState);
// Retrieve the identity of the page. In case the page might be malicous,
// returns early.
GURL pageURL;
if (!GetPageURLAndCheckTrustLevel(webState, &pageURL)) {
return;
}
if (!web::UrlHasWebScheme(pageURL)) {
return;
}
if (!webState->ContentIsHTML()) {
// If the current page is not HTML, it does not contain any HTML forms.
UniqueIDDataTabHelper* uniqueIDDataTabHelper =
UniqueIDDataTabHelper::FromWebState(_webState);
uint32_t maxUniqueID = uniqueIDDataTabHelper->GetNextAvailableRendererID();
password_manager::PasswordManagerJavaScriptFeature* feature =
password_manager::PasswordManagerJavaScriptFeature::GetInstance();
web::WebFrame* mainFrame =
feature->GetWebFramesManager(_webState)->GetMainWebFrame();
[self didFinishPasswordFormExtraction:std::vector<FormData>()
withMaxUniqueID:maxUniqueID
triggeredByFormChange:false
inFrame:mainFrame];
}
}
- (void)webFramesManager:(web::WebFramesManager*)webFramesManager
frameBecameAvailable:(web::WebFrame*)webFrame {
DCHECK(webFrame);
UniqueIDDataTabHelper* uniqueIDDataTabHelper =
UniqueIDDataTabHelper::FromWebState(_webState);
uint32_t nextAvailableRendererID =
uniqueIDDataTabHelper->GetNextAvailableRendererID();
[self.formHelper setUpForUniqueIDsWithInitialState:nextAvailableRendererID
inFrame:webFrame];
if (_webState->ContentIsHTML()) {
[self findPasswordFormsAndSendToPasswordStoreForFormChange:false
inFrame:webFrame];
}
}
// Track detaching iframes.
- (void)webFramesManager:(web::WebFramesManager*)webFramesManager
frameBecameUnavailable:(const std::string&)frameId {
// No need to try to detect submissions when the webState is being destroyed.
if (_webState->IsBeingDestroyed()) {
return;
}
web::WebFrame* webFrame = webFramesManager->GetFrameWithId(frameId);
if (!webFrame || webFrame->IsMainFrame()) {
return;
}
// Casting is safe, as this code is run on iOS Chrome & WebView only.
auto* driver = static_cast<IOSPasswordManagerDriver*>(
[_driverHelper PasswordManagerDriver:webFrame]);
auto fieldDataManager =
UniqueIDDataTabHelper::FromWebState(_webState)->GetFieldDataManager();
_passwordManager->OnIframeDetach(frameId, driver, *fieldDataManager);
}
- (void)webStateDestroyed:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
web::WebFramesManager* framesManager =
password_manager::PasswordManagerJavaScriptFeature::GetInstance()
->GetWebFramesManager(_webState);
framesManager->RemoveObserver(_webFramesManagerObserverBridge.get());
_webFramesManagerObserverBridge.reset();
_formActivityObserverBridge.reset();
_webState = nullptr;
}
_formGenerationData.clear();
_isPasswordGenerated = NO;
_lastTypedfieldIdentifier = FieldRendererId();
_lastTypedValue = nil;
_lastFocusedFormIdentifier = FormRendererId();
_lastFocusedFieldIdentifier = FieldRendererId();
_lastFocusedFrame = nullptr;
_passwordManager = nullptr;
_lastSubmittedPasswordManagerDriver = nullptr;
}
#pragma mark - FormSuggestionProvider
- (void)checkIfSuggestionsAvailableForForm:
(FormSuggestionProviderQuery*)formQuery
hasUserGesture:(BOOL)hasUserGesture
webState:(web::WebState*)webState
completionHandler:
(SuggestionsAvailableCompletion)completion {
DCHECK_EQ(_webState, webState);
if (!GetPageURLAndCheckTrustLevel(webState, nullptr)) {
completion(NO);
return;
}
password_manager::PasswordManagerJavaScriptFeature* feature =
password_manager::PasswordManagerJavaScriptFeature::GetInstance();
web::WebFrame* frame = feature->GetWebFramesManager(webState)->GetFrameWithId(
SysNSStringToUTF8(formQuery.frameID));
// Clicking on a password form field from a different form on the same page
// triggers displaying the on-screen keyboard. When the keyboard is
// displayed, FormInputAccessoryMediator uses the cached parameters from the
// previous clicked field in the previous password form. Getting the frame
// from this previous frame id will result in a null frame pointer, hence
// the check below.
if (!frame) {
completion(NO);
return;
}
[self.suggestionHelper
checkIfSuggestionsAvailableForForm:formQuery
completionHandler:^(BOOL suggestionsAvailable) {
// Always display "Show All..." for password fields.
completion([formQuery isOnPasswordField] ||
suggestionsAvailable);
}];
if (self.isPasswordGenerated &&
([formQuery.type isEqual:@"input"] ||
[formQuery.type isEqual:@"keyup"]) &&
formQuery.uniqueFieldID == self.passwordGeneratedIdentifier) {
// On other platforms, when the user clicks on generation field, we show
// password in clear text. And the user has the possibility to edit it. On
// iOS, it's harder to do (it's probably bad idea to change field type from
// password to text). The decision was to give everything to the automatic
// flow and avoid the manual flow, for a cleaner and simpler UI.
if (formQuery.typedValue.length < kMinimumLengthForEditedPassword) {
self.isPasswordGenerated = NO;
LogPasswordGenerationEvent(
autofill::password_generation::PASSWORD_DELETED);
self.passwordGeneratedIdentifier = FieldRendererId();
_passwordManager->OnPasswordNoLongerGenerated(
[_driverHelper PasswordManagerDriver:frame]);
} else {
// Inject updated value to possibly update confirmation field.
[self injectGeneratedPasswordForFormId:formQuery.uniqueFormID
inFrame:frame
generatedPassword:formQuery.typedValue
completionHandler:nil];
}
}
if (formQuery.uniqueFieldID != _lastTypedfieldIdentifier ||
![formQuery.typedValue isEqual:_lastTypedValue]) {
// This method is called multiple times for the same user keystroke. Inform
// only once the keystroke.
_lastTypedfieldIdentifier = formQuery.uniqueFieldID;
_lastTypedValue = formQuery.typedValue;
if ([formQuery.type isEqual:@"input"] ||
[formQuery.type isEqual:@"keyup"]) {
[self.formHelper updateFieldDataOnUserInput:formQuery.uniqueFieldID
inputValue:formQuery.typedValue];
_passwordManager->UpdateStateOnUserInput(
[_driverHelper PasswordManagerDriver:frame], formQuery.uniqueFormID,
formQuery.uniqueFieldID, SysNSStringToUTF16(formQuery.typedValue));
}
}
}
- (void)retrieveSuggestionsForForm:(FormSuggestionProviderQuery*)formQuery
webState:(web::WebState*)webState
completionHandler:(SuggestionsReadyCompletion)completion {
DCHECK_EQ(_webState, webState);
if (!GetPageURLAndCheckTrustLevel(webState, nullptr)) {
completion({}, self);
return;
}
password_manager::PasswordManagerJavaScriptFeature* feature =
password_manager::PasswordManagerJavaScriptFeature::GetInstance();
const std::string frameId = SysNSStringToUTF8(formQuery.frameID);
web::WebFrame* frame =
feature->GetWebFramesManager(_webState)->GetFrameWithId(frameId);
if (frame == nullptr) {
completion({}, self);
return;
}
NSArray<FormSuggestion*>* rawSuggestions = [self.suggestionHelper
retrieveSuggestionsWithFormID:formQuery.uniqueFormID
fieldIdentifier:formQuery.uniqueFieldID
forFrameId:frameId
fieldType:formQuery.fieldType];
NSMutableArray<FormSuggestion*>* suggestions = [NSMutableArray array];
bool isPasswordField = [formQuery isOnPasswordField];
for (FormSuggestion* rawSuggestion in rawSuggestions) {
// 1) If this is a focus event or the field is empty show all suggestions.
// Otherwise:
// 2) If this is a username field then show only credentials with matching
// prefixes.
// 3) If this is a password field then show suggestions only if
// the field is empty.
if (![formQuery hasFocusType] && formQuery.typedValue.length > 0 &&
(isPasswordField ||
![rawSuggestion.value hasPrefix:formQuery.typedValue])) {
continue;
}
DCHECK(self.delegate.passwordManagerClient);
NSString* value = [rawSuggestion.value
stringByAppendingString:kPasswordFormSuggestionSuffix];
FormSuggestion* suggestion = [FormSuggestion
suggestionWithValue:value
displayDescription:rawSuggestion.displayDescription
icon:nil
popupItemId:autofill::PopupItemId::kAutocompleteEntry
backendIdentifier:nil
requiresReauth:YES];
[suggestions addObject:suggestion];
}
absl::optional<PasswordDropdownState> suggestionState;
if (suggestions.count) {
suggestionState = PasswordDropdownState::kStandard;
}
if ([self canGeneratePasswordForForm:formQuery.uniqueFormID
fieldIdentifier:formQuery.uniqueFieldID
fieldType:formQuery.fieldType
inFrame:frame]) {
// Add "Suggest Password...".
NSString* suggestPassword = GetNSString(IDS_IOS_SUGGEST_PASSWORD);
FormSuggestion* suggestion = [FormSuggestion
suggestionWithValue:suggestPassword
displayDescription:nil
icon:nil
popupItemId:autofill::PopupItemId::kGeneratePasswordEntry
backendIdentifier:nil
requiresReauth:NO];
[suggestions addObject:suggestion];
suggestionState = PasswordDropdownState::kStandardGenerate;
}
if (suggestionState) {
LogPasswordDropdownShown(*suggestionState, [self IsOffTheRecord]);
}
if (suggestions.count == 0 || ![_delegate shouldShowAccountStorageNotice]) {
completion(suggestions, self);
return;
}
__weak __typeof(self) weakSelf = self;
[_delegate showAccountStorageNotice:^{
if (!weakSelf) {
return;
}
if (weakSelf.delegate && !weakSelf.delegate.passwordManagerClient
->GetPasswordFeatureManager()
->IsOptedInForAccountStorage()) {
// Re-fetch, account suggestions are no longer valid.
[weakSelf retrieveSuggestionsForForm:formQuery
webState:webState
completionHandler:completion];
} else {
completion(suggestions, weakSelf);
}
}];
}
- (void)didSelectSuggestion:(FormSuggestion*)suggestion
form:(NSString*)formName
uniqueFormID:(FormRendererId)uniqueFormID
fieldIdentifier:(NSString*)fieldIdentifier
uniqueFieldID:(FieldRendererId)uniqueFieldID
frameID:(NSString*)frameID
completionHandler:(SuggestionHandledCompletion)completion {
password_manager::PasswordManagerJavaScriptFeature* feature =
password_manager::PasswordManagerJavaScriptFeature::GetInstance();
const std::string frameId = SysNSStringToUTF8(frameID);
web::WebFrame* frame =
feature->GetWebFramesManager(_webState)->GetFrameWithId(frameId);
switch (suggestion.popupItemId) {
case autofill::PopupItemId::kAllSavedPasswordsEntry: {
completion();
password_manager::metrics_util::LogPasswordDropdownItemSelected(
password_manager::metrics_util::PasswordDropdownSelectedOption::
kShowAll,
[self IsOffTheRecord]);
return;
}
case autofill::PopupItemId::kGeneratePasswordEntry: {
// Don't call completion because current suggestion state should remain
// whether user injects a generated password or cancels.
[self generatePasswordForFormId:uniqueFormID
fieldIdentifier:uniqueFieldID
inFrame:frame
isManuallyTriggered:NO];
password_manager::metrics_util::LogPasswordDropdownItemSelected(
password_manager::metrics_util::PasswordDropdownSelectedOption::
kGenerate,
[self IsOffTheRecord]);
return;
}
default: {
password_manager::metrics_util::LogPasswordDropdownItemSelected(
password_manager::metrics_util::PasswordDropdownSelectedOption::
kPassword,
[self IsOffTheRecord]);
DCHECK([suggestion.value hasSuffix:kPasswordFormSuggestionSuffix]);
NSString* username = [suggestion.value
substringToIndex:suggestion.value.length -
kPasswordFormSuggestionSuffix.length];
std::unique_ptr<password_manager::FillData> fillData =
[self.suggestionHelper passwordFillDataForUsername:username
forFrameId:frameId];
if (!fillData) {
completion();
return;
}
[self.formHelper fillPasswordFormWithFillData:*fillData
inFrame:frame
triggeredOnField:uniqueFieldID
completionHandler:^(BOOL success) {
completion();
}];
break;
}
}
[_delegate sharedPasswordController:self didAcceptSuggestion:suggestion];
}
- (SuggestionProviderType)type {
return SuggestionProviderTypePassword;
}
- (autofill::PopupType)suggestionType {
return autofill::PopupType::kPasswords;
}
#pragma mark - PasswordManagerDriverDelegate
- (const GURL&)lastCommittedURL {
return _webState ? _webState->GetLastCommittedURL() : GURL::EmptyGURL();
}
- (void)processPasswordFormFillData:
(const autofill::PasswordFormFillData&)formData
forFrameId:(const std::string&)frameId
isMainFrame:(BOOL)isMainFrame
forSecurityOrigin:(const GURL&)origin {
// Biometric auth is always enabled on iOS so wait_for_username is
// specifically set to prevent filling without user confirmation.
DCHECK(formData.wait_for_username);
[self.suggestionHelper processWithPasswordFormFillData:formData
forFrameId:frameId
isMainFrame:isMainFrame
forSecurityOrigin:origin];
}
- (void)onNoSavedCredentialsWithFrameId:(const std::string&)frameId {
[self.suggestionHelper processWithNoSavedCredentialsWithFrameId:frameId];
[self detachListenersForBottomSheet:frameId];
}
- (void)formEligibleForGenerationFound:(const PasswordFormGenerationData&)form {
_formGenerationData[form.form_renderer_id] = form;
}
#pragma mark - PasswordFormHelperDelegate
- (void)formHelper:(PasswordFormHelper*)formHelper
didSubmitForm:(const FormData&)form
inFrame:(web::WebFrame*)frame {
DCHECK(frame);
IOSPasswordManagerDriver* driver =
[_driverHelper PasswordManagerDriver:frame];
if (frame->IsMainFrame()) {
_passwordManager->OnPasswordFormSubmitted(driver, form);
} else {
// Show a save prompt immediately because for iframes it is very hard to
// figure out correctness of password forms submission.
_passwordManager->OnSubframeFormSubmission(driver, form);
}
}
#pragma mark - PasswordSuggestionHelperDelegate
- (void)suggestionHelperShouldTriggerFormExtraction:
(PasswordSuggestionHelper*)suggestionHelper
inFrame:(web::WebFrame*)frame {
[self findPasswordFormsAndSendToPasswordStoreForFormChange:false
inFrame:frame];
}
- (void)attachListenersForBottomSheet:
(const std::vector<autofill::FieldRendererId>&)rendererIds
forFrameId:(const std::string&)frameId {
[self.delegate attachListenersForBottomSheet:rendererIds forFrameId:frameId];
}
- (void)detachListenersForBottomSheet:(const std::string&)frameId {
[self.delegate detachListenersForBottomSheet:frameId];
}
#pragma mark - Private methods
- (void)didFinishPasswordFormExtraction:(const std::vector<FormData>&)forms
withMaxUniqueID:(uint32_t)maxID
triggeredByFormChange:(BOOL)triggeredByFormChange
inFrame:(web::WebFrame*)frame {
// Do nothing if |self| has been detached.
if (!_passwordManager) {
return;
}
IOSPasswordManagerDriver* driver =
[_driverHelper PasswordManagerDriver:frame];
if (!forms.empty()) {
[self.suggestionHelper updateStateOnPasswordFormExtracted];
UniqueIDDataTabHelper* uniqueIDDataTabHelper =
UniqueIDDataTabHelper::FromWebState(_webState);
// Update NextAvailableRendererId if a bigger value was extracted.
if (uniqueIDDataTabHelper->GetNextAvailableRendererID() < maxID)
uniqueIDDataTabHelper->SetNextAvailableRendererID(++maxID);
// Invoke the password manager callback to autofill password forms
// on the loaded page.
_passwordManager->OnPasswordFormsParsed(driver, forms);
} else if (frame) {
[self onNoSavedCredentialsWithFrameId:frame->GetFrameId()];
}
// Invoke the password manager callback to check if password was
// accepted or rejected. If accepted, infobar is presented. If
// rejected, the provisionally saved password is deleted. On Chrome
// w/ a renderer, it is the renderer who calls OnPasswordFormsParsed()
// and OnPasswordFormsRendered(). Bling has to improvised a bit on the
// ordering of these two calls.
// Only check for form submissions if forms are not being parsed due to
// added elements to the form.
if (!triggeredByFormChange) {
_passwordManager->OnPasswordFormsRendered(driver, forms);
}
}
- (void)findPasswordFormsAndSendToPasswordStoreForFormChange:
(BOOL)triggeredByFormChange
inFrame:
(web::WebFrame*)frame {
// Read all password forms from the page and send them to the password
// manager.
__weak SharedPasswordController* weakSelf = self;
auto completionHandler =
^(const std::vector<FormData>& forms, uint32_t maxID) {
[weakSelf didFinishPasswordFormExtraction:forms
withMaxUniqueID:maxID
triggeredByFormChange:triggeredByFormChange
inFrame:frame];
};
[self.formHelper findPasswordFormsInFrame:frame
completionHandler:completionHandler];
}
- (BOOL)canGeneratePasswordForForm:(FormRendererId)formIdentifier
fieldIdentifier:(FieldRendererId)fieldIdentifier
fieldType:(NSString*)fieldType
inFrame:(web::WebFrame*)frame {
if (![_driverHelper PasswordGenerationHelper:frame]->IsGenerationEnabled(
/*log_debug_data*/ true)) {
return NO;
}
if (![fieldType isEqual:kPasswordFieldType]) {
return NO;
}
const PasswordFormGenerationData* generationData =
[self formForGenerationFromFormID:formIdentifier];
if (!generationData) {
return NO;
}
FieldRendererId newPasswordIdentifier =
generationData->new_password_renderer_id;
if (fieldIdentifier == newPasswordIdentifier) {
return YES;
}
// Don't show password generation if the field is 'confirm password'.
return NO;
}
- (const PasswordFormGenerationData*)formForGenerationFromFormID:
(FormRendererId)formIdentifier {
if (_formGenerationData.find(formIdentifier) != _formGenerationData.end()) {
return &_formGenerationData[formIdentifier];
}
return nullptr;
}
- (void)generatePasswordForFormId:(FormRendererId)formIdentifier
fieldIdentifier:(FieldRendererId)fieldIdentifier
inFrame:(web::WebFrame*)frame
isManuallyTriggered:(BOOL)isManuallyTriggered {
const autofill::PasswordFormGenerationData* generationData =
[self formForGenerationFromFormID:formIdentifier];
if (!isManuallyTriggered && !generationData) {
return;
}
BOOL shouldUpdateGenerationData =
!generationData ||
generationData->new_password_renderer_id != fieldIdentifier;
if (isManuallyTriggered && shouldUpdateGenerationData) {
PasswordFormGenerationData newGenerationData = {
.form_renderer_id = formIdentifier,
.new_password_renderer_id = fieldIdentifier,
};
[self formEligibleForGenerationFound:newGenerationData];
}
__weak SharedPasswordController* weakSelf = self;
auto formDataCompletion = ^(BOOL found, const autofill::FormData& form) {
autofill::FormSignature formSignature =
found ? CalculateFormSignature(form) : autofill::FormSignature(0);
autofill::FieldSignature fieldSignature = autofill::FieldSignature(0);
int maxLength = 0;
if (found) {
for (const autofill::FormFieldData& field : form.fields) {
if (field.unique_renderer_id == fieldIdentifier) {
fieldSignature = CalculateFieldSignatureForField(field);
maxLength = field.max_length;
break;
}
}
}
std::u16string generatedPassword =
[self->_driverHelper PasswordGenerationHelper:frame]->GeneratePassword(
[self lastCommittedURL], formSignature, fieldSignature, maxLength);
self.generatedPotentialPassword = SysUTF16ToNSString(generatedPassword);
auto clearPotentialPassword = ^{
weakSelf.generatedPotentialPassword = nil;
};
[self.delegate
sharedPasswordController:self
showGeneratedPotentialPassword:self.generatedPotentialPassword
decisionHandler:^(BOOL accept) {
if (accept) {
LogPasswordGenerationEvent(
autofill::password_generation::
PASSWORD_ACCEPTED);
[weakSelf
injectGeneratedPasswordForFormId:formIdentifier
inFrame:frame
generatedPassword:
weakSelf
.generatedPotentialPassword
completionHandler:
clearPotentialPassword];
} else {
clearPotentialPassword();
}
}];
};
[self.formHelper extractPasswordFormData:formIdentifier
inFrame:frame
completionHandler:formDataCompletion];
IOSPasswordManagerDriver* driver =
[_driverHelper PasswordManagerDriver:frame];
_passwordManager->SetGenerationElementAndTypeForForm(
driver, formIdentifier, fieldIdentifier,
isManuallyTriggered ? PasswordGenerationType::kManual
: PasswordGenerationType::kAutomatic);
}
- (void)injectGeneratedPasswordForFormId:(FormRendererId)formIdentifier
inFrame:(web::WebFrame*)frame
generatedPassword:(NSString*)generatedPassword
completionHandler:(void (^)())completionHandler {
const autofill::PasswordFormGenerationData* generationData =
[self formForGenerationFromFormID:formIdentifier];
if (!generationData) {
return;
}
FieldRendererId newPasswordUniqueId =
generationData->new_password_renderer_id;
FieldRendererId confirmPasswordUniqueId =
generationData->confirmation_password_renderer_id;
__weak SharedPasswordController* weakSelf = self;
auto generatedPasswordInjected = ^(BOOL success) {
if (success) {
[weakSelf onFilledPasswordForm:formIdentifier
withGeneratedPassword:generatedPassword
passwordUniqueId:newPasswordUniqueId
inFrame:frame];
}
if (completionHandler) {
completionHandler();
}
};
[self.formHelper fillPasswordForm:formIdentifier
inFrame:frame
newPasswordIdentifier:newPasswordUniqueId
confirmPasswordIdentifier:confirmPasswordUniqueId
generatedPassword:generatedPassword
completionHandler:generatedPasswordInjected];
}
- (void)onFilledPasswordForm:(FormRendererId)formIdentifier
withGeneratedPassword:(NSString*)generatedPassword
passwordUniqueId:(FieldRendererId)newPasswordUniqueId
inFrame:(web::WebFrame*)frame {
__weak SharedPasswordController* weakSelf = self;
auto passwordPresaved = ^(BOOL found, const autofill::FormData& form) {
// If the form isn't found, it disappeared between the call to
// [self.formHelper fillPasswordForm:newPasswordIdentifier:...]
// and here. There isn't much that can be done.
if (!found)
return;
[weakSelf presaveGeneratedPassword:generatedPassword
passwordUniqueId:newPasswordUniqueId
formData:form
inFrame:frame];
};
[self.formHelper extractPasswordFormData:formIdentifier
inFrame:frame
completionHandler:passwordPresaved];
self.isPasswordGenerated = YES;
self.passwordGeneratedIdentifier = newPasswordUniqueId;
}
- (void)presaveGeneratedPassword:(NSString*)generatedPassword
passwordUniqueId:(FieldRendererId)newPasswordUniqueId
formData:(const autofill::FormData&)formData
inFrame:(web::WebFrame*)frame {
if (!_passwordManager)
return;
_passwordManager->OnPresaveGeneratedPassword(
[_driverHelper PasswordManagerDriver:frame], formData,
SysNSStringToUTF16(generatedPassword));
}
// Checks that all fields with |fieldIds| have user input recorded by
// the FieldDataManager.
- (BOOL)allFieldsContainUserInput:
(const std::vector<FieldRendererId>&)fieldIds {
for (auto fieldId : fieldIds) {
if (!self.formHelper.fieldDataManager->HasFieldData(fieldId)) {
return NO;
}
}
return YES;
}
#pragma mark - FormActivityObserver
- (void)webState:(web::WebState*)webState
didRegisterFormActivity:(const autofill::FormActivityParams&)params
inFrame:(web::WebFrame*)frame {
DCHECK_EQ(_webState, webState);
GURL pageURL;
if (!GetPageURLAndCheckTrustLevel(webState, &pageURL) || !frame ||
params.input_missing) {
_lastFocusedFormIdentifier = FormRendererId();
_lastFocusedFieldIdentifier = FieldRendererId();
_lastFocusedFrame = nullptr;
return;
}
if (params.type == "input" || params.type == "change") {
_lastSubmittedPasswordManagerDriver =
IOSPasswordManagerDriverFactory::GetRetainableDriver(_webState, frame);
}
if (params.type == "focus") {
_lastFocusedFormIdentifier = params.unique_form_id;
_lastFocusedFieldIdentifier = params.unique_field_id;
_lastFocusedFrame = frame;
}
// If there's a change in password forms on a page, they should be parsed
// again.
if (params.type == "form_changed") {
[self findPasswordFormsAndSendToPasswordStoreForFormChange:true
inFrame:frame];
}
}
// If the form was removed, PasswordManager should be informed to decide
// whether the form was submitted.
- (void)webState:(web::WebState*)webState
didRegisterFormRemoval:(const autofill::FormRemovalParams&)params
inFrame:(web::WebFrame*)frame {
DCHECK_EQ(_webState, webState);
if (!params.unique_form_id) {
// If formless password fields were removed, check that all of them had
// user input.
if (![self allFieldsContainUserInput:params.removed_unowned_fields]) {
return;
}
}
auto fieldDataManager =
UniqueIDDataTabHelper::FromWebState(_webState)->GetFieldDataManager();
_passwordManager->OnPasswordFormRemoved(
[_driverHelper PasswordManagerDriver:frame], *fieldDataManager,
params.unique_form_id);
}
@end