diff options
Diffstat (limited to 'chromium/components/autofill/ios')
10 files changed, 387 insertions, 231 deletions
diff --git a/chromium/components/autofill/ios/browser/BUILD.gn b/chromium/components/autofill/ios/browser/BUILD.gn index 53cba6a9bb1..0371187bd64 100644 --- a/chromium/components/autofill/ios/browser/BUILD.gn +++ b/chromium/components/autofill/ios/browser/BUILD.gn @@ -36,6 +36,7 @@ source_set("browser") { "//components/autofill/core/browser", "//components/autofill/core/common", "//components/prefs:prefs", + "//components/prefs/ios", "//google_apis", "//ios/web", "//ui/gfx/geometry", diff --git a/chromium/components/autofill/ios/browser/autofill_agent.mm b/chromium/components/autofill/ios/browser/autofill_agent.mm index 2ecb512ff8f..43c90590c63 100644 --- a/chromium/components/autofill/ios/browser/autofill_agent.mm +++ b/chromium/components/autofill/ios/browser/autofill_agent.mm @@ -35,6 +35,8 @@ #include "components/autofill/ios/browser/autofill_util.h" #import "components/autofill/ios/browser/form_suggestion.h" #import "components/autofill/ios/browser/js_autofill_manager.h" +#import "components/prefs/ios/pref_observer_bridge.h" +#include "components/prefs/pref_change_registrar.h" #include "components/prefs/pref_service.h" #include "ios/web/public/url_scheme_util.h" #include "ios/web/public/web_state/form_activity_params.h" @@ -63,8 +65,7 @@ typedef void (^FetchFormsCompletionHandler)(BOOL, const FormDataVector&); void GetFormAndField(autofill::FormData* form, autofill::FormFieldData* field, const FormDataVector& forms, - const std::string& fieldIdentifier, - const std::string& type) { + const std::string& fieldIdentifier) { DCHECK_GE(forms.size(), 1U); *form = forms[0]; const base::string16 fieldIdentifier16 = base::UTF8ToUTF16(fieldIdentifier); @@ -88,7 +89,7 @@ void GetFormAndField(autofill::FormData* form, } // namespace -@interface AutofillAgent ()<CRWWebStateObserver> +@interface AutofillAgent ()<CRWWebStateObserver, PrefObserverDelegate> // Notifies the autofill manager when forms are detected on a page. - (void)notifyAutofillManager:(autofill::AutofillManager*)autofillManager @@ -116,14 +117,6 @@ void GetFormAndField(autofill::FormData* form, pageURL:(const GURL&)pageURL completionHandler:(FetchFormsCompletionHandler)completionHandler; -// 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 - fieldIdentifier:(const std::string&)fieldIdentifier - type:(const std::string&)type - webState:(web::WebState*)webState; - // Returns whether Autofill is enabled by checking if Autofill is turned on and // if the current URL has a web scheme and the page content is HTML. - (BOOL)isAutofillEnabled; @@ -193,6 +186,11 @@ void GetFormAndField(autofill::FormData* form, // The autofill data that needs to be send when the |webState_| is shown. // The string is in JSON format. NSString* pendingFormJSON_; + + // Bridge to listen to pref changes. + std::unique_ptr<PrefObserverBridge> prefObserverBridge_; + // Registrar for pref changes notifications. + PrefChangeRegistrar prefChangeRegistrar_; } - (instancetype)initWithPrefService:(PrefService*)prefService @@ -206,6 +204,11 @@ void GetFormAndField(autofill::FormData* form, webStateObserverBridge_ = std::make_unique<web::WebStateObserverBridge>(self); webState_->AddObserver(webStateObserverBridge_.get()); + prefObserverBridge_ = std::make_unique<PrefObserverBridge>(self); + prefChangeRegistrar_.Init(prefService); + prefObserverBridge_->ObserveChangesForPreference( + autofill::prefs::kAutofillEnabled, &prefChangeRegistrar_); + jsAutofillManager_ = [[JsAutofillManager alloc] initWithReceiver:webState_->GetJSInjectionReceiver()]; } @@ -233,6 +236,9 @@ void GetFormAndField(autofill::FormData* form, webStateObserverBridge_.reset(); webState_ = nullptr; } + + // Do not wait for deallocation. Remove all observers here. + prefChangeRegistrar_.RemoveAll(); } #pragma mark - @@ -370,8 +376,7 @@ void GetFormAndField(autofill::FormData* form, autofill::FormFieldData field; autofill::FormData form; GetFormAndField(&form, &field, forms, - base::SysNSStringToUTF8(fieldIdentifier), - base::SysNSStringToUTF8(type)); + base::SysNSStringToUTF8(fieldIdentifier)); // Save the completion and go look for suggestions. suggestionsAvailableCompletion_ = [completion copy]; @@ -613,14 +618,13 @@ void GetFormAndField(autofill::FormData* form, if (![self isAutofillEnabled]) return; - // Returns early and reset the suggestion state if an error occurs. - if (params.input_missing) + // Return early if the page is not processed yet. + if (!pageProcessed_) 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]; + // Return early if |params| is not complete. + if (params.input_missing) + return; web::URLVerificationTrustLevel trustLevel; const GURL pageURL(webState->GetCurrentURL(&trustLevel)); @@ -628,35 +632,40 @@ void GetFormAndField(autofill::FormData* form, // If the event is a form_changed, then the event concerns the whole page and // not a particular form. The whole page need to be reparsed to find the new // forms. - if (params.type.compare("form_changed") == 0) { + if (params.type == "form_changed") { [self scanFormsInPage:webState pageURL:pageURL]; return; } - // 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 (params.type.compare("blur") == 0) + // We are only interested in 'input' events in order to notify the autofill + // manager for metrics purposes. + if (params.type != "input" || + (params.field_type != "text" && params.field_type != "password")) { return; + } - // Necessary so the strings can be used inside a block. - std::string fieldNameCopy = params.field_name; - std::string fieldIdentifierCopy = params.field_identifier; - std::string typeCopy = params.type; + // Necessary so the string can be used inside the block. + std::string fieldIdentifier = params.field_identifier; __weak AutofillAgent* weakSelf = self; id completionHandler = ^(BOOL success, const FormDataVector& forms) { - if (success && forms.size() == 1) { - [weakSelf processFormActivityExtractedData:forms - fieldName:fieldNameCopy - fieldIdentifier:fieldIdentifierCopy - type:typeCopy - webState:webState]; - } + if (!success || forms.size() != 1) + return; + + DCHECK_EQ(webState_, webState); + autofill::AutofillManager* autofillManager = + [weakSelf autofillManagerFromWebState:webState]; + if (!autofillManager) + return; + + autofill::FormFieldData field; + autofill::FormData form; + GetFormAndField(&form, &field, forms, fieldIdentifier); + autofillManager->OnTextFieldDidChange(form, field, gfx::RectF(), + base::TimeTicks::Now()); }; - // Re-extract the active form and field only. There is no minimum field + // Extract the active form and field only. There is no minimum field // requirement because key/value suggestions are offered even on short forms. [self fetchFormsFiltered:YES withName:base::UTF8ToUTF16(params.form_name) @@ -665,31 +674,20 @@ void GetFormAndField(autofill::FormData* form, completionHandler:completionHandler]; } -#pragma mark - Private methods. +#pragma mark - PrefObserverDelegate -- (void)processFormActivityExtractedData:(const FormDataVector&)forms - fieldName:(const std::string&)fieldName - fieldIdentifier:(const std::string&)fieldIdentifier - type:(const std::string&)type - webState:(web::WebState*)webState { - DCHECK_EQ(webState_, webState); - autofill::AutofillManager* autofillManager = - [self autofillManagerFromWebState:webState]; - if (!autofillManager) +- (void)onPreferenceChanged:(const std::string&)preferenceName { + if (preferenceName != autofill::prefs::kAutofillEnabled) return; - autofill::FormFieldData field; - autofill::FormData form; - GetFormAndField(&form, &field, forms, fieldIdentifier, 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, gfx::RectF(), - base::TimeTicks::Now()); - } + // Processing the page can be needed here if Autofill is enabled in settings + // when the page is already loaded. + if ([self isAutofillEnabled]) + [self processPage:webState_]; } +#pragma mark - Private methods. + - (BOOL)isAutofillEnabled { if (!prefService_->GetBoolean(autofill::prefs::kAutofillEnabled)) return NO; @@ -728,32 +726,29 @@ void GetFormAndField(autofill::FormData* form, } - (void)onFormDataFilled:(const autofill::FormData&)form { - std::unique_ptr<base::DictionaryValue> JSONForm(new base::DictionaryValue); - JSONForm->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. - auto JSONFields = std::make_unique<base::DictionaryValue>(); - - const std::vector<autofill::FormFieldData>& autofillFields = form.fields; - for (const auto& autofillField : autofillFields) { - if (JSONFields->HasKey(base::UTF16ToUTF8(autofillField.id)) && - autofillField.value.empty()) + base::Value autofillData(base::Value::Type::DICTIONARY); + autofillData.SetKey("formName", base::Value(base::UTF16ToUTF8(form.name))); + + base::Value fieldsData(base::Value::Type::DICTIONARY); + for (const auto& field : form.fields) { + // Skip empty fields and those that are not autofilled. + if (field.value.empty() || !field.is_autofilled) continue; - JSONFields->SetKey(base::UTF16ToUTF8(autofillField.id), - base::Value(autofillField.value)); + + fieldsData.SetKey(base::UTF16ToUTF8(field.id), base::Value(field.value)); } - JSONForm->Set("fields", std::move(JSONFields)); + autofillData.SetKey("fields", std::move(fieldsData)); - // Stringify the JSON data and send it to the UIWebView-side fillForm method. std::string JSONString; - base::JSONWriter::Write(*JSONForm.get(), &JSONString); - NSString* nsJSONString = base::SysUTF8ToNSString(JSONString); + base::JSONWriter::Write(autofillData, &JSONString); + // Store the form data when WebState is not visible, to send it as soon as it + // becomes visible again, e.g., when the CVC unmask prompt is showing. if (!webState_->IsVisible()) { - pendingFormJSON_ = nsJSONString; + pendingFormJSON_ = base::SysUTF8ToNSString(JSONString); return; } - [self sendDataToWebState:nsJSONString]; + [self sendDataToWebState:base::SysUTF8ToNSString(JSONString)]; } - (void)sendDataToWebState:(NSString*)JSONData { @@ -800,16 +795,16 @@ void GetFormAndField(autofill::FormData* form, return; } - base::DictionaryValue predictionData; + base::Value predictionData(base::Value::Type::DICTIONARY); for (const auto& form : forms) { - auto formJSONData = std::make_unique<base::DictionaryValue>(); + base::Value fieldData(base::Value::Type::DICTIONARY); DCHECK(form.fields.size() == form.data.fields.size()); for (size_t i = 0; i < form.fields.size(); i++) { - formJSONData->SetKey(base::UTF16ToUTF8(form.data.fields[i].id), - base::Value(form.fields[i].overall_type)); + fieldData.SetKey(base::UTF16ToUTF8(form.data.fields[i].id), + base::Value(form.fields[i].overall_type)); } - predictionData.SetWithoutPathExpansion(base::UTF16ToUTF8(form.data.name), - std::move(formJSONData)); + predictionData.SetKey(base::UTF16ToUTF8(form.data.name), + std::move(fieldData)); } std::string dataString; base::JSONWriter::Write(predictionData, &dataString); diff --git a/chromium/components/autofill/ios/browser/autofill_agent_unittests.mm b/chromium/components/autofill/ios/browser/autofill_agent_unittests.mm index f4cda3f135c..05b985dc457 100644 --- a/chromium/components/autofill/ios/browser/autofill_agent_unittests.mm +++ b/chromium/components/autofill/ios/browser/autofill_agent_unittests.mm @@ -41,18 +41,28 @@ class AutofillAgentTests : public PlatformTest { webState:&test_web_state_]; } + void TearDown() override { + [autofill_agent_ detachFromWebState]; + + PlatformTest::TearDown(); + } + web::TestWebState test_web_state_; - AutofillAgent* autofill_agent_; std::unique_ptr<PrefService> prefs_; + AutofillAgent* autofill_agent_; id mock_js_injection_receiver_; DISALLOW_COPY_AND_ASSIGN(AutofillAgentTests); }; +// Tests that form's name and fields' identifiers, values, and whether they are +// autofilled are sent to the JS. Fields with empty values and those that are +// not autofilled are skipped. TEST_F(AutofillAgentTests, OnFormDataFilledTest) { autofill::FormData form; form.origin = GURL("https://myform.com"); form.action = GURL("https://myform.com/submit"); + form.name = base::ASCIIToUTF16("CC form"); autofill::FormFieldData field; field.form_control_type = "text"; @@ -60,23 +70,31 @@ TEST_F(AutofillAgentTests, OnFormDataFilledTest) { field.name = base::ASCIIToUTF16("number"); field.id = base::ASCIIToUTF16("number"); field.value = base::ASCIIToUTF16("number_value"); + field.is_autofilled = true; form.fields.push_back(field); field.label = base::ASCIIToUTF16("Name on Card"); field.name = base::ASCIIToUTF16("name"); field.id = base::ASCIIToUTF16("name"); field.value = base::ASCIIToUTF16("name_value"); + field.is_autofilled = true; + form.fields.push_back(field); + field.label = base::ASCIIToUTF16("Expiry Month"); + field.name = base::ASCIIToUTF16("expiry_month"); + field.id = base::ASCIIToUTF16("expiry_month"); + field.value = base::ASCIIToUTF16("01"); + field.is_autofilled = false; form.fields.push_back(field); field.label = base::ASCIIToUTF16("Unknown field"); field.name = base::ASCIIToUTF16("unknown"); field.id = base::ASCIIToUTF16("unknown"); field.value = base::ASCIIToUTF16(""); + field.is_autofilled = true; form.fields.push_back(field); // Fields are in alphabetical order. [[mock_js_injection_receiver_ expect] executeJavaScript: @"__gCrWeb.autofill.fillForm({\"fields\":{\"name\":\"name_value\"," - @"\"number\":\"number_value\",\"unknown\":\"\"},\"formName\":\"\"}, " - @"\"\");" + @"\"number\":\"number_value\"},\"formName\":\"CC form\"}, \"\");" completionHandler:[OCMArg any]]; [autofill_agent_ onFormDataFilled:form]; test_web_state_.WasShown(); @@ -84,33 +102,32 @@ TEST_F(AutofillAgentTests, OnFormDataFilledTest) { EXPECT_OCMOCK_VERIFY(mock_js_injection_receiver_); } +// Tests that in the case of conflict in fields' identifiers, the last seen +// value of a given field is used. TEST_F(AutofillAgentTests, OnFormDataFilledWithNameCollisionTest) { autofill::FormData form; form.origin = GURL("https://myform.com"); form.action = GURL("https://myform.com/submit"); autofill::FormFieldData field; - // Check that in case of conflict, the last value of a given field is used. field.form_control_type = "text"; field.label = base::ASCIIToUTF16("State"); field.name = base::ASCIIToUTF16("region"); field.id = base::ASCIIToUTF16("region"); field.value = base::ASCIIToUTF16("California"); - form.fields.push_back(field); - field.label = base::ASCIIToUTF16("Province"); - field.name = base::ASCIIToUTF16("region"); - field.id = base::ASCIIToUTF16("region"); - field.value = base::ASCIIToUTF16(""); + field.is_autofilled = true; form.fields.push_back(field); field.label = base::ASCIIToUTF16("Other field"); field.name = base::ASCIIToUTF16("field1"); field.id = base::ASCIIToUTF16("field1"); field.value = base::ASCIIToUTF16("value 1"); + field.is_autofilled = true; form.fields.push_back(field); field.label = base::ASCIIToUTF16("Other field"); field.name = base::ASCIIToUTF16("field1"); field.id = base::ASCIIToUTF16("field1"); field.value = base::ASCIIToUTF16("value 2"); + field.is_autofilled = true; form.fields.push_back(field); // Fields are in alphabetical order. [[mock_js_injection_receiver_ expect] diff --git a/chromium/components/autofill/ios/browser/autofill_driver_ios.h b/chromium/components/autofill/ios/browser/autofill_driver_ios.h index febf00c0a70..a39cbc34c76 100644 --- a/chromium/components/autofill/ios/browser/autofill_driver_ios.h +++ b/chromium/components/autofill/ios/browser/autofill_driver_ios.h @@ -46,7 +46,7 @@ class AutofillDriverIOS : public AutofillDriver, const std::vector<autofill::FormStructure*>& forms) override; void SendAutofillTypePredictionsToRenderer( const std::vector<FormStructure*>& forms) override; - void RendererShouldClearFilledForm() override; + void RendererShouldClearFilledSection() override; void RendererShouldClearPreviewedForm() override; void RendererShouldAcceptDataListSuggestion( const base::string16& value) override; diff --git a/chromium/components/autofill/ios/browser/autofill_driver_ios.mm b/chromium/components/autofill/ios/browser/autofill_driver_ios.mm index 6d1e612f5f1..d94e80e6bd8 100644 --- a/chromium/components/autofill/ios/browser/autofill_driver_ios.mm +++ b/chromium/components/autofill/ios/browser/autofill_driver_ios.mm @@ -91,8 +91,7 @@ void AutofillDriverIOS::DidInteractWithCreditCardForm() { } } -void AutofillDriverIOS::RendererShouldClearFilledForm() { -} +void AutofillDriverIOS::RendererShouldClearFilledSection() {} void AutofillDriverIOS::RendererShouldClearPreviewedForm() { } diff --git a/chromium/components/autofill/ios/browser/resources/autofill_controller.js b/chromium/components/autofill/ios/browser/resources/autofill_controller.js index 326d8c8c6e0..5bb6f60d0ea 100644 --- a/chromium/components/autofill/ios/browser/resources/autofill_controller.js +++ b/chromium/components/autofill/ios/browser/resources/autofill_controller.js @@ -17,6 +17,15 @@ */ goog.provide('__crWeb.autofill'); +/** + * The autofill data for a form. + * @typedef {{ + * formName: string, + * fields: !Object<string, string>, + * }} + */ +var FormData; + /* Beginning of anonymous object. */ (function() { @@ -223,13 +232,22 @@ __gCrWeb.autofill['fillActiveFormField'] = function(data) { __gCrWeb.autofill.fillFormField(data, activeElement); }; +// Remove Autofill styling when control element is edited by the user. +function controlElementInputListener_(evt) { + if (evt.isTrusted) { + evt.target.removeAttribute('chrome-autofilled'); + evt.target.isAutofilled = false; + evt.target.removeEventListener('input', controlElementInputListener_); + } +}; + /** * Fills a number of fields in the same named form for full-form Autofill. * Applies Autofill CSS (i.e. yellow background) to filled elements. * Only empty fields will be filled, except that field named * |forceFillFieldName| will always be filled even if non-empty. * - * @param {Object} data Dictionary of data to fill in. + * @param {!FormData} data Autofill data to fill in. * @param {string} forceFillFieldIdentifier Identified field will always be * filled even if non-empty. May be null. */ @@ -246,67 +264,53 @@ __gCrWeb.autofill['fillForm'] = function(data, forceFillFieldIdentifier) { __gCrWeb.autofill.styleInjected = true; } - // Remove Autofill styling when control element is edited by the user. - var controlElementInputListener = function(evt) { - if (evt.isTrusted) { - evt.target.removeAttribute('chrome-autofilled'); - evt.target.isAutofilled = false; - evt.target.removeEventListener('input', controlElementInputListener); - } - }; - var form = __gCrWeb.form.getFormElementFromIdentifier(data.formName); - var controlElements = []; - if (form) { - controlElements = __gCrWeb.form.getFormControlElements(form); - } else { - var fieldsets = []; - controlElements = - getUnownedAutofillableFormFieldElements_(document.all, fieldsets); - } - var delay = 0; - for (var i = 0; i < controlElements.length; ++i) { + var controlElements = form ? + __gCrWeb.form.getFormControlElements(form) : + getUnownedAutofillableFormFieldElements_(document.all, /*fieldsets=*/[]); + + for (var i = 0, delay = 0; i < controlElements.length; + ++i, delay += __gCrWeb.autofill.delayBetweenFieldFillingMs) { var element = controlElements[i]; - if (!__gCrWeb.fill.isAutofillableElement(element)) { + if (!__gCrWeb.fill.isAutofillableElement(element)) continue; - } + + // TODO(crbug.com/836013): Investigate autofilling checkable elements. + if (__gCrWeb.fill.isCheckableElement(element)) + continue; + + // Skip fields if autofill data is missing. var fieldIdentifier = __gCrWeb.form.getFieldIdentifier(element); + var value = data.fields[fieldIdentifier]; + if (!value) + continue; - // Skip non-empty fields unless this is the forceFillFieldName or it's a - // 'select-one' element. 'select-one' elements are always autofilled even - // if non-empty; see AutofillManager::FillOrPreviewDataModelForm(). + // Skip non-empty fields unless: + // a) The element's identifier matches |forceFillFieldIdentifier|; or + // b) The element is a 'select-one' element. 'select-one' elements are + // always autofilled; see AutofillManager::FillOrPreviewDataModelForm(). + // c) The "value" or "placeholder" attributes match the value, if any; or if (element.value && !__gCrWeb.autofill.sanitizedFieldIsEmpty(element.value) && + fieldIdentifier !== forceFillFieldIdentifier && !__gCrWeb.fill.isSelectElement(element) && - fieldIdentifier !== forceFillFieldIdentifier) { + !((element.hasAttribute('value') && + element.getAttribute('value') == element.value) || + (element.hasAttribute('placeholder') && + element.getAttribute('placeholder').toLowerCase() == + element.value.toLowerCase()))) { continue; } - // Don't fill field if source value is empty or missing. - var value = data.fields[fieldIdentifier]; - if (!value) continue; - - if (__gCrWeb.fill.isTextInput(element) || - __gCrWeb.fill.isTextAreaElement(element) || - __gCrWeb.fill.isSelectElement(element)) { - (function(_element, _value, _delay) { - window.setTimeout(function() { - __gCrWeb.fill.setInputElementValue( - _value, _element, function(changed) { - if (!changed) - return; - _element.setAttribute('chrome-autofilled', ''); - _element.isAutofilled = true; - _element.addEventListener('input', controlElementInputListener); - }); - }, _delay); - })(element, value, delay); - delay = delay + __gCrWeb.autofill.delayBetweenFieldFillingMs; - } else if (__gCrWeb.fill.isCheckableElement(element)) { - // TODO(bondd): Handle __gCrWeb.fill.isCheckableElement(element) == - // true. |is_checked| is not currently passed in by the caller. - } - + (function(_element, _value, _delay) { + window.setTimeout(function() { + __gCrWeb.fill.setInputElementValue(_value, _element, function() { + _element.setAttribute('chrome-autofilled', ''); + _element.isAutofilled = true; + _element.addEventListener('input', controlElementInputListener_); + }); + }, _delay); + })(element, value, delay); } if (form) { @@ -325,31 +329,29 @@ __gCrWeb.autofill['fillForm'] = function(data, forceFillFieldIdentifier) { } }; +// TODO(crbug.com/816941): Clear should only clear the current section and not +// the whole form. /** - * Clear autofilled fields of the specified form. Fields that are not currently - * autofilled are not modified. + * Clear autofilled fields of the specified form section. Fields that are not + * currently autofilled are not modified. * Field contents are cleared, and Autofill flag and styling are removed. * 'change' events are sent for fields whose contents changed. - * Based on FormCache::ClearFormWithElement(). + * Based on FormCache::ClearSectionWithElement(). * * @param {string} formName Identifier for form element (from * getFormIdentifier). */ __gCrWeb.autofill['clearAutofilledFields'] = function(formName) { var form = __gCrWeb.form.getFormElementFromIdentifier(formName); - var controlElements = []; - if (form) { - controlElements = __gCrWeb.form.getFormControlElements(form); - } else { - var fieldsets = []; - controlElements = - getUnownedAutofillableFormFieldElements_(document.all, fieldsets); - } - var delay = 0; + var controlElements = form ? + __gCrWeb.form.getFormControlElements(form) : + getUnownedAutofillableFormFieldElements_(document.all, /*fieldsets=*/[]); - for (var i = 0; i < controlElements.length; ++i) { + for (var i = 0, delay = 0; i < controlElements.length; + ++i, delay += __gCrWeb.autofill.delayBetweenFieldFillingMs) { var element = controlElements[i]; - if (!element.isAutofilled || element.disabled) continue; + if (!element.isAutofilled || element.disabled) + continue; var value = null; if (__gCrWeb.fill.isTextInput(element) || @@ -360,8 +362,7 @@ __gCrWeb.autofill['clearAutofilledFields'] = function(formName) { // TODO(bondd): Store initial values and reset to the correct one here. value = element.options[0].value; } else if (__gCrWeb.fill.isCheckableElement(element)) { - // TODO(bondd): Handle checkable elements. They aren't properly supported - // by iOS Autofill yet. + // TODO(crbug.com/836013): Investigate autofilling checkable elements. } if (value !== null) { (function(_element, _value, _delay) { @@ -371,11 +372,10 @@ __gCrWeb.autofill['clearAutofilledFields'] = function(formName) { _element.removeAttribute('chrome-autofilled'); _element.isAutofilled = false; _element.removeEventListener( - 'input', controlElementInputListener); + 'input', controlElementInputListener_); }); }, _delay); })(element, value, delay); - delay = delay + __gCrWeb.autofill.delayBetweenFieldFillingMs; } } }; @@ -415,8 +415,8 @@ __gCrWeb.autofill.extractNewForms = function( /** @type {HTMLCollection} */ var webForms = document.forms; - var extractMask = __gCrWeb.fill.EXTRACT_MASK_VALUE | - __gCrWeb.fill.EXTRACT_MASK_OPTIONS; + var extractMask = + __gCrWeb.fill.EXTRACT_MASK_VALUE | __gCrWeb.fill.EXTRACT_MASK_OPTIONS; var numFieldsSeen = 0; for (var formIndex = 0; formIndex < webForms.length; ++formIndex) { /** @type {HTMLFormElement} */ @@ -703,7 +703,7 @@ __gCrWeb.autofill['sanitizedFieldIsEmpty'] = function(value) { // Some sites enter values such as ____-____-____-____ or (___)-___-____ in // their fields. Check if the field value is empty after the removal of the // formatting characters. - return __gCrWeb.common.trim(value.replace(/[-_()/|]/g,'')) == ''; - }; + return __gCrWeb.common.trim(value.replace(/[-_()/|]/g, '')) == ''; +}; }()); // End of anonymous object diff --git a/chromium/components/autofill/ios/fill/fill_js_unittest.mm b/chromium/components/autofill/ios/fill/fill_js_unittest.mm index 65b0b16331c..c3d22a6f313 100644 --- a/chromium/components/autofill/ios/fill/fill_js_unittest.mm +++ b/chromium/components/autofill/ios/fill/fill_js_unittest.mm @@ -58,7 +58,6 @@ TEST_F(FillJsTest, GetCanonicalActionForForm) { LoadHtmlAndInject(html); id result = ExecuteJavaScript( @"__gCrWeb.fill.getCanonicalActionForForm(document.body.children[0])"); - // [NSThread sleepForTimeInterval:10000]; NSString* base_url = base::SysUTF8ToNSString(BaseUrl()); NSString* expected_action = [data.expected_action stringByReplacingOccurrencesOfString:@"baseurl/" diff --git a/chromium/components/autofill/ios/fill/form_unittest.mm b/chromium/components/autofill/ios/fill/form_unittest.mm index a072e31249b..4bf728d5108 100644 --- a/chromium/components/autofill/ios/fill/form_unittest.mm +++ b/chromium/components/autofill/ios/fill/form_unittest.mm @@ -30,18 +30,42 @@ class FormJsTest : public web::WebJsTest<web::WebTestWithWebState> { std::make_unique<FormTestClient>()) {} }; -// Tests that keyup event correctly delivered to WebStateObserver. -TEST_F(FormJsTest, KeyUpEvent) { +// Tests that keyup event correctly delivered to WebStateObserver if the element +// is focused. +TEST_F(FormJsTest, KeyUpEventFocused) { web::TestWebStateObserver observer(web_state()); - LoadHtml(@"<p></p>"); + LoadHtml(@"<p><input id='test'/></p>"); ASSERT_FALSE(observer.form_activity_info()); - ExecuteJavaScript(@"document.dispatchEvent(new KeyboardEvent('keyup'));"); + ExecuteJavaScript( + @"var e = document.getElementById('test');" + "e.focus();" + "var ev = new KeyboardEvent('keyup', {bubbles:true});" + "e.dispatchEvent(ev);"); + web::TestWebStateObserver* block_observer = &observer; + WaitForCondition(^bool { + return block_observer->form_activity_info() != nullptr; + }); web::TestFormActivityInfo* info = observer.form_activity_info(); ASSERT_TRUE(info); EXPECT_EQ("keyup", info->form_activity.type); EXPECT_FALSE(info->form_activity.input_missing); } +// Tests that keyup event is not delivered to WebStateObserver if the element is +// not focused. +TEST_F(FormJsTest, KeyUpEventNotFocused) { + web::TestWebStateObserver observer(web_state()); + LoadHtml(@"<p><input id='test'/></p>"); + ASSERT_FALSE(observer.form_activity_info()); + ExecuteJavaScript( + @"var e = document.getElementById('test');" + "var ev = new KeyboardEvent('keyup', {bubbles:true});" + "e.dispatchEvent(ev);"); + WaitForBackgroundTasks(); + web::TestFormActivityInfo* info = observer.form_activity_info(); + ASSERT_FALSE(info); +} + // Tests that focus event correctly delivered to WebStateObserver. TEST_F(FormJsTest, FocusMainFrame) { web::TestWebStateObserver observer(web_state()); @@ -52,6 +76,10 @@ TEST_F(FormJsTest, FocusMainFrame) { "</form>"); ASSERT_FALSE(observer.form_activity_info()); ExecuteJavaScript(@"document.getElementById('id1').focus();"); + web::TestWebStateObserver* block_observer = &observer; + WaitForCondition(^bool { + return block_observer->form_activity_info() != nullptr; + }); web::TestFormActivityInfo* info = observer.form_activity_info(); ASSERT_TRUE(info); EXPECT_EQ("focus", info->form_activity.type); @@ -88,6 +116,10 @@ TEST_F(FormJsTest, FocusSameOriginIFrame) { ExecuteJavaScript( @"document.getElementById('frame1').contentDocument.getElementById('id1')" @".focus()"); + web::TestWebStateObserver* block_observer = &observer; + WaitForCondition(^bool { + return block_observer->form_activity_info() != nullptr; + }); web::TestFormActivityInfo* info = observer.form_activity_info(); ASSERT_TRUE(info); EXPECT_EQ("focus", info->form_activity.type); diff --git a/chromium/components/autofill/ios/fill/resources/fill.js b/chromium/components/autofill/ios/fill/resources/fill.js index 28ee9065532..0baae455648 100644 --- a/chromium/components/autofill/ios/fill/resources/fill.js +++ b/chromium/components/autofill/ios/fill/resources/fill.js @@ -191,8 +191,8 @@ function setInputElementAngularValue_(value, input) { } /** - * Sets the value of an input and dispatches a change event if - * |shouldSendChangeEvent|. + * Sets the value of an input, dispatches the events on the changed element and + * call |callback| if it is defined. * * It is based on the logic in * @@ -207,43 +207,119 @@ function setInputElementAngularValue_(value, input) { * * @param {string} value The value the input element will be set. * @param {Element} input The input element of which the value is set. - * @param {function(boolean)=} callback Callback function with a boolean + * @param {function()=} callback Callback function with a boolean * argument that indicates if the input element's value was changed. */ __gCrWeb.fill.setInputElementValue = function( value, input, callback = undefined) { - if (!input) { + if (!input) return; + + var activeElement = document.activeElement; + if (input != activeElement) { + __gCrWeb.fill.createAndDispatchHTMLEvent( + activeElement, value, 'blur', true, false); + __gCrWeb.fill.createAndDispatchHTMLEvent( + input, value, 'focus', true, false); } - var changed = false; - if (input.type === 'checkbox' || input.type === 'radio') { - changed = input.checked !== value; - input.checked = value; - } else if (input.type === 'select-one') { - changed = input.value !== value; - input.value = value; - } else { + + setInputElementValue_(value, input); + if (callback) + callback(); + + if (input != activeElement) { + __gCrWeb.fill.createAndDispatchHTMLEvent(input, value, 'blur', true, false); + __gCrWeb.fill.createAndDispatchHTMLEvent( + activeElement, value, 'focus', true, false); + } +}; + +/** + * Internal function to set the element value. + * + * @param {string} value The value the input element will be set. + * @param {Element} input The input element of which the value is set. + */ +function setInputElementValue_(value, input) { + var propertyName = (input.type === 'checkbox' || input.type === 'radio') ? + 'checked' : + 'value'; + if (input.type !== 'select-one' && input.type !== 'checkbox' && + input.type !== 'radio') { // In HTMLInputElement.cpp there is a check on canSetValue(value), which // returns false only for file input. As file input is not relevant for // autofill and this method is only used for autofill for now, there is no // such check in this implementation. - var sanitizedValue = - __gCrWeb.fill.sanitizeValueForInputElement(value, input); - changed = sanitizedValue !== input.value; - input.value = sanitizedValue; + value = __gCrWeb.fill.sanitizeValueForInputElement(value, input); } + + // Return early if the value hasn't changed. + if (input[propertyName] == value) + return; + + // When the user inputs a value in an HTMLInput field, the property setter is + // not called. The different frameworks often call it explicitly when + // receiving the input event. + // This is probably due to the sync between the HTML object and the DOM + // object. + // The sequence of event is: User input -> input event -> setter. + // When the property is set programmatically (input.value = 'foo'), the setter + // is called immediately (then probably called again on the input event) + // JS input -> setter. + // The only way to emulate the user behavior is to override the property + // The getter will return the new value to emulate the fact the the HTML + // value was updated without calling the setter. + // The setter simply forwards the set to the older property descriptor. + // Once the setter has been called, just forward get and set calls. + + var oldPropertyDescriptor = /** @type {!Object} */ ( + Object.getOwnPropertyDescriptor(input, propertyName)); + var overrideProperty = + oldPropertyDescriptor && oldPropertyDescriptor.configurable; + var setterCalled = false; + + if (overrideProperty) { + var newProperty = { + get: function() { + if (setterCalled && oldPropertyDescriptor.get) { + return oldPropertyDescriptor.get.call(input); + } + // Simulate the fact that the HTML value has been set but not yet the + // property. + return value + ''; + }, + configurable: true + }; + if (oldPropertyDescriptor.set) { + newProperty.set = function(e) { + setterCalled = true; + oldPropertyDescriptor.set.call(input, value); + } + } + Object.defineProperty(input, propertyName, newProperty); + } else { + setterCalled = true; + input[propertyName] = value; + } + if (window['angular']) { // The page uses the AngularJS framework. Update the angular value before // sending events. setInputElementAngularValue_(value, input); } - if (changed) { - __gCrWeb.fill.notifyElementValueChanged(input); - } - if (callback) { - callback(changed); + __gCrWeb.fill.notifyElementValueChanged(input, value); + + if (overrideProperty) { + Object.defineProperty(input, propertyName, oldPropertyDescriptor); + if (!setterCalled && input[propertyName] != value) { + // The setter was never called. This may be intentional (the framework + // ignored the input event) or not (the event did not conform to what + // framework expected). The whole function will likely fail, but try to + // set the value directly as a last try. + input[propertyName] = value; + } } -}; +} /** * Returns a sanitized value of proposedValue for a given input element type. @@ -384,15 +460,23 @@ __gCrWeb.fill.sanitizeValueForNumberInputType = function(proposedValue) { /** * Creates and sends notification that element has changed. * - * Most handlers react to 'change' or 'input' event, so sent both. + * Send events that 'mimic' the user typing in a field. + * 'input' event is often use in case of a text field, and 'change'event is + * more often used in case of selects. * * @param {Element} element The element that changed. */ -__gCrWeb.fill.notifyElementValueChanged = function(element) { - __gCrWeb.fill.createAndDispatchHTMLEvent(element, 'keydown', true, false); - __gCrWeb.fill.createAndDispatchHTMLEvent(element, 'change', true, false); - __gCrWeb.fill.createAndDispatchHTMLEvent(element, 'input', true, false); - __gCrWeb.fill.createAndDispatchHTMLEvent(element, 'keyup', true, false); +__gCrWeb.fill.notifyElementValueChanged = function(element, value) { + __gCrWeb.fill.createAndDispatchHTMLEvent( + element, value, 'keydown', true, false); + __gCrWeb.fill.createAndDispatchHTMLEvent( + element, value, 'keypress', true, false); + __gCrWeb.fill.createAndDispatchHTMLEvent( + element, value, 'input', true, false); + __gCrWeb.fill.createAndDispatchHTMLEvent( + element, value, 'keyup', true, false); + __gCrWeb.fill.createAndDispatchHTMLEvent( + element, value, 'change', true, false); }; /** @@ -406,30 +490,25 @@ __gCrWeb.fill.notifyElementValueChanged = function(element) { * canceled. */ __gCrWeb.fill.createAndDispatchHTMLEvent = function( - element, type, bubbles, cancelable) { - var changeEvent = - /** @type {!Event} */ (element.ownerDocument.createEvent('HTMLEvents')); - changeEvent.initEvent(type, bubbles, cancelable); - // Some frameworks will use the data field to update their cache value. - changeEvent.data = element.value; - - // Adding a |simulated| flag on the event will force the React framework to - // update the backend store. - changeEvent.simulated = true; - - element.dispatchEvent(changeEvent); + element, value, type, bubbles, cancelable) { + var event = + new Event(type, {bubbles: bubbles, cancelable: cancelable, data: value}); + if (type == 'input') { + event.inputType = 'insertText'; + } + element.dispatchEvent(event); }; - /** - * Returns a canonical action for |formElement|. It works the same as upstream - * function GetCanonicalActionForForm. - * @param {HTMLFormElement} formElement - * @return {string} Canonical action. - */ +/** + * Returns a canonical action for |formElement|. It works the same as upstream + * function GetCanonicalActionForForm. + * @param {HTMLFormElement} formElement + * @return {string} Canonical action. + */ __gCrWeb.fill.getCanonicalActionForForm = function(formElement) { - var rawAction = formElement.getAttribute('action') || ""; - var absoluteUrl = __gCrWeb.common.absoluteURL( - formElement.ownerDocument, rawAction); + var rawAction = formElement.getAttribute('action') || ''; + var absoluteUrl = + __gCrWeb.common.absoluteURL(formElement.ownerDocument, rawAction); return __gCrWeb.common.removeQueryAndReferenceFromURL(absoluteUrl); }; diff --git a/chromium/components/autofill/ios/fill/resources/form.js b/chromium/components/autofill/ios/fill/resources/form.js index cb41ac7db49..e3e717a1a8e 100644 --- a/chromium/components/autofill/ios/fill/resources/form.js +++ b/chromium/components/autofill/ios/fill/resources/form.js @@ -59,6 +59,16 @@ __gCrWeb.form.formMutationObserver = null; __gCrWeb.form.formMutationMessageToSend = null; /** + * A message scheduled to be sent to host on the next runloop. + */ +__gCrWeb.form.messageToSend = null; + +/** + * The last HTML element that was focused by the user. + */ +__gCrWeb.form.lastFocusedElement = null; + +/** * Based on Element::isFormControlElement() (WebKit) * @param {Element} element A DOM element. * @return {boolean} true if the |element| is a form control element. @@ -287,31 +297,55 @@ __gCrWeb.form.getFormElementFromIdentifier = function(name) { return null; }; - +/** + * Schedule |mesg| to be sent on next runloop. + * If called multiple times on the same runloop, only the last message is really + * sent. + */ +var sendMessageOnNextLoop_ = function(mesg) { + if (!__gCrWeb.form.messageToSend) { + setTimeout(function() { + __gCrWeb.message.invokeOnHost(__gCrWeb.form.messageToSend); + __gCrWeb.form.messageToSend = null; + }, 0); + } + __gCrWeb.form.messageToSend = mesg; +} /** - * Focus and input events for form elements are messaged to the main - * application for broadcast to WebStateObservers. + * Focus, input, change, keyup and blur events for form elements (form and input + * elements) are messaged to the main application for broadcast to + * WebStateObservers. + * Events will be included in a message to be sent in a future runloop (without + * delay). If an event is already scheduled to be sent, it is replaced by |evt|. + * Notably, 'blur' event will not be transmitted to the main application if they + * are triggered by the focus of another element as the 'focus' event will + * replace it. + * Only the events targetting the active element (or the previously active in + * case of 'blur') are sent to the main application. * This is done with a single event handler for each type being added to the * main document element which checks the source element of the event; this * is much easier to manage than adding handlers to individual elements. * @private */ var formActivity_ = function(evt) { - var srcElement = evt.srcElement; - var value = srcElement.value || ''; - var fieldType = srcElement.type || ''; - + var target = evt.target; + var value = target.value || ''; + var fieldType = target.type || ''; + if (evt.type != 'blur') { + __gCrWeb.form.lastFocusedElement = document.activeElement; + } + if (target != __gCrWeb.form.lastFocusedElement) return; var msg = { 'command': 'form.activity', - 'formName': __gCrWeb.form.getFormIdentifier(evt.srcElement.form), - 'fieldName': __gCrWeb.form.getFieldName(srcElement), - 'fieldIdentifier': __gCrWeb.form.getFieldIdentifier(srcElement), + 'formName': __gCrWeb.form.getFormIdentifier(evt.target.form), + 'fieldName': __gCrWeb.form.getFieldName(target), + 'fieldIdentifier': __gCrWeb.form.getFieldIdentifier(target), 'fieldType': fieldType, 'type': evt.type, 'value': value }; - __gCrWeb.message.invokeOnHost(msg); + sendMessageOnNextLoop_(msg); }; /** |