| // Copyright 2014 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/passwords/password_generation_agent.h" |
| |
| #include <stddef.h> |
| |
| #include "base/mac/foundation_util.h" |
| #include "base/mac/scoped_block.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/autofill/core/browser/password_generator.h" |
| #include "components/autofill/core/common/form_data.h" |
| #include "components/autofill/core/common/password_form.h" |
| #include "components/autofill/core/common/password_generation_util.h" |
| #import "components/autofill/ios/browser/js_suggestion_manager.h" |
| #include "components/password_manager/core/browser/password_manager.h" |
| #include "google_apis/gaia/gaia_urls.h" |
| #import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" |
| #include "ios/chrome/browser/experimental_flags.h" |
| #import "ios/chrome/browser/passwords/js_password_manager.h" |
| #import "ios/chrome/browser/passwords/password_generation_edit_view.h" |
| #import "ios/chrome/browser/ui/commands/generic_chrome_command.h" |
| #include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
| #include "ios/web/public/url_scheme_util.h" |
| #import "ios/web/public/web_state/js/crw_js_injection_receiver.h" |
| #include "ios/web/public/web_state/web_state.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| // Target length of generated passwords. |
| const int kGeneratedPasswordLength = 20; |
| |
| // The minimum number of text fields that a form needs to be considered as |
| // an account creation form. |
| const size_t kMinimumTextFieldsForAccountCreation = 3; |
| |
| // Returns true if |urls| contains |url|. |
| bool VectorContainsURL(const std::vector<GURL>& urls, const GURL& url) { |
| return std::find(urls.begin(), urls.end(), url) != urls.end(); |
| } |
| |
| // Returns whether |field| should be considered a text field. Implementation |
| // mirrors that of password_controller.js. |
| // TODO(crbug.com/433856): Figure out how to determine if |field| is visible. |
| bool IsTextField(const autofill::FormFieldData& field) { |
| return field.form_control_type == "text" || |
| field.form_control_type == "email" || |
| field.form_control_type == "number" || |
| field.form_control_type == "tel" || field.form_control_type == "url" || |
| field.form_control_type == "search" || |
| field.form_control_type == "password"; |
| } |
| |
| } // namespace |
| |
| @interface PasswordGenerationAgent ()<CRWWebStateObserver, |
| FormInputAccessoryViewProvider, |
| PasswordGenerationOfferDelegate, |
| PasswordGenerationPromptDelegate> |
| |
| // Clears all per-page state. |
| - (void)clearState; |
| |
| // Returns YES if |form| belongs to the GAIA realm. |
| - (BOOL)formHasGAIARealm:(const autofill::PasswordForm&)form; |
| |
| // Returns YES if |form| contains enough text fields to be considered as an |
| // account creation form. |
| - (BOOL)formHasEnoughTextFieldsForAccountCreation: |
| (const autofill::PasswordForm&)form; |
| |
| // Returns a list of all password fields in |form|. |
| - (std::vector<autofill::FormFieldData>)passwordFieldsInForm: |
| (const autofill::PasswordForm&)form; |
| |
| // Merges the data from local heuristics, the autofill server, and the password |
| // manager to find the field that should trigger the password generation UI |
| // when selected by the user. The resulting field is stored in |
| // |_passwordGenerationField|. This logic is nearly identical to that of the |
| // upstream autofill::PasswordGenerationAgent::DetermineGenerationElement. |
| - (void)determinePasswordGenerationField; |
| |
| // Returns YES if the specified form and field should trigger the |
| // password generation UI. |
| - (BOOL)isGenerationForm:(const base::string16&)formName |
| field:(const base::string16&)fieldName; |
| |
| // The name of the form identified as an account creation form, if it exists. |
| - (NSString*)passwordGenerationFormName; |
| |
| // Hides and deletes the alert with generation prompt, if it exists. |
| - (void)hideAlert; |
| |
| // Returns an autoreleased input accessory view corresponding to the current |
| // password generation state. Should only be used when password generation |
| // should be offered for the currently-focused form field. |
| - (UIView*)currentAccessoryView; |
| |
| // Initializes PasswordGenerationAgent, which observes the specified web state, |
| // and allows injecting JavaScript managers. |
| - (instancetype) |
| initWithWebState:(web::WebState*)webState |
| passwordManager:(password_manager::PasswordManager*)passwordManager |
| passwordManagerDriver:(password_manager::PasswordManagerDriver*)driver |
| JSPasswordManager:(JsPasswordManager*)JSPasswordManager |
| JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager |
| passwordsUiDelegate:(id<PasswordsUiDelegate>)UIDelegate |
| NS_DESIGNATED_INITIALIZER; |
| |
| @end |
| |
| @implementation PasswordGenerationAgent { |
| // Bridge to observe the web state from Objective-C. |
| std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge; |
| |
| // The origin URLs of forms on the current page that have not been blacklisted |
| // by the password manager. |
| std::vector<GURL> _allowedGenerationFormOrigins; |
| |
| // Stores the account creation form we detected on the page. |
| std::unique_ptr<autofill::PasswordForm> _possibleAccountCreationForm; |
| |
| // Password fields found in |_possibleAccountCreationForm|. |
| std::vector<autofill::FormFieldData> _passwordFields; |
| |
| // The password field that triggers the password generation UI. |
| std::unique_ptr<autofill::FormFieldData> _passwordGenerationField; |
| |
| // Wrapper for suggestion JavaScript. Used for form navigation. |
| JsSuggestionManager* _suggestionManager; |
| |
| // Wrapper for passwords JavaScript. Used for form filling. |
| JsPasswordManager* _javaScriptPasswordManager; |
| |
| // Driver that is passed to PasswordManager when a password is generated. |
| password_manager::PasswordManagerDriver* _passwordManagerDriver; |
| |
| // PasswordManager to inform when a password is generated. |
| password_manager::PasswordManager* _passwordManager; |
| |
| // Callback to update the custom keyboard accessory view. Will be non-nil when |
| // this PasswordGenerationAgent controls the keyboard accessory view. |
| AccessoryViewReadyCompletion _accessoryViewReadyCompletion; |
| |
| // The delegate for controlling the password generation UI. |
| id<PasswordsUiDelegate> _passwords_ui_delegate; |
| |
| // The password that was generated and accepted by the user. |
| NSString* _generatedPassword; |
| } |
| |
| - (instancetype)init { |
| NOTREACHED(); |
| return nil; |
| } |
| |
| - (instancetype) |
| initWithWebState:(web::WebState*)webState |
| passwordManager:(password_manager::PasswordManager*)passwordManager |
| passwordManagerDriver:(password_manager::PasswordManagerDriver*)driver |
| passwordsUiDelegate:(id<PasswordsUiDelegate>)delegate { |
| JsPasswordManager* javaScriptPasswordManager = |
| base::mac::ObjCCast<JsPasswordManager>([webState->GetJSInjectionReceiver() |
| instanceOfClass:[JsPasswordManager class]]); |
| JsSuggestionManager* suggestionManager = |
| base::mac::ObjCCast<JsSuggestionManager>( |
| [webState->GetJSInjectionReceiver() |
| instanceOfClass:[JsSuggestionManager class]]); |
| return [self initWithWebState:webState |
| passwordManager:passwordManager |
| passwordManagerDriver:driver |
| JSPasswordManager:javaScriptPasswordManager |
| JSSuggestionManager:suggestionManager |
| passwordsUiDelegate:delegate]; |
| } |
| |
| - (instancetype) |
| initWithWebState:(web::WebState*)webState |
| passwordManager:(password_manager::PasswordManager*)passwordManager |
| passwordManagerDriver:(password_manager::PasswordManagerDriver*)driver |
| JSPasswordManager:(JsPasswordManager*)javaScriptPasswordManager |
| JSSuggestionManager:(JsSuggestionManager*)suggestionManager |
| passwordsUiDelegate:(id<PasswordsUiDelegate>)delegate { |
| DCHECK([NSThread isMainThread]); |
| DCHECK(webState); |
| DCHECK_EQ([self class], [PasswordGenerationAgent class]); |
| self = [super init]; |
| if (self) { |
| _passwordManager = passwordManager; |
| _passwordManagerDriver = driver; |
| _javaScriptPasswordManager = javaScriptPasswordManager; |
| _suggestionManager = suggestionManager; |
| _webStateObserverBridge.reset( |
| new web::WebStateObserverBridge(webState, self)); |
| _passwords_ui_delegate = delegate; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| DCHECK([NSThread isMainThread]); |
| } |
| |
| - (autofill::PasswordForm*)possibleAccountCreationForm { |
| return _possibleAccountCreationForm.get(); |
| } |
| |
| - (const std::vector<autofill::FormFieldData>&)passwordFields { |
| return _passwordFields; |
| } |
| |
| - (autofill::FormFieldData*)passwordGenerationField { |
| return _passwordGenerationField.get(); |
| } |
| |
| - (void)clearState { |
| [self hideAlert]; |
| _allowedGenerationFormOrigins.clear(); |
| _possibleAccountCreationForm.reset(); |
| _passwordFields.clear(); |
| _passwordGenerationField.reset(); |
| _generatedPassword = nil; |
| } |
| |
| - (BOOL)formHasGAIARealm:(const autofill::PasswordForm&)form { |
| // Do not generate password for GAIA since it is used to retrieve the |
| // generated paswords. |
| return GURL(form.signon_realm) == |
| GaiaUrls::GetInstance()->gaia_login_form_realm(); |
| } |
| |
| - (BOOL)formHasEnoughTextFieldsForAccountCreation: |
| (const autofill::PasswordForm&)form { |
| size_t numVisibleTextFields = 0; |
| for (const auto& formFieldData : form.form_data.fields) |
| if (IsTextField(formFieldData)) |
| ++numVisibleTextFields; |
| return (numVisibleTextFields >= kMinimumTextFieldsForAccountCreation); |
| } |
| |
| - (std::vector<autofill::FormFieldData>)passwordFieldsInForm: |
| (const autofill::PasswordForm&)form { |
| std::vector<autofill::FormFieldData> passwordFields; |
| for (const auto& formFieldData : form.form_data.fields) |
| if (formFieldData.form_control_type == "password") |
| passwordFields.push_back(formFieldData); |
| return passwordFields; |
| } |
| |
| - (void)allowPasswordGenerationForForm:(const autofill::PasswordForm&)form { |
| _allowedGenerationFormOrigins.push_back(form.origin); |
| [self determinePasswordGenerationField]; |
| } |
| |
| - (void)processParsedPasswordForms: |
| (const std::vector<autofill::PasswordForm>&)forms { |
| for (const auto& passwordForm : forms) { |
| if ([self formHasGAIARealm:passwordForm]) |
| continue; |
| if (![self formHasEnoughTextFieldsForAccountCreation:passwordForm]) |
| continue; |
| std::vector<autofill::FormFieldData> passwordFields( |
| [self passwordFieldsInForm:passwordForm]); |
| if (passwordFields.empty()) |
| continue; |
| // This form checks out. |
| _possibleAccountCreationForm.reset( |
| new autofill::PasswordForm(passwordForm)); |
| _passwordFields = passwordFields; |
| break; |
| } |
| [self determinePasswordGenerationField]; |
| } |
| |
| - (void)determinePasswordGenerationField { |
| // If the current page hasn't been parsed yet or doesn't contain any account |
| // creation forms, wait. |
| if (!_possibleAccountCreationForm) |
| return; |
| if (_passwordFields.empty()) |
| return; |
| |
| // If the form origin hasn't been cleared by both the autofill and the |
| // password manager, wait. |
| GURL origin = _possibleAccountCreationForm->origin; |
| if (!experimental_flags::UseOnlyLocalHeuristicsForPasswordGeneration()) { |
| if (!VectorContainsURL(_allowedGenerationFormOrigins, origin)) |
| return; |
| } |
| |
| // Use the first password field in the form as the generation field. |
| _passwordGenerationField.reset( |
| new autofill::FormFieldData(_passwordFields[0])); |
| autofill::password_generation::LogPasswordGenerationEvent( |
| autofill::password_generation::GENERATION_AVAILABLE); |
| } |
| |
| - (id<FormInputAccessoryViewProvider>)accessoryViewProvider { |
| return self; |
| } |
| |
| - (BOOL)isGenerationForm:(const base::string16&)formName |
| field:(const base::string16&)fieldName { |
| return _possibleAccountCreationForm && |
| _possibleAccountCreationForm->form_data.name == formName && |
| _passwordGenerationField && |
| _passwordGenerationField->name == fieldName; |
| } |
| |
| - (NSString*)passwordGenerationFormName { |
| return base::SysUTF16ToNSString(_possibleAccountCreationForm->form_data.name); |
| } |
| |
| - (void)hideAlert { |
| [_passwords_ui_delegate hideGenerationAlert]; |
| } |
| |
| - (UIView*)currentAccessoryView { |
| return [_generatedPassword length] > 0 |
| ? [[PasswordGenerationEditView alloc] |
| initWithPassword:_generatedPassword] |
| : [[PasswordGenerationOfferView alloc] initWithDelegate:self]; |
| } |
| |
| #pragma mark - |
| #pragma mark CRWWebStateObserver |
| |
| - (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success { |
| [self clearState]; |
| } |
| |
| - (void)webStateDestroyed:(web::WebState*)webState { |
| [self clearState]; |
| _webStateObserverBridge.reset(); |
| } |
| |
| #pragma mark - |
| #pragma mark PasswordGenerationPromptDelegate |
| |
| - (void)acceptPasswordGeneration:(id)sender { |
| [self hideAlert]; |
| __weak PasswordGenerationAgent* weakSelf = self; |
| id completionHandler = ^(BOOL success) { |
| if (!success) |
| return; |
| PasswordGenerationAgent* strongSelf = weakSelf; |
| if (!strongSelf) |
| return; |
| if (strongSelf->_passwordManager) { |
| // Might be null in tests. |
| strongSelf->_passwordManager->SetHasGeneratedPasswordForForm( |
| strongSelf->_passwordManagerDriver, |
| *strongSelf->_possibleAccountCreationForm, true); |
| } |
| if (strongSelf->_accessoryViewReadyCompletion) { |
| strongSelf->_accessoryViewReadyCompletion( |
| [strongSelf currentAccessoryView], strongSelf); |
| } |
| }; |
| [_javaScriptPasswordManager fillPasswordForm:[self passwordGenerationFormName] |
| withGeneratedPassword:_generatedPassword |
| completionHandler:completionHandler]; |
| } |
| |
| - (void)showSavedPasswords:(id)sender { |
| [self hideAlert]; |
| GenericChromeCommand* command = [[GenericChromeCommand alloc] |
| initWithTag:IDC_SHOW_SAVE_PASSWORDS_SETTINGS]; |
| [command executeOnMainWindow]; |
| } |
| |
| #pragma mark - |
| #pragma mark PasswordGenerationOfferDelegate |
| |
| - (void)generatePassword { |
| _generatedPassword = [base::SysUTF8ToNSString( |
| autofill::PasswordGenerator(kGeneratedPasswordLength).Generate()) copy]; |
| [_passwords_ui_delegate showGenerationAlertWithPassword:_generatedPassword |
| andPromptDelegate:self]; |
| } |
| |
| #pragma mark - |
| #pragma mark FormInputAccessoryViewProvider |
| |
| - (id<FormInputAccessoryViewDelegate>)accessoryViewDelegate { |
| return nil; |
| } |
| |
| - (void)setAccessoryViewDelegate:(id<FormInputAccessoryViewDelegate>)delegate { |
| // Unused. |
| } |
| |
| - (void) |
| checkIfAccessoryViewIsAvailableForFormNamed:(const std::string&)formName |
| fieldName:(const std::string&)fieldName |
| webState:(web::WebState*)webState |
| completionHandler: |
| (AccessoryViewAvailableCompletion) |
| completionHandler { |
| completionHandler( |
| _passwordGenerationField && |
| [self isGenerationForm:base::UTF8ToUTF16(formName) |
| field:base::UTF8ToUTF16(fieldName)]); |
| } |
| |
| - (void)retrieveAccessoryViewForFormNamed:(const std::string&)formName |
| fieldName:(const std::string&)fieldName |
| value:(const std::string&)value |
| type:(const std::string&)type |
| webState:(web::WebState*)webState |
| accessoryViewUpdateBlock: |
| (AccessoryViewReadyCompletion)accessoryViewUpdateBlock { |
| DCHECK(!_accessoryViewReadyCompletion); |
| if ([_generatedPassword length] > 0) |
| _generatedPassword = [base::SysUTF8ToNSString(value) copy]; |
| accessoryViewUpdateBlock([self currentAccessoryView], self); |
| _accessoryViewReadyCompletion = [accessoryViewUpdateBlock copy]; |
| } |
| |
| - (void)inputAccessoryViewControllerDidReset: |
| (FormInputAccessoryViewController*)controller { |
| [self hideAlert]; |
| DCHECK(_accessoryViewReadyCompletion); |
| _accessoryViewReadyCompletion = nil; |
| } |
| |
| - (void)resizeAccessoryView { |
| DCHECK(_accessoryViewReadyCompletion); |
| _accessoryViewReadyCompletion([self currentAccessoryView], self); |
| } |
| |
| - (BOOL)getLogKeyboardAccessoryMetrics { |
| // Only store metrics for regular Autofill, not passwords. |
| return NO; |
| } |
| |
| @end |