Teach form_parser use server hints
This CL extends the browser-side FormData parser in password manager to
use server predictions to determine roles of input fields.
This support is incomplete and does not handle well partial predictions
yet, e.g., if only the USERNAME is predicted. It also does not handle
the "NOT_PASSWORD" prediction. Those will get addressed in separate CLs.
Bug: 845426
Change-Id: If8e444fb865c2c8358b122fed875386a9aa05ec8
Reviewed-on: https://chromium-review.googlesource.com/1087066
Commit-Queue: Vaclav Brozek <[email protected]>
Reviewed-by: Vadym Doroshenko <[email protected]>
Cr-Commit-Position: refs/heads/master@{#566388}
diff --git a/components/password_manager/core/browser/form_parsing/form_parser.cc b/components/password_manager/core/browser/form_parsing/form_parser.cc
index 09a1baa..e96971b 100644
--- a/components/password_manager/core/browser/form_parsing/form_parser.cc
+++ b/components/password_manager/core/browser/form_parsing/form_parser.cc
@@ -155,6 +155,51 @@
return false;
}
+// Returns the first element of |fields| which has the specified
+// |unique_renderer_id|, or null if there is no such element.
+const FormFieldData* FindFieldWithUniqueRendererId(
+ const std::vector<const FormFieldData*>& fields,
+ uint32_t unique_renderer_id) {
+ for (const FormFieldData* field : fields) {
+ if (field->unique_renderer_id == unique_renderer_id)
+ return field;
+ }
+ return nullptr;
+}
+
+// Tries to parse |fields| based on server |predictions|.
+std::unique_ptr<ParseResult> ParseUsingPredictions(
+ const std::vector<const FormFieldData*>& fields,
+ const FormPredictions& predictions) {
+ auto result = std::make_unique<ParseResult>();
+ // Note: The code does not check whether there is at most 1 username, 1
+ // current password and at most 2 new passwords. It is assumed that server
+ // side predictions are sane.
+ for (const auto& prediction : predictions) {
+ switch (DeriveFromServerFieldType(prediction.second.type)) {
+ case CredentialFieldType::kUsername:
+ result->username_field =
+ FindFieldWithUniqueRendererId(fields, prediction.first);
+ break;
+ case CredentialFieldType::kCurrentPassword:
+ result->password_field =
+ FindFieldWithUniqueRendererId(fields, prediction.first);
+ break;
+ case CredentialFieldType::kNewPassword:
+ result->new_password_field =
+ FindFieldWithUniqueRendererId(fields, prediction.first);
+ break;
+ case CredentialFieldType::kConfirmationPassword:
+ result->confirmation_password_field =
+ FindFieldWithUniqueRendererId(fields, prediction.first);
+ break;
+ case CredentialFieldType::kNone:
+ break;
+ }
+ }
+ return result->IsEmpty() ? nullptr : std::move(result);
+}
+
// Tries to parse |fields| based on autocomplete attributes from
// |autocomplete_cache|. Assumption on the usage autocomplete attributes:
// 1. Not more than 1 field with autocomplete=username.
@@ -201,9 +246,7 @@
}
}
- if (result->IsEmpty())
- return nullptr;
- return result;
+ return result->IsEmpty() ? nullptr : std::move(result);
}
// Returns only relevant password fields from |fields|. Namely
@@ -405,7 +448,6 @@
const autofill::FormData& form_data,
const FormPredictions* form_predictions,
FormParsingMode mode) {
- // TODO(crbug.com/845426): Use predictions.
std::vector<const FormFieldData*> fields = GetTextFields(form_data.fields);
@@ -427,7 +469,15 @@
result->action = form_data.action;
result->form_data = form_data;
- // TODO(crbug.com/845426): Add parsing with trusted server-side hints.
+ if (form_predictions) {
+ // Try to parse with server predictions.
+ auto predictions_parse_result =
+ ParseUsingPredictions(fields, *form_predictions);
+ if (predictions_parse_result) {
+ SetFields(*predictions_parse_result, result.get());
+ return result;
+ }
+ }
// Try to parse with autocomplete attributes.
auto autocomplete_parse_result =
diff --git a/components/password_manager/core/browser/form_parsing/form_parser_unittest.cc b/components/password_manager/core/browser/form_parsing/form_parser_unittest.cc
index ddc3d1e0..97833de 100644
--- a/components/password_manager/core/browser/form_parsing/form_parser_unittest.cc
+++ b/components/password_manager/core/browser/form_parsing/form_parser_unittest.cc
@@ -63,6 +63,7 @@
const char* autocomplete_attribute = nullptr;
const char* value = kNonimportantValue;
const char* form_control_type = "text";
+ PasswordFieldPrediction prediction = {.type = autofill::MAX_VALID_FIELD_TYPE};
};
// Describes a test case for the parser.
@@ -104,9 +105,11 @@
// Creates a FormData to be fed to the parser. Includes FormFieldData as
// described in |fields_description|. Generates |fill_result| and |save_result|
-// expectations about the result in FILLING and SAVING mode, respectively.
+// expectations about the result in FILLING and SAVING mode, respectively. Also
+// fills |predictions| with the predictions contained in FieldDataDescriptions.
FormData GetFormDataAndExpectation(
const std::vector<FieldDataDescription>& fields_description,
+ FormPredictions* predictions,
ParseResultIds* fill_result,
ParseResultIds* save_result) {
FormData form_data;
@@ -175,6 +178,9 @@
save_result->confirmation_password_id = unique_id;
break;
}
+ if (field_description.prediction.type != autofill::MAX_VALID_FIELD_TYPE) {
+ (*predictions)[unique_id] = field_description.prediction;
+ }
}
return form_data;
}
@@ -239,10 +245,11 @@
// checks the results.
void CheckTestData(const std::vector<FormParsingTestCase>& test_cases) {
for (const FormParsingTestCase& test_case : test_cases) {
+ FormPredictions predictions;
ParseResultIds fill_result;
ParseResultIds save_result;
- const FormData form_data =
- GetFormDataAndExpectation(test_case.fields, &fill_result, &save_result);
+ const FormData form_data = GetFormDataAndExpectation(
+ test_case.fields, &predictions, &fill_result, &save_result);
for (auto mode : {FormParsingMode::FILLING, FormParsingMode::SAVING}) {
SCOPED_TRACE(
testing::Message("Test description: ")
@@ -250,7 +257,7 @@
<< (mode == FormParsingMode::FILLING ? "Filling" : "Saving"));
std::unique_ptr<PasswordForm> parsed_form =
- ParseFormData(form_data, nullptr, mode);
+ ParseFormData(form_data, &predictions, mode);
const ParseResultIds& expected_ids =
mode == FormParsingMode::FILLING ? fill_result : save_result;
@@ -827,6 +834,62 @@
});
}
+TEST(FormParserTest, ServerHints) {
+ CheckTestData({
+ {
+ "Empty predictions don't cause panic",
+ {
+ {.form_control_type = "text"},
+ {.role = ElementRole::USERNAME, .form_control_type = "text"},
+ {.role = ElementRole::CURRENT_PASSWORD,
+ .form_control_type = "password"},
+ },
+ },
+ {
+ "Username-only predictions are ignored",
+ {
+ {.form_control_type = "text",
+ .prediction = {.type = autofill::USERNAME}},
+ {.role = ElementRole::USERNAME, .form_control_type = "text"},
+ {.role = ElementRole::CURRENT_PASSWORD,
+ .form_control_type = "password"},
+ },
+ },
+ {
+ "Simple predictions work",
+ {
+ {.role = ElementRole::USERNAME,
+ .form_control_type = "text",
+ .prediction = {.type = autofill::USERNAME_AND_EMAIL_ADDRESS}},
+ {.form_control_type = "text"},
+ {.form_control_type = "password"},
+ {.role = ElementRole::CURRENT_PASSWORD,
+ .prediction = {.type = autofill::PASSWORD},
+ .form_control_type = "password"},
+ },
+ },
+ {
+ "Longer predictions work",
+ {
+ {.role = ElementRole::USERNAME,
+ .prediction = {.type = autofill::USERNAME},
+ .form_control_type = "text"},
+ {.form_control_type = "text"},
+ {.form_control_type = "password"},
+ {.role = ElementRole::NEW_PASSWORD,
+ .prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD},
+ .form_control_type = "password"},
+ {.role = ElementRole::CONFIRMATION_PASSWORD,
+ .prediction = {.type = autofill::CONFIRMATION_PASSWORD},
+ .form_control_type = "password"},
+ {.role = ElementRole::CURRENT_PASSWORD,
+ .prediction = {.type = autofill::PASSWORD},
+ .form_control_type = "password"},
+ },
+ },
+ });
+}
+
} // namespace
} // namespace password_manager
diff --git a/components/password_manager/core/browser/form_parsing/password_field_prediction.cc b/components/password_manager/core/browser/form_parsing/password_field_prediction.cc
index a499cfe..c77a9704 100644
--- a/components/password_manager/core/browser/form_parsing/password_field_prediction.cc
+++ b/components/password_manager/core/browser/form_parsing/password_field_prediction.cc
@@ -12,19 +12,33 @@
using autofill::FormStructure;
using autofill::ServerFieldType;
+namespace password_manager {
+
namespace {
// Returns true if the field is password or username prediction.
bool IsCredentialRelatedPrediction(ServerFieldType type) {
- return type == autofill::PASSWORD ||
- type == autofill::ACCOUNT_CREATION_PASSWORD ||
- type == autofill::NEW_PASSWORD ||
- type == autofill::CONFIRMATION_PASSWORD || type == autofill::USERNAME;
+ return DeriveFromServerFieldType(type) != CredentialFieldType::kNone;
}
} // namespace
-namespace password_manager {
+CredentialFieldType DeriveFromServerFieldType(ServerFieldType type) {
+ switch (type) {
+ case autofill::USERNAME:
+ case autofill::USERNAME_AND_EMAIL_ADDRESS:
+ return CredentialFieldType::kUsername;
+ case autofill::PASSWORD:
+ return CredentialFieldType::kCurrentPassword;
+ case autofill::ACCOUNT_CREATION_PASSWORD:
+ case autofill::NEW_PASSWORD:
+ return CredentialFieldType::kNewPassword;
+ case autofill::CONFIRMATION_PASSWORD:
+ return CredentialFieldType::kConfirmationPassword;
+ default:
+ return CredentialFieldType::kNone;
+ }
+}
FormPredictions ConvertToFormPredictions(const FormData& observed_form,
const FormStructure& form_structure) {
diff --git a/components/password_manager/core/browser/form_parsing/password_field_prediction.h b/components/password_manager/core/browser/form_parsing/password_field_prediction.h
index de7cb7c..b4f48d2d 100644
--- a/components/password_manager/core/browser/form_parsing/password_field_prediction.h
+++ b/components/password_manager/core/browser/form_parsing/password_field_prediction.h
@@ -17,6 +17,18 @@
namespace password_manager {
+enum class CredentialFieldType {
+ kNone,
+ kUsername,
+ kCurrentPassword,
+ kNewPassword,
+ kConfirmationPassword
+};
+
+// Transforms the general field type to the information useful for password
+// forms.
+CredentialFieldType DeriveFromServerFieldType(autofill::ServerFieldType type);
+
// Contains server predictions for a field.
// This is the struct rather than using because it will be expanded soon with
// additional information.
diff --git a/components/password_manager/core/browser/form_parsing/password_field_prediction_unittest.cc b/components/password_manager/core/browser/form_parsing/password_field_prediction_unittest.cc
index bc047634..05f1c79 100644
--- a/components/password_manager/core/browser/form_parsing/password_field_prediction_unittest.cc
+++ b/components/password_manager/core/browser/form_parsing/password_field_prediction_unittest.cc
@@ -12,15 +12,19 @@
#include "testing/gtest/include/gtest/gtest.h"
using autofill::AutofillField;
+using autofill::ACCOUNT_CREATION_PASSWORD;
using autofill::CONFIRMATION_PASSWORD;
using autofill::EMAIL_ADDRESS;
using autofill::FormData;
using autofill::FormFieldData;
using autofill::FormStructure;
+using autofill::NEW_PASSWORD;
+using autofill::NO_SERVER_DATA;
using autofill::PASSWORD;
using autofill::ServerFieldType;
using autofill::UNKNOWN_TYPE;
using autofill::USERNAME;
+using autofill::USERNAME_AND_EMAIL_ADDRESS;
using base::ASCIIToUTF16;
namespace password_manager {
@@ -80,6 +84,33 @@
}
}
+TEST(FormPredictionsTest, DeriveFromServerFieldType) {
+ struct TestCase {
+ const char* name;
+ // Input.
+ ServerFieldType server_type;
+ CredentialFieldType expected_result;
+ } test_cases[] = {
+ {"No prediction", NO_SERVER_DATA, CredentialFieldType::kNone},
+ {"Irrelevant type", EMAIL_ADDRESS, CredentialFieldType::kNone},
+ {"Username", USERNAME, CredentialFieldType::kUsername},
+ {"Username/Email", USERNAME_AND_EMAIL_ADDRESS,
+ CredentialFieldType::kUsername},
+ {"Password", PASSWORD, CredentialFieldType::kCurrentPassword},
+ {"New password", NEW_PASSWORD, CredentialFieldType::kNewPassword},
+ {"Account creation password", ACCOUNT_CREATION_PASSWORD,
+ CredentialFieldType::kNewPassword},
+ {"Confirmation password", CONFIRMATION_PASSWORD,
+ CredentialFieldType::kConfirmationPassword},
+ };
+
+ for (const TestCase& test_case : test_cases) {
+ SCOPED_TRACE(test_case.name);
+ EXPECT_EQ(test_case.expected_result,
+ DeriveFromServerFieldType(test_case.server_type));
+ }
+}
+
} // namespace
} // namespace password_manager