summaryrefslogtreecommitdiff
path: root/chromium/components/autofill/ios
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/components/autofill/ios')
-rw-r--r--chromium/components/autofill/ios/browser/BUILD.gn1
-rw-r--r--chromium/components/autofill/ios/browser/autofill_agent.mm157
-rw-r--r--chromium/components/autofill/ios/browser/autofill_agent_unittests.mm35
-rw-r--r--chromium/components/autofill/ios/browser/autofill_driver_ios.h2
-rw-r--r--chromium/components/autofill/ios/browser/autofill_driver_ios.mm3
-rw-r--r--chromium/components/autofill/ios/browser/resources/autofill_controller.js146
-rw-r--r--chromium/components/autofill/ios/fill/fill_js_unittest.mm1
-rw-r--r--chromium/components/autofill/ios/fill/form_unittest.mm40
-rw-r--r--chromium/components/autofill/ios/fill/resources/fill.js177
-rw-r--r--chromium/components/autofill/ios/fill/resources/form.js56
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);
};
/**