Bug 1556953 - Only fill with an exact match when tabbing between fields in a login form. r=MattN

Differential Revision: https://phabricator.services.mozilla.com/D47191

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Sam Foster 2019-10-08 23:49:54 +00:00
Родитель 23f72bc330
Коммит 13f15730a5
4 изменённых файлов: 250 добавлений и 36 удалений

Просмотреть файл

@ -1077,6 +1077,20 @@ this.LoginManagerContent = {
showMasterPassword: false,
})
.then(({ form, loginsFound, recipes }) => {
if (!loginGUID) {
// not an explicit autocomplete menu selection, filter for exact matches only
loginsFound = this._filterForExactFormOriginLogins(
loginsFound,
acForm
);
// filter the list for exact matches with the username
// NOTE: this could be an empty string which is a valid username
let searchString = usernameField.value.toLowerCase();
loginsFound = loginsFound.filter(
l => l.username.toLowerCase() == searchString
);
}
this._fillForm(form, loginsFound, recipes, {
autofillForm: true,
clobberPassword: true,
@ -1709,6 +1723,53 @@ this.LoginManagerContent = {
});
},
/**
* Filter logins for exact origin/formActionOrigin and dedupe on usernamematche
* @param {nsILoginInfo[]} logins an array of nsILoginInfo that could be
* used for the form, including ones with a different form action origin
* which are only used when the fill is userTriggered
* @param {LoginForm} form
*/
_filterForExactFormOriginLogins(logins, form) {
let loginOrigin = LoginHelper.getLoginOrigin(
form.ownerDocument.documentURI
);
let formActionOrigin = LoginHelper.getFormActionOrigin(form);
logins = logins.filter(l => {
let formActionMatches = LoginHelper.isOriginMatching(
l.formActionOrigin,
formActionOrigin,
{
schemeUpgrades: LoginHelper.schemeUpgrades,
acceptWildcardMatch: true,
acceptDifferentSubdomains: false,
}
);
let formOriginMatches = LoginHelper.isOriginMatching(
l.origin,
loginOrigin,
{
schemeUpgrades: LoginHelper.schemeUpgrades,
acceptWildcardMatch: true,
acceptDifferentSubdomains: false,
}
);
return formActionMatches && formOriginMatches;
});
// Since the logins are already filtered now to only match the origin and formAction,
// dedupe to just the username since remaining logins may have different schemes.
logins = LoginHelper.dedupeLogins(
logins,
["username"],
["scheme", "timePasswordChanged"],
loginOrigin,
formActionOrigin
);
return logins;
},
/**
* Attempt to find the username and password fields in a form, and fill them
* in using the provided logins and recipes.
@ -1839,41 +1900,7 @@ this.LoginManagerContent = {
if (!userTriggered) {
// Only autofill logins that match the form's action and origin. In the above code
// we have attached autocomplete for logins that don't match the form action.
let loginOrigin = LoginHelper.getLoginOrigin(
form.ownerDocument.documentURI
);
let formActionOrigin = LoginHelper.getFormActionOrigin(form);
foundLogins = foundLogins.filter(l => {
let formActionMatches = LoginHelper.isOriginMatching(
l.formActionOrigin,
formActionOrigin,
{
schemeUpgrades: LoginHelper.schemeUpgrades,
acceptWildcardMatch: true,
acceptDifferentSubdomains: false,
}
);
let formOriginMatches = LoginHelper.isOriginMatching(
l.origin,
loginOrigin,
{
schemeUpgrades: LoginHelper.schemeUpgrades,
acceptWildcardMatch: true,
acceptDifferentSubdomains: false,
}
);
return formActionMatches && formOriginMatches;
});
// Since the logins are already filtered now to only match the origin and formAction,
// dedupe to just the username since remaining logins may have different schemes.
foundLogins = LoginHelper.dedupeLogins(
foundLogins,
["username"],
["scheme", "timePasswordChanged"],
loginOrigin,
formActionOrigin
);
foundLogins = this._filterForExactFormOriginLogins(foundLogins, form);
}
// Nothing to do if we have no matching logins available.

Просмотреть файл

@ -43,6 +43,9 @@ skip-if = toolkit == 'android' # autocomplete
[test_autocomplete_sandboxed.html]
scheme = https
skip-if = toolkit == 'android' # autocomplete
[test_autocomplete_tab_between_fields.html]
scheme = https
skip-if = toolkit == 'android' # autocomplete
[test_autofill_autocomplete_types.html]
scheme = https
skip-if = toolkit == 'android' # bug 1533965

Просмотреть файл

@ -15,6 +15,11 @@ function initLogins() {
let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
login1.init("https://example.com", "https://autofill", null, "user1", "pass1");
Services.logins.addLogin(login1);
// add a 2nd matching user to prevent autofill
let login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
login2.init("https://example.com", "https://autofill", null, "user2", "pass2");
Services.logins.addLogin(login2);
}
runInParent(initLogins);
@ -27,7 +32,7 @@ function preventDefaultAndStopProgagation(event) {
}
</script>
<p id="display">
<form id="form1" action="https://no-autofill" onsubmit="return false;">
<form id="form1" action="https://autofill" onsubmit="return false;">
<input type="text" name="uname">
<input type="password" name="pword">
<button type="submit">Submit</button>

Просмотреть файл

@ -0,0 +1,179 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test autocomplete behavior when tabbing between form fields</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<script type="text/javascript" src="pwmgr_common.js"></script>
<script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script>
let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
SpecialPowers.Ci.nsILoginInfo,
"init");
let readyPromise = registerRunTests();
</script>
<p id="display"></p>
<!-- we presumably can't hide the content for this test. -->
<div id="content">
<form id="form1" action="https://autofill" onsubmit="return false;">
<input type="text" name="uname">
<input type="password" name="pword">
<button type="submit">Submit</button>
</form>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
const { TestUtils } = SpecialPowers.Cu.import("resource://testing-common/TestUtils.jsm");
async function prepareLogins(logins = []) {
await LoginManager.removeAllLogins();
for (let login of logins) {
let storageAddPromise = promiseStorageChanged(["addLogin"]);
await LoginManager.addLogin(login);
await storageAddPromise;
}
let count = (await LoginManager.getAllLogins()).length;
is(count, logins.length, "All logins were added");
}
const availableLogins = {
"exampleUser1": new nsLoginInfo("https://example.com", "https://autofill", null,
"user1", "pass1", "uname", "pword"),
"subdomainUser1": new nsLoginInfo("https://sub.example.com", "https://autofill", null,
"user1", "pass1", "uname", "pword"),
}
const tests = [
{
name: "single_login_exact_origin_no_inputs",
logins: ["exampleUser1"],
expectedAutofillUsername: "user1",
expectedAutofillPassword: "pass1",
expectedACLabels: ["user1"],
typeUsername: null,
expectedTabbedUsername: "",
expectedTabbedPassword: "",
},
{
name: "single_login_exact_origin_initial_letter",
logins: ["exampleUser1"],
expectedAutofillUsername: "user1",
expectedAutofillPassword: "pass1",
expectedACLabels: ["user1"],
typeUsername: "u",
expectedTabbedUsername: "u",
expectedTabbedPassword: "",
},
{
name: "single_login_exact_origin_type_username",
logins: ["exampleUser1"],
expectedAutofillUsername: "user1",
expectedAutofillPassword: "pass1",
expectedACLabels: ["user1"],
typeUsername: "user1",
expectedTabbedUsername: "user1",
expectedTabbedPassword: "pass1",
},
{
name: "single_login_subdomain_no_inputs",
logins: ["subdomainUser1"],
expectedAutofillUsername: "",
expectedAutofillPassword: "",
expectedACLabels: ["user1"],
typeUsername: null,
expectedTabbedUsername: "",
expectedTabbedPassword: "",
},
{
name: "single_login_subdomain_type_username",
logins: ["subdomainUser1"],
expectedAutofillUsername: "",
expectedAutofillPassword: "",
expectedACLabels: ["user1"],
typeUsername: "user1",
expectedTabbedUsername: "user1",
expectedTabbedPassword: "",
},
];
add_task(async function setup() {
ok(readyPromise, "check promise is available");
await readyPromise;
});
async function testResultOfTabInteractions(testData) {
await SimpleTest.promiseFocus(window);
let logins = testData.logins.map(name => availableLogins[name]);
await prepareLogins(logins);
info("recreating form");
let processed = promiseFormsProcessed();
recreateTree(document.getElementById("form1"));
info("waiting for form processed");
await processed;
// check autofill results
checkForm(1, testData.expectedAutofillUsername, testData.expectedAutofillPassword);
let pword = $_(1, "pword");
let uname = $_(1, "uname");
SpecialPowers.wrap(pword).setUserInput("");
SpecialPowers.wrap(uname).setUserInput("");
info("Placing focus in the password field");
const shownPromise = promiseACShown();
pword.focus();
await synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un
await new Promise(resolve => SimpleTest.executeSoon(resolve));
// moving focus shouldn't change anything
checkForm(1, "", "");
await synthesizeKey("KEY_ArrowDown");
info("waiting for AC results");
let results = await shownPromise;
info("checking results");
checkAutoCompleteResults(results, testData.expectedACLabels, "example.com", "Check all rows are correct");
if (testData.typeUsername) {
await sendString(testData.typeUsername);
}
// don't select anything from the AC menu
await synthesizeKey("KEY_Escape");
await TestUtils.waitForCondition(async () => {
let popupState = await getPopupState();
return !popupState.open;
}, "AutoComplete popup should have closed");
await synthesizeKey("KEY_Tab");
await new Promise(resolve => SimpleTest.executeSoon(resolve));
ok($_(1, "pword").matches("input:focus"), "pword field is focused");
checkForm(1, testData.expectedTabbedUsername, testData.expectedTabbedPassword);
recreateTree(document.getElementById("form1"));
await promiseFormsProcessed();
// tidy up by closing any open AC popup
await synthesizeKey("KEY_Escape");
}
for (let testData of tests) {
let tmp = {
async [testData.name]() {
await testResultOfTabInteractions(testData);
},
};
add_task(tmp[testData.name]);
}
</script>
</pre>
</body>
</html>