// 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> #import "base/ios/weak_nsobject.h" #include "base/mac/foundation_util.h" #include "base/mac/scoped_block.h" #include "base/mac/scoped_nsobject.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" 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(dconnelly): Figure out how (and if) to determine if |field| is visible. // http://crbug.com/433856 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. // TODO(dconnelly): Try to find a way to share this code with the upstream // implementation, even though it lives in the renderer. // http://crbug.com/434679. - (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. scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge; // The origin URLs of forms on the current page that contain account creation // forms as reported by autofill. std::vector<GURL> _accountCreationFormOrigins; // 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. scoped_ptr<autofill::PasswordForm> _possibleAccountCreationForm; // Password fields found in |_possibleAccountCreationForm|. std::vector<autofill::FormFieldData> _passwordFields; // The password field that triggers the password generation UI. scoped_ptr<autofill::FormFieldData> _passwordGenerationField; // Wrapper for suggestion JavaScript. Used for form navigation. base::scoped_nsobject<JsSuggestionManager> _JSSuggestionManager; // Wrapper for passwords JavaScript. Used for form filling. base::scoped_nsobject<JsPasswordManager> _JSPasswordManager; // 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. base::mac::ScopedBlock<AccessoryViewReadyCompletion> _accessoryViewReadyCompletion; // The delegate for controlling the password generation UI. base::scoped_nsprotocol<id<PasswordsUiDelegate>> _passwords_ui_delegate; // The password that was generated and accepted by the user. base::scoped_nsobject<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>)UIDelegate { JsPasswordManager* JSPasswordManager = base::mac::ObjCCast<JsPasswordManager>([webState->GetJSInjectionReceiver() instanceOfClass:[JsPasswordManager class]]); JsSuggestionManager* JSSuggestionManager = base::mac::ObjCCast<JsSuggestionManager>( [webState->GetJSInjectionReceiver() instanceOfClass:[JsSuggestionManager class]]); return [self initWithWebState:webState passwordManager:passwordManager passwordManagerDriver:driver JSPasswordManager:JSPasswordManager JSSuggestionManager:JSSuggestionManager passwordsUiDelegate:UIDelegate]; } - (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 { DCHECK([NSThread isMainThread]); DCHECK(webState); DCHECK_EQ([self class], [PasswordGenerationAgent class]); self = [super init]; if (self) { _passwordManager = passwordManager; _passwordManagerDriver = driver; _JSPasswordManager.reset([JSPasswordManager retain]); _JSSuggestionManager.reset([JSSuggestionManager retain]); _webStateObserverBridge.reset( new web::WebStateObserverBridge(webState, self)); _passwords_ui_delegate.reset([UIDelegate retain]); } return self; } - (void)dealloc { DCHECK([NSThread isMainThread]); [super dealloc]; } - (autofill::PasswordForm*)possibleAccountCreationForm { return _possibleAccountCreationForm.get(); } - (const std::vector<autofill::FormFieldData>&)passwordFields { return _passwordFields; } - (autofill::FormFieldData*)passwordGenerationField { return _passwordGenerationField.get(); } - (void)clearState { [self hideAlert]; _accountCreationFormOrigins.clear(); _allowedGenerationFormOrigins.clear(); _possibleAccountCreationForm.reset(); _passwordFields.clear(); _passwordGenerationField.reset(); _generatedPassword.reset(); } - (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)registerAccountCreationForms: (const std::vector<autofill::FormData>&)forms { for (const auto& form : forms) _accountCreationFormOrigins.push_back(form.origin); [self determinePasswordGenerationField]; } - (void)allowPasswordGenerationForForm:(const autofill::PasswordForm&)form { _allowedGenerationFormOrigins.push_back(form.origin); [self determinePasswordGenerationField]; } - (void)processParsedPasswordForms: (const std::vector<autofill::PasswordForm>&)forms { // TODO(dconnelly): Find a way to share some of this logic with the desktop // agent. http://crbug.com/434679. 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; if (!VectorContainsURL(_accountCreationFormOrigins, 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] autorelease] : [[[PasswordGenerationOfferView alloc] initWithDelegate:self] autorelease]; } #pragma mark - #pragma mark CRWWebStateObserver - (void)webStateDidLoadPage:(web::WebState*)webState { [self clearState]; } - (void)webStateDestroyed:(web::WebState*)webState { [self clearState]; _webStateObserverBridge.reset(); } #pragma mark - #pragma mark PasswordGenerationPromptDelegate - (void)acceptPasswordGeneration:(id)sender { [self hideAlert]; base::WeakNSObject<PasswordGenerationAgent> weakSelf(self); id completionHandler = ^(BOOL success) { if (!success) return; base::scoped_nsobject<PasswordGenerationAgent> strongSelf( [weakSelf retain]); if (!strongSelf) return; if (strongSelf.get()->_passwordManager) { // Might be null in tests. strongSelf.get()->_passwordManager->SetHasGeneratedPasswordForForm( strongSelf.get()->_passwordManagerDriver, *strongSelf.get()->_possibleAccountCreationForm, true); } if (strongSelf.get()->_accessoryViewReadyCompletion) { strongSelf.get()->_accessoryViewReadyCompletion.get()( [strongSelf currentAccessoryView], strongSelf); } }; [_JSPasswordManager fillPasswordForm:[self passwordGenerationFormName] withGeneratedPassword:_generatedPassword completionHandler:completionHandler]; } - (void)showSavedPasswords:(id)sender { [self hideAlert]; base::scoped_nsobject<GenericChromeCommand> command( [[GenericChromeCommand alloc] initWithTag:IDC_SHOW_SAVE_PASSWORDS_SETTINGS]); [command executeOnMainWindow]; } #pragma mark - #pragma mark PasswordGenerationOfferDelegate - (void)generatePassword { _generatedPassword.reset([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.reset([base::SysUTF8ToNSString(value) copy]); accessoryViewUpdateBlock([self currentAccessoryView], self); _accessoryViewReadyCompletion.reset([accessoryViewUpdateBlock copy]); } - (void)inputAccessoryViewControllerDidReset: (FormInputAccessoryViewController*)controller { [self hideAlert]; DCHECK(_accessoryViewReadyCompletion); _accessoryViewReadyCompletion.reset(); } - (void)resizeAccessoryView { DCHECK(_accessoryViewReadyCompletion); _accessoryViewReadyCompletion.get()([self currentAccessoryView], self); } - (BOOL)getLogKeyboardAccessoryMetrics { // Only store metrics for regular Autofill, not passwords. return NO; } @end