Index: ios/chrome/browser/autofill/autofill_agent.mm |
diff --git a/ios/chrome/browser/autofill/autofill_agent.mm b/ios/chrome/browser/autofill/autofill_agent.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..1b95bf5a80d07db638437b71745aeee2fb408d10 |
--- /dev/null |
+++ b/ios/chrome/browser/autofill/autofill_agent.mm |
@@ -0,0 +1,870 @@ |
+// Copyright 2013 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/autofill/autofill_agent.h" |
+ |
+#include <memory> |
+#include <string> |
+ |
+#include "base/format_macros.h" |
+#include "base/guid.h" |
+#include "base/json/json_reader.h" |
+#include "base/json/json_writer.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/mac/scoped_block.h" |
+#include "base/metrics/field_trial.h" |
+#include "base/strings/string16.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "base/strings/utf_string_conversions.h" |
+#include "base/values.h" |
+#include "components/autofill/core/browser/autofill_manager.h" |
+#include "components/autofill/core/browser/autofill_metrics.h" |
+#include "components/autofill/core/browser/autofill_profile.h" |
+#include "components/autofill/core/browser/credit_card.h" |
+#include "components/autofill/core/browser/keyboard_accessory_metrics_logger.h" |
+#include "components/autofill/core/browser/popup_item_ids.h" |
+#include "components/autofill/core/common/autofill_constants.h" |
+#include "components/autofill/core/common/autofill_pref_names.h" |
+#include "components/autofill/core/common/autofill_util.h" |
+#include "components/autofill/core/common/form_data.h" |
+#include "components/autofill/core/common/form_field_data.h" |
+#include "components/autofill/ios/browser/autofill_driver_ios.h" |
+#import "components/autofill/ios/browser/form_suggestion.h" |
+#import "components/autofill/ios/browser/js_autofill_manager.h" |
+#include "components/prefs/pref_service.h" |
+#include "ios/chrome/browser/application_context.h" |
+#include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
+#include "ios/chrome/browser/pref_names.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/url_verification_constants.h" |
+#import "ios/web/public/web_state/web_state.h" |
+#include "ui/gfx/geometry/rect.h" |
+#include "url/gurl.h" |
+ |
+#if !defined(__has_feature) || !__has_feature(objc_arc) |
+#error "This file requires ARC support." |
+#endif |
+ |
+namespace { |
+ |
+using FormDataVector = std::vector<autofill::FormData>; |
+ |
+// The type of the completion handler block for |
+// |fetchFormsWithName:minimumRequiredFieldsCount:pageURL:completionHandler| |
+typedef void (^FetchFormsCompletionHandler)(BOOL, const FormDataVector&); |
+ |
+// Gets the first form and field specified by |fieldName| from |forms|, |
+// modifying the returned field so that input elements are also handled. |
+void GetFormAndField(autofill::FormData* form, |
+ autofill::FormFieldData* field, |
+ const FormDataVector& forms, |
+ const std::string& fieldName, |
+ const std::string& type) { |
+ DCHECK_GE(forms.size(), 1U); |
+ *form = forms[0]; |
+ const base::string16 fieldName16 = base::UTF8ToUTF16(fieldName); |
+ for (const auto& currentField : form->fields) { |
+ if (currentField.name == fieldName16) { |
+ *field = currentField; |
+ break; |
+ } |
+ } |
+ if (field->SameFieldAs(autofill::FormFieldData())) |
+ return; |
+ |
+ // Hack to get suggestions from select input elements. |
+ if (field->form_control_type == "select-one") { |
+ // Any value set will cause the AutofillManager to filter suggestions (only |
+ // show suggestions that begin the same as the current value) with the |
+ // effect that one only suggestion would be returned; the value itself. |
+ field->value = base::string16(); |
+ } |
+} |
+ |
+} // namespace |
+ |
+@interface AutofillAgent ()<CRWWebStateObserver> |
+ |
+// Notifies the autofill manager when forms are detected on a page. |
+- (void)notifyAutofillManager:(autofill::AutofillManager*)autofillManager |
+ ofFormsSeen:(const FormDataVector&)forms; |
+ |
+// Notifies the autofill manager when forms are submitted. |
+- (void)notifyAutofillManager:(autofill::AutofillManager*)autofillManager |
+ ofFormsSubmitted:(const FormDataVector&)forms |
+ userInitiated:(BOOL)userInitiated; |
+ |
+// Invokes the form extraction script and loads the output into the format |
+// expected by the AutofillManager. |
+// If |formName| is non-empty, only a form of that name is extracted. |
+// Only forms with at least |requiredFieldsCount| fields are extracted. |
+// Calls |completionHandler| with a success BOOL of YES and the form data that |
+// was extracted. |
+// Calls |completionHandler| with NO if the forms could not be extracted. |
+// |completionHandler| cannot be nil. |
+- (void)fetchFormsWithName:(const base::string16&)formName |
+ minimumRequiredFieldsCount:(NSUInteger)requiredFieldsCount |
+ pageURL:(const GURL&)pageURL |
+ completionHandler:(FetchFormsCompletionHandler)completionHandler; |
+ |
+// Processes the JSON form data extracted from the page into the format expected |
+// by AutofillManager and fills it in |formsData|. |
+// |formsData| cannot be nil. |
+// Returns a BOOL indicating the success value and the vector of form data. |
+- (BOOL)getExtractedFormsData:(FormDataVector*)formsData |
+ fromJSON:(NSString*)formJSON |
+ formName:(const base::string16&)formName |
+ pageURL:(const GURL&)pageURL; |
+ |
+// Processes the JSON form data extracted from the page when form activity is |
+// detected and informs the AutofillManager. |
+- (void)processFormActivityExtractedData:(const FormDataVector&)forms |
+ fieldName:(const std::string&)fieldName |
+ type:(const std::string&)type |
+ webState:(web::WebState*)webState; |
+ |
+// Sends a request to AutofillManager to retrieve suggestions for the specified |
+// form and field. |
+- (void)queryAutofillWithForms:(const FormDataVector&)forms |
+ field:(NSString*)fieldName |
+ type:(NSString*)type |
+ typedValue:(NSString*)typedValue |
+ webState:(web::WebState*)webState |
+ completionHandler:(SuggestionsAvailableCompletion)completion; |
+ |
+// Rearranges and filters the suggestions to move profile or credit card |
+// suggestions to the front if the user has selected one recently and remove |
+// key/value suggestions if the user hasn't started typing. |
+- (NSArray*)processSuggestions:(NSArray*)suggestions; |
+ |
+@end |
+ |
+@implementation AutofillAgent { |
+ // Timestamp of the first time forms are seen. |
+ base::TimeTicks formsSeenTimestamp_; |
+ |
+ // Bridge to observe the web state from Objective-C. |
+ std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge; |
+ |
+ // The browser state for which this agent was created. |
+ ios::ChromeBrowserState* browserState_; |
+ |
+ // Manager for Autofill JavaScripts. |
+ JsAutofillManager* jsAutofillManager_; |
+ |
+ // The name of the most recent autocomplete field; tracks the currently- |
+ // focused form element in order to force filling of the currently selected |
+ // form element, even if it's non-empty. |
+ base::string16 pendingAutocompleteField_; |
+ // The identifier of the most recent suggestion accepted by the user. Only |
+ // used to reorder future suggestion lists, placing matching suggestions first |
+ // in the list. |
+ NSInteger mostRecentSelectedIdentifier_; |
+ |
+ // Suggestions state: |
+ // The most recent form suggestions. |
+ NSArray* mostRecentSuggestions_; |
+ // The completion to inform FormSuggestionController that a user selection |
+ // has been handled. |
+ SuggestionHandledCompletion suggestionHandledCompletion_; |
+ // The completion to inform FormSuggestionController that suggestions are |
+ // available for a given form and field. |
+ SuggestionsAvailableCompletion suggestionsAvailableCompletion_; |
+ // The text entered by the user into the active field. |
+ NSString* typedValue_; |
+ // Popup delegate for the most recent suggestions. |
+ // The reference is weak because a weak pointer is sent to our |
+ // AutofillManagerDelegate. |
+ base::WeakPtr<autofill::AutofillPopupDelegate> popupDelegate_; |
+} |
+ |
+@synthesize browserState = browserState_; |
+ |
+- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState |
+ webState:(web::WebState*)webState { |
+ DCHECK(browserState); |
+ DCHECK(webState); |
+ self = [super init]; |
+ if (self) { |
+ browserState_ = browserState; |
+ _webStateObserverBridge.reset( |
+ new web::WebStateObserverBridge(webState, self)); |
+ jsAutofillManager_ = base::mac::ObjCCastStrict<JsAutofillManager>( |
+ [webState->GetJSInjectionReceiver() |
+ instanceOfClass:[JsAutofillManager class]]); |
+ } |
+ return self; |
+} |
+ |
+- (instancetype)init { |
+ NOTREACHED(); |
+ return nil; |
+} |
+ |
+- (void)detachFromWebState { |
+ [[NSNotificationCenter defaultCenter] removeObserver:self]; |
+ _webStateObserverBridge.reset(); |
+} |
+ |
+#pragma mark - |
+#pragma mark Private |
+ |
+// Returns the autofill manager associated with a web::WebState instance. |
+// Returns nullptr if there is no autofill manager associated anymore, this can |
+// happen when |close| has been called on the |webState|. Also returns nullptr |
+// if detachFromWebState has been called. |
+- (autofill::AutofillManager*)autofillManagerFromWebState: |
+ (web::WebState*)webState { |
+ if (!webState || !_webStateObserverBridge) |
+ return nullptr; |
+ return autofill::AutofillDriverIOS::FromWebState(webState) |
+ ->autofill_manager(); |
+} |
+ |
+// Extracts a single form field from the JSON dictionary into a FormFieldData |
+// object. |
+// Returns NO if the field could not be extracted. |
+- (BOOL)extractFormField:(const base::DictionaryValue&)field |
+ asFieldData:(autofill::FormFieldData*)fieldData { |
+ if (!field.GetString("name", &fieldData->name) || |
+ !field.GetString("form_control_type", &fieldData->form_control_type)) { |
+ return NO; |
+ } |
+ |
+ // Optional fields. |
+ field.GetString("label", &fieldData->label); |
+ field.GetString("value", &fieldData->value); |
+ field.GetString("autocomplete_attribute", &fieldData->autocomplete_attribute); |
+ |
+ int maxLength; |
+ if (field.GetInteger("max_length", &maxLength)) |
+ fieldData->max_length = maxLength; |
+ |
+ field.GetBoolean("is_autofilled", &fieldData->is_autofilled); |
+ |
+ // TODO(crbug.com/427614): Extract |is_checked|. |
+ bool isCheckable = false; |
+ field.GetBoolean("is_checkable", &isCheckable); |
+ autofill::SetCheckStatus(fieldData, isCheckable, false); |
+ |
+ field.GetBoolean("is_focusable", &fieldData->is_focusable); |
+ field.GetBoolean("should_autocomplete", &fieldData->should_autocomplete); |
+ |
+ // ROLE_ATTRIBUTE_OTHER is the default value. The only other value as of this |
+ // writing is ROLE_ATTRIBUTE_PRESENTATION. |
+ int role; |
+ if (field.GetInteger("role", &role) && |
+ role == autofill::AutofillField::ROLE_ATTRIBUTE_PRESENTATION) { |
+ fieldData->role = autofill::AutofillField::ROLE_ATTRIBUTE_PRESENTATION; |
+ } |
+ |
+ // TODO(crbug.com/427614): Extract |text_direction|. |
+ |
+ // Load option values where present. |
+ const base::ListValue* optionValues; |
+ if (field.GetList("option_values", &optionValues)) { |
+ for (const auto& optionValue : *optionValues) { |
+ base::string16 value; |
+ if (optionValue->GetAsString(&value)) |
+ fieldData->option_values.push_back(value); |
+ } |
+ } |
+ |
+ // Load option contents where present. |
+ const base::ListValue* optionContents; |
+ if (field.GetList("option_contents", &optionContents)) { |
+ for (const auto& optionContent : *optionContents) { |
+ base::string16 content; |
+ if (optionContent->GetAsString(&content)) |
+ fieldData->option_contents.push_back(content); |
+ } |
+ } |
+ |
+ if (fieldData->option_values.size() != fieldData->option_contents.size()) |
+ return NO; // Option values and contents lists should match 1-1. |
+ |
+ return YES; |
+} |
+ |
+- (void)notifyAutofillManager:(autofill::AutofillManager*)autofillManager |
+ ofFormsSeen:(const FormDataVector&)forms { |
+ DCHECK(autofillManager); |
+ DCHECK(!forms.empty()); |
+ autofillManager->Reset(); |
+ autofillManager->OnFormsSeen(forms, formsSeenTimestamp_); |
+} |
+ |
+- (void)notifyAutofillManager:(autofill::AutofillManager*)autofillManager |
+ ofFormsSubmitted:(const FormDataVector&)forms |
+ userInitiated:(BOOL)userInitiated { |
+ DCHECK(autofillManager); |
+ // Exactly one form should be extracted. |
+ DCHECK_EQ(1U, forms.size()); |
+ autofill::FormData form = forms[0]; |
+ |
+ // iOS doesn't get a separate "will submit form" notification so call |
+ // OnWillSubmitForm() here. |
+ autofillManager->OnWillSubmitForm(form, base::TimeTicks::Now()); |
+ autofillManager->OnFormSubmitted(form); |
+ autofill::KeyboardAccessoryMetricsLogger::OnFormSubmitted(); |
+} |
+ |
+- (void)fetchFormsWithName:(const base::string16&)formName |
+ minimumRequiredFieldsCount:(NSUInteger)requiredFieldsCount |
+ pageURL:(const GURL&)pageURL |
+ completionHandler:(FetchFormsCompletionHandler)completionHandler { |
+ DCHECK(completionHandler); |
+ // Necessary so the values can be used inside a block. |
+ base::string16 formNameCopy = formName; |
+ GURL pageURLCopy = pageURL; |
+ __weak AutofillAgent* weakSelf = self; |
+ [jsAutofillManager_ |
+ fetchFormsWithMinimumRequiredFieldsCount:requiredFieldsCount |
+ completionHandler:^(NSString* formJSON) { |
+ std::vector<autofill::FormData> formData; |
+ BOOL success = |
+ [weakSelf getExtractedFormsData:&formData |
+ fromJSON:formJSON |
+ formName:formNameCopy |
+ pageURL:pageURLCopy]; |
+ completionHandler(success, formData); |
+ }]; |
+} |
+ |
+- (BOOL)getExtractedFormsData:(FormDataVector*)formsData |
+ fromJSON:(NSString*)formJSON |
+ formName:(const base::string16&)formName |
+ pageURL:(const GURL&)pageURL { |
+ DCHECK(formsData); |
+ // Convert JSON string to JSON object |dataJson|. |
+ int errorCode = 0; |
+ std::string errorMessage; |
+ std::unique_ptr<base::Value> dataJson(base::JSONReader::ReadAndReturnError( |
+ base::SysNSStringToUTF8(formJSON), base::JSON_PARSE_RFC, &errorCode, |
+ &errorMessage)); |
+ if (errorCode) { |
+ LOG(WARNING) << "JSON parse error in form extraction: " |
+ << errorMessage.c_str(); |
+ return NO; |
+ } |
+ |
+ // Returned data should be a dictionary. |
+ const base::DictionaryValue* data; |
+ if (!dataJson->GetAsDictionary(&data)) |
+ return NO; |
+ |
+ // Get the list of forms. |
+ const base::ListValue* formsList; |
+ if (!data->GetList("forms", &formsList)) |
+ return NO; |
+ |
+ // Iterate through all the extracted forms and copy the data from JSON into |
+ // AutofillManager structures. |
+ for (const auto& formDict : *formsList) { |
+ // Each form list entry should be a JSON dictionary. |
+ const base::DictionaryValue* formData; |
+ if (!formDict->GetAsDictionary(&formData)) |
+ return NO; |
+ |
+ // Form data is copied into a FormData object field-by-field. |
+ autofill::FormData form; |
+ if (!formData->GetString("name", &form.name)) |
+ return NO; |
+ if (!formName.empty() && formName != form.name) |
+ continue; |
+ |
+ // Origin is mandatory. |
+ base::string16 origin; |
+ if (!formData->GetString("origin", &origin)) |
+ return NO; |
+ |
+ // Use GURL object to verify origin of host page URL. |
+ form.origin = GURL(origin); |
+ if (form.origin.GetOrigin() != pageURL.GetOrigin()) { |
+ LOG(WARNING) << "Form extraction aborted due to same origin policy"; |
+ return NO; |
+ } |
+ |
+ // Action is optional. |
+ base::string16 action; |
+ formData->GetString("action", &action); |
+ form.action = GURL(action); |
+ |
+ // Is form tag is optional. |
+ bool is_form_tag; |
+ if (formData->GetBoolean("is_form_tag", &is_form_tag)) |
+ form.is_form_tag = is_form_tag; |
+ |
+ // Field list (mandatory) is extracted. |
+ const base::ListValue* fieldsList; |
+ if (!formData->GetList("fields", &fieldsList)) |
+ return NO; |
+ for (const auto& fieldDict : *fieldsList) { |
+ const base::DictionaryValue* field; |
+ autofill::FormFieldData fieldData; |
+ if (fieldDict->GetAsDictionary(&field) && |
+ [self extractFormField:*field asFieldData:&fieldData]) { |
+ form.fields.push_back(fieldData); |
+ } else { |
+ return NO; |
+ } |
+ } |
+ formsData->push_back(form); |
+ } |
+ return YES; |
+} |
+ |
+- (NSArray*)processSuggestions:(NSArray*)suggestions { |
+ // The suggestion array is cloned (to claim ownership) and to slightly |
+ // reorder; a future improvement is to base order on text typed in other |
+ // fields by users as well as accepted suggestions (crbug.com/245261). |
+ NSMutableArray* suggestionsCopy = [suggestions mutableCopy]; |
+ |
+ // If the most recently selected suggestion was a profile or credit card |
+ // suggestion, move it to the front of the suggestions. |
+ if (mostRecentSelectedIdentifier_ > 0) { |
+ NSUInteger idx = [suggestionsCopy |
+ indexOfObjectPassingTest:^BOOL(id obj, NSUInteger, BOOL*) { |
+ FormSuggestion* suggestion = obj; |
+ return suggestion.identifier == mostRecentSelectedIdentifier_; |
+ }]; |
+ |
+ if (idx != NSNotFound) { |
+ FormSuggestion* suggestion = suggestionsCopy[idx]; |
+ [suggestionsCopy removeObjectAtIndex:idx]; |
+ [suggestionsCopy insertObject:suggestion atIndex:0]; |
+ } |
+ } |
+ |
+ // Filter out any key/value suggestions if the user hasn't typed yet. |
+ if ([typedValue_ length] == 0) { |
+ for (NSInteger idx = [suggestionsCopy count] - 1; idx >= 0; idx--) { |
+ FormSuggestion* suggestion = suggestionsCopy[idx]; |
+ if (suggestion.identifier == autofill::POPUP_ITEM_ID_AUTOCOMPLETE_ENTRY) { |
+ [suggestionsCopy removeObjectAtIndex:idx]; |
+ } |
+ } |
+ } |
+ |
+ // If "clear form" entry exists then move it to the front of the suggestions. |
+ for (NSInteger idx = [suggestionsCopy count] - 1; idx > 0; idx--) { |
+ FormSuggestion* suggestion = suggestionsCopy[idx]; |
+ if (suggestion.identifier == autofill::POPUP_ITEM_ID_CLEAR_FORM) { |
+ FormSuggestion* suggestionToMove = suggestionsCopy[idx]; |
+ [suggestionsCopy removeObjectAtIndex:idx]; |
+ [suggestionsCopy insertObject:suggestionToMove atIndex:0]; |
+ break; |
+ } |
+ } |
+ |
+ return suggestionsCopy; |
+} |
+ |
+- (void)onSuggestionsReady:(NSArray*)suggestions |
+ popupDelegate: |
+ (const base::WeakPtr<autofill::AutofillPopupDelegate>&) |
+ delegate { |
+ popupDelegate_ = delegate; |
+ mostRecentSuggestions_ = [[self processSuggestions:suggestions] copy]; |
+ if (suggestionsAvailableCompletion_) |
+ suggestionsAvailableCompletion_([mostRecentSuggestions_ count] > 0); |
+ suggestionsAvailableCompletion_ = nil; |
+} |
+ |
+#pragma mark - |
+#pragma mark FormSuggestionProvider |
+ |
+- (void)queryAutofillWithForms:(const FormDataVector&)forms |
+ field:(NSString*)fieldName |
+ type:(NSString*)type |
+ typedValue:(NSString*)typedValue |
+ webState:(web::WebState*)webState |
+ completionHandler:(SuggestionsAvailableCompletion)completion { |
+ autofill::AutofillManager* autofillManager = |
+ [self autofillManagerFromWebState:webState]; |
+ if (!autofillManager) |
+ return; |
+ |
+ // Passed to delegates; we don't use it so it's set to zero. |
+ int queryId = 0; |
+ |
+ // Find the right form and field. |
+ autofill::FormFieldData field; |
+ autofill::FormData form; |
+ GetFormAndField(&form, &field, forms, base::SysNSStringToUTF8(fieldName), |
+ base::SysNSStringToUTF8(type)); |
+ |
+ // Save the completion and go look for suggestions. |
+ suggestionsAvailableCompletion_ = [completion copy]; |
+ typedValue_ = [typedValue copy]; |
+ |
+ // Query the AutofillManager for suggestions. Results will arrive in |
+ // [AutofillController showAutofillPopup]. |
+ autofillManager->OnQueryFormFieldAutofill(queryId, form, field, gfx::RectF()); |
+} |
+ |
+- (void)checkIfSuggestionsAvailableForForm:(NSString*)formName |
+ field:(NSString*)fieldName |
+ type:(NSString*)type |
+ typedValue:(NSString*)typedValue |
+ webState:(web::WebState*)webState |
+ completionHandler: |
+ (SuggestionsAvailableCompletion)completion { |
+ web::URLVerificationTrustLevel trustLevel; |
+ const GURL pageURL(webState->GetCurrentURL(&trustLevel)); |
+ if (trustLevel != web::URLVerificationTrustLevel::kAbsolute) { |
+ DLOG(WARNING) << "Suggestions not offered on untrusted page"; |
+ completion(NO); |
+ return; |
+ } |
+ |
+ // Once the active form and field are extracted, send a query to the |
+ // AutofillManager for suggestions. |
+ __weak AutofillAgent* weakSelf = self; |
+ id completionHandler = ^(BOOL success, const FormDataVector& forms) { |
+ if (success && forms.size() == 1) { |
+ [weakSelf queryAutofillWithForms:forms |
+ field:fieldName |
+ type:type |
+ typedValue:typedValue |
+ webState:webState |
+ completionHandler:completion]; |
+ } |
+ }; |
+ |
+ // Re-extract the active form and field only. All forms with at least one |
+ // input element are considered because key/value suggestions are offered |
+ // even on short forms. |
+ [self fetchFormsWithName:base::SysNSStringToUTF16(formName) |
+ minimumRequiredFieldsCount:1 |
+ pageURL:pageURL |
+ completionHandler:completionHandler]; |
+} |
+ |
+- (void)retrieveSuggestionsForForm:(NSString*)formName |
+ field:(NSString*)fieldName |
+ type:(NSString*)type |
+ typedValue:(NSString*)typedValue |
+ webState:(web::WebState*)webState |
+ completionHandler:(SuggestionsReadyCompletion)completion { |
+ DCHECK(mostRecentSuggestions_) |
+ << "Requestor should have called " |
+ << "|checkIfSuggestionsAvailableForForm:field:type:completionHandler:| " |
+ << "and waited for the result before calling " |
+ << "|retrieveSuggestionsForForm:field:type:completionHandler:|."; |
+ completion(mostRecentSuggestions_, self); |
+} |
+ |
+- (void)didSelectSuggestion:(FormSuggestion*)suggestion |
+ forField:(NSString*)fieldName |
+ form:(NSString*)formName |
+ completionHandler:(SuggestionHandledCompletion)completion { |
+ [[UIDevice currentDevice] playInputClick]; |
+ suggestionHandledCompletion_ = [completion copy]; |
+ mostRecentSelectedIdentifier_ = suggestion.identifier; |
+ |
+ if (suggestion.identifier > 0) { |
+ pendingAutocompleteField_ = base::SysNSStringToUTF16(fieldName); |
+ if (popupDelegate_) { |
+ popupDelegate_->DidAcceptSuggestion( |
+ base::SysNSStringToUTF16(suggestion.value), suggestion.identifier, 0); |
+ } |
+ } else if (suggestion.identifier == |
+ autofill::POPUP_ITEM_ID_AUTOCOMPLETE_ENTRY) { |
+ // FormSuggestion is a simple, single value that can be filled out now. |
+ [self fillField:base::SysNSStringToUTF8(fieldName) |
+ formName:base::SysNSStringToUTF8(formName) |
+ value:base::SysNSStringToUTF16(suggestion.value)]; |
+ } else if (suggestion.identifier == autofill::POPUP_ITEM_ID_CLEAR_FORM) { |
+ [jsAutofillManager_ |
+ clearAutofilledFieldsForFormNamed:formName |
+ completionHandler:suggestionHandledCompletion_]; |
+ suggestionHandledCompletion_ = nil; |
+ } else { |
+ NOTREACHED() << "unknown identifier " << suggestion.identifier; |
+ } |
+} |
+ |
+#pragma mark - |
+#pragma mark CRWWebStateObserver |
+ |
+- (void)webStateDestroyed:(web::WebState*)webState { |
+ [self detachFromWebState]; |
+} |
+ |
+- (void)webState:(web::WebState*)webState |
+ didSubmitDocumentWithFormNamed:(const std::string&)formName |
+ userInitiated:(BOOL)userInitiated { |
+ if (!browserState_->GetPrefs()->GetBoolean(autofill::prefs::kAutofillEnabled)) |
+ return; |
+ |
+ web::URLVerificationTrustLevel trustLevel; |
+ const GURL pageURL(webState->GetCurrentURL(&trustLevel)); |
+ if (trustLevel != web::URLVerificationTrustLevel::kAbsolute) { |
+ DLOG(WARNING) << "Form submit not handled on untrusted page"; |
+ return; |
+ } |
+ |
+ __weak AutofillAgent* weakSelf = self; |
+ id completionHandler = ^(BOOL success, const FormDataVector& forms) { |
+ AutofillAgent* strongSelf = weakSelf; |
+ if (!strongSelf || !success) |
+ return; |
+ autofill::AutofillManager* autofillManager = |
+ [strongSelf autofillManagerFromWebState:webState]; |
+ if (!autofillManager || forms.empty()) |
+ return; |
+ if (forms.size() > 1) { |
+ DLOG(WARNING) << "Only one form should be extracted."; |
+ return; |
+ } |
+ [strongSelf notifyAutofillManager:autofillManager |
+ ofFormsSubmitted:forms |
+ userInitiated:userInitiated]; |
+ |
+ }; |
+ // This code is racing against the new page loading and will not get the |
+ // password form data if the page has changed. In most cases this code wins |
+ // the race. |
+ // TODO(crbug.com/418827): Fix this by passing in more data from the JS side. |
+ [self fetchFormsWithName:base::UTF8ToUTF16(formName) |
+ minimumRequiredFieldsCount:1 |
+ pageURL:pageURL |
+ completionHandler:completionHandler]; |
+} |
+ |
+- (void)webStateDidLoadPage:(web::WebState*)webState { |
+ if (!browserState_->GetPrefs()->GetBoolean( |
+ autofill::prefs::kAutofillEnabled) || |
+ !webState->ContentIsHTML()) { |
+ return; |
+ } |
+ [self processPage:webState]; |
+} |
+ |
+- (void)processPage:(web::WebState*)webState { |
+ web::URLVerificationTrustLevel trustLevel; |
+ const GURL pageURL(webState->GetCurrentURL(&trustLevel)); |
+ if (trustLevel != web::URLVerificationTrustLevel::kAbsolute) { |
+ DLOG(WARNING) << "Page load not handled on untrusted page"; |
+ return; |
+ } |
+ |
+ if (!web::UrlHasWebScheme(pageURL)) |
+ return; |
+ |
+ // This process is only done once. |
+ if ([jsAutofillManager_ hasBeenInjected]) |
+ return; |
+ |
+ popupDelegate_.reset(); |
+ suggestionsAvailableCompletion_ = nil; |
+ suggestionHandledCompletion_ = nil; |
+ mostRecentSuggestions_ = nil; |
+ typedValue_ = nil; |
+ |
+ [jsAutofillManager_ inject]; |
+ |
+ __weak AutofillAgent* weakSelf = self; |
+ id completionHandler = ^(BOOL success, const FormDataVector& forms) { |
+ AutofillAgent* strongSelf = weakSelf; |
+ if (!strongSelf || !success) |
+ return; |
+ autofill::AutofillManager* autofillManager = |
+ [strongSelf autofillManagerFromWebState:webState]; |
+ if (!autofillManager || forms.empty()) |
+ return; |
+ [strongSelf notifyAutofillManager:autofillManager ofFormsSeen:forms]; |
+ }; |
+ // The document has now been fully loaded. Scan for forms to be extracted. |
+ // Because of the cost of communicating with the server, only forms that have |
+ // enough forms to make them likely candidates for profile completion are |
+ // extracted. |
+ [self fetchFormsWithName:base::string16() |
+ minimumRequiredFieldsCount:autofill::kRequiredFieldsForPredictionRoutines |
+ pageURL:pageURL |
+ completionHandler:completionHandler]; |
+} |
+ |
+- (void)webState:(web::WebState*)webState |
+ didRegisterFormActivityWithFormNamed:(const std::string&)formName |
+ fieldName:(const std::string&)fieldName |
+ type:(const std::string&)type |
+ value:(const std::string&)value |
+ keyCode:(int)keyCode |
+ inputMissing:(BOOL)inputMissing { |
+ if (!browserState_->GetPrefs()->GetBoolean(autofill::prefs::kAutofillEnabled)) |
+ return; |
+ web::URLVerificationTrustLevel trustLevel; |
+ const GURL pageURL(webState->GetCurrentURL(&trustLevel)); |
+ if (trustLevel != web::URLVerificationTrustLevel::kAbsolute) { |
+ DLOG(WARNING) << "Form activity not handled on untrusted page"; |
+ return; |
+ } |
+ |
+ // Autofill and suggestion scripts are only injected for web URLs. |
+ if (!web::UrlHasWebScheme(pageURL) || !webState->ContentIsHTML()) |
+ return; |
+ |
+ // Returns early and reset the suggestion state if an error occurs. |
+ if (inputMissing) |
+ return; |
+ |
+ // Processing the page can be needed here if Autofill is enabled in settings |
+ // when the page is already loaded, or if the user focuses a field before the |
+ // page is fully loaded. |
+ [self processPage:webState]; |
+ |
+ // Blur not handled; we don't reset the suggestion state because if the |
+ // keyboard is about to be dismissed there's no point. If not it means the |
+ // next focus event will update the suggestion state within milliseconds, so |
+ // if we do it now a flicker will be seen. |
+ if (type.compare("blur") == 0) |
+ return; |
+ |
+ // Necessary so the strings can be used inside a block. |
+ std::string fieldNameCopy = fieldName; |
+ std::string typeCopy = type; |
+ |
+ __weak AutofillAgent* weakSelf = self; |
+ id completionHandler = ^(BOOL success, const FormDataVector& forms) { |
+ if (success && forms.size() == 1) { |
+ [weakSelf processFormActivityExtractedData:forms |
+ fieldName:fieldNameCopy |
+ type:typeCopy |
+ webState:webState]; |
+ } |
+ }; |
+ |
+ // Re-extract the active form and field only. There is no minimum field |
+ // requirement because key/value suggestions are offered event on short forms. |
+ [self fetchFormsWithName:base::UTF8ToUTF16(formName) |
+ minimumRequiredFieldsCount:1 |
+ pageURL:pageURL |
+ completionHandler:completionHandler]; |
+} |
+ |
+- (void)processFormActivityExtractedData:(const FormDataVector&)forms |
+ fieldName:(const std::string&)fieldName |
+ type:(const std::string&)type |
+ webState:(web::WebState*)webState { |
+ autofill::AutofillManager* autofillManager = |
+ [self autofillManagerFromWebState:webState]; |
+ if (!autofillManager) |
+ return; |
+ |
+ autofill::FormFieldData field; |
+ autofill::FormData form; |
+ GetFormAndField(&form, &field, forms, fieldName, type); |
+ |
+ // Tell the manager about the form activity (for metrics). |
+ if (type.compare("input") == 0 && (field.form_control_type == "text" || |
+ field.form_control_type == "password")) { |
+ autofillManager->OnTextFieldDidChange(form, field, base::TimeTicks::Now()); |
+ } |
+} |
+ |
+#pragma mark - |
+#pragma mark AutofillViewClient |
+ |
+// Complete a field named |fieldName| on the form named |formName| using |value| |
+// then move the cursor. |
+// TODO(crbug.com/661621): |dataString| ends up at fillFormField() in |
+// autofill_controller.js. fillFormField() expects an AutofillFormFieldData |
+// object, which |dataString| is not because 'form' is not a specified member of |
+// AutofillFormFieldData. fillFormField() also expects members 'max_length' and |
+// 'is_checked' to exist. |
+- (void)fillField:(const std::string&)fieldName |
+ formName:(const std::string&)formName |
+ value:(const base::string16)value { |
+ base::DictionaryValue data; |
+ data.SetString("name", fieldName); |
+ data.SetString("form", formName); |
+ data.SetString("value", value); |
+ std::string dataString; |
+ base::JSONWriter::Write(data, &dataString); |
+ |
+ DCHECK(suggestionHandledCompletion_); |
+ [jsAutofillManager_ fillActiveFormField:base::SysUTF8ToNSString(dataString) |
+ completionHandler:suggestionHandledCompletion_]; |
+ suggestionHandledCompletion_ = nil; |
+} |
+ |
+- (void)onFormDataFilled:(const autofill::FormData&)form { |
+ std::unique_ptr<base::DictionaryValue> formData(new base::DictionaryValue); |
+ formData->SetString("formName", base::UTF16ToUTF8(form.name)); |
+ // Note: Destruction of all child base::Value types is handled by the root |
+ // formData object on its own destruction. |
+ base::DictionaryValue* fieldsData = new base::DictionaryValue; |
+ |
+ const std::vector<autofill::FormFieldData>& fields = form.fields; |
+ for (const auto& fieldData : fields) { |
+ fieldsData->SetStringWithoutPathExpansion(base::UTF16ToUTF8(fieldData.name), |
+ fieldData.value); |
+ } |
+ formData->Set("fields", fieldsData); |
+ |
+ // Stringify the JSON data and send it to the UIWebView-side fillForm method. |
+ std::string dataString; |
+ base::JSONWriter::Write(*formData.get(), &dataString); |
+ |
+ // It is possible that the fill was not initiated by selecting a suggestion. |
+ // In this case we provide an empty callback. |
+ if (!suggestionHandledCompletion_) |
+ suggestionHandledCompletion_ = [^{ |
+ } copy]; |
+ [jsAutofillManager_ |
+ fillForm:base::SysUTF8ToNSString(dataString) |
+ forceFillFieldName:base::SysUTF16ToNSString(pendingAutocompleteField_) |
+ completionHandler:suggestionHandledCompletion_]; |
+ suggestionHandledCompletion_ = nil; |
+} |
+ |
+// Returns the first value from the array of possible values that has a match in |
+// the list of accepted values in the AutofillField, or nil if there is no |
+// match. If AutofillField does not specify valid values, the first value is |
+// returned from the list. |
++ (NSString*)selectFrom:(NSArray*)values for:(autofill::AutofillField*)field { |
+ if (field->option_values.empty()) |
+ return values[0]; |
+ |
+ for (NSString* value in values) { |
+ std::string valueString = base::SysNSStringToUTF8(value); |
+ for (size_t i = 0; i < field->option_values.size(); ++i) { |
+ if (base::UTF16ToUTF8(field->option_values[i]) == valueString) |
+ return value; |
+ } |
+ for (size_t i = 0; i < field->option_contents.size(); ++i) { |
+ if (base::UTF16ToUTF8(field->option_contents[i]) == valueString) |
+ return value; |
+ } |
+ } |
+ |
+ return nil; |
+} |
+ |
+- (void)renderAutofillTypePredictions: |
+ (const std::vector<autofill::FormStructure*>&)structure { |
+ base::DictionaryValue predictionData; |
+ for (autofill::FormStructure* form : structure) { |
+ // |predictionData| will take ownership below. |
+ base::DictionaryValue* formJSONData = new base::DictionaryValue; |
+ autofill::FormData formData = form->ToFormData(); |
+ for (const autofill::AutofillField* field : *form) { |
+ autofill::AutofillType type(field->Type()); |
+ if (type.IsUnknown()) |
+ continue; |
+ formJSONData->SetStringWithoutPathExpansion( |
+ base::UTF16ToUTF8(field->name), type.ToString()); |
+ } |
+ predictionData.SetWithoutPathExpansion(base::UTF16ToUTF8(formData.name), |
+ formJSONData); |
+ } |
+ std::string dataString; |
+ base::JSONWriter::Write(predictionData, &dataString); |
+ [jsAutofillManager_ fillPredictionData:base::SysUTF8ToNSString(dataString)]; |
+} |
+ |
+@end |