Bug 1147563 - Provide autocomplete experience when formSubmitURL does not match. r=MattN

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

--HG--
rename : toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html => toolkit/components/passwordmgr/test/mochitest/test_autofill_different_formSubmitURL.html
extra : moz-landing-system : lando
This commit is contained in:
Jared Wein 2019-04-02 18:24:47 +00:00
Родитель c7531ea325
Коммит 1676f31627
6 изменённых файлов: 251 добавлений и 14 удалений

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

@ -272,6 +272,7 @@ var LoginHelper = {
*/
isOriginMatching(aLoginOrigin, aSearchOrigin, aOptions = {
schemeUpgrades: false,
acceptWildcardMatch: false,
}) {
if (aLoginOrigin == aSearchOrigin) {
return true;
@ -281,6 +282,10 @@ var LoginHelper = {
return false;
}
if (aOptions.acceptWildcardMatch && aLoginOrigin == "") {
return true;
}
if (aOptions.schemeUpgrades) {
try {
let loginURI = Services.io.newURI(aLoginOrigin);
@ -480,12 +485,17 @@ var LoginHelper = {
* String representing the origin to use for preferring one login over
* another when they are dupes. This is used with "scheme" for
* `resolveBy` so the scheme from this origin will be preferred.
* @param {string} [preferredFormActionOrigin = undefined]
* String representing the action origin to use for preferring one login over
* another when they are dupes. This is used with "actionOrigin" for
* `resolveBy` so the scheme from this action origin will be preferred.
*
* @returns {nsILoginInfo[]} list of unique logins.
*/
dedupeLogins(logins, uniqueKeys = ["username", "password"],
resolveBy = ["timeLastUsed"],
preferredOrigin = undefined) {
preferredOrigin = undefined,
preferredFormActionOrigin = undefined) {
const KEY_DELIMITER = ":";
if (!preferredOrigin && resolveBy.includes("scheme")) {
@ -530,6 +540,16 @@ var LoginHelper = {
for (let preference of resolveBy) {
switch (preference) {
case "actionOrigin": {
if (!preferredFormActionOrigin) {
break;
}
if (LoginHelper.isOriginMatching(existingLogin.formSubmitURL, preferredFormActionOrigin, {schemeUpgrades: LoginHelper.schemeUpgrades}) &&
!LoginHelper.isOriginMatching(login.formSubmitURL, preferredFormActionOrigin, {schemeUpgrades: LoginHelper.schemeUpgrades})) {
return false;
}
break;
}
case "scheme": {
if (!preferredOriginScheme) {
break;

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

@ -1179,9 +1179,10 @@ var LoginManagerContent = {
*
* @param {LoginForm} form
* @param {nsILoginInfo[]} foundLogins an array of nsILoginInfo that could be
used for the form
* used for the form, including ones with a different form action origin
* which are only used when the fill is userTriggered
* @param {Set} recipes a set of recipes that could be used to affect how the
form is filled
* form is filled
* @param {Object} [options = {}] a list of options for this method
* @param {HTMLInputElement} [options.inputElement = null] an optional target
* input element we want to fill
@ -1229,8 +1230,9 @@ var LoginManagerContent = {
};
try {
// Nothing to do if we have no matching logins available,
// and there isn't a need to show the insecure form warning.
// Nothing to do if we have no matching (excluding form action
// checks) logins available, and there isn't a need to show
// the insecure form warning.
if (foundLogins.length == 0 &&
(InsecurePasswordUtils.isFormSecure(form) ||
!LoginHelper.showInsecureFieldWarning)) {
@ -1286,6 +1288,19 @@ var LoginManagerContent = {
usernameField.addEventListener("keydown", observer);
}
if (!userTriggered) {
// Only autofill logins that match the form's action. In the above code
// we have attached autocomplete for logins that don't match the form action.
foundLogins = foundLogins.filter(l => {
return LoginHelper.isOriginMatching(l.formSubmitURL,
LoginHelper.getFormActionOrigin(form),
{
schemeUpgrades: LoginHelper.schemeUpgrades,
acceptWildcardMatch: true,
});
});
}
// Nothing to do if we have no matching logins available.
// Only insecure pages reach this block and logs the same
// telemetry flag.

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

@ -40,14 +40,17 @@ var LoginManagerParent = {
// to avoid spamming master password prompts on autocomplete searches.
_lastMPLoginCancelled: Math.NEGATIVE_INFINITY,
_searchAndDedupeLogins(formOrigin, actionOrigin) {
_searchAndDedupeLogins(formOrigin, actionOrigin, {looseActionOriginMatch} = {}) {
let logins;
let matchData = {
hostname: formOrigin,
schemeUpgrades: LoginHelper.schemeUpgrades,
};
if (!looseActionOriginMatch) {
matchData.formSubmitURL = actionOrigin;
}
try {
logins = LoginHelper.searchLoginsWithObject({
hostname: formOrigin,
formSubmitURL: actionOrigin,
schemeUpgrades: LoginHelper.schemeUpgrades,
});
logins = LoginHelper.searchLoginsWithObject(matchData);
} catch (e) {
// Record the last time the user cancelled the MP prompt
// to avoid spamming them with MP prompts for autocomplete.
@ -61,10 +64,11 @@ var LoginManagerParent = {
// Dedupe so the length checks below still make sense with scheme upgrades.
let resolveBy = [
"actionOrigin",
"scheme",
"timePasswordChanged",
];
return LoginHelper.dedupeLogins(logins, ["username"], resolveBy, formOrigin);
return LoginHelper.dedupeLogins(logins, ["username"], resolveBy, formOrigin, actionOrigin);
},
// Listeners are added in BrowserGlue.jsm on desktop
@ -222,7 +226,8 @@ var LoginManagerParent = {
return;
}
let logins = this._searchAndDedupeLogins(formOrigin, actionOrigin);
// Autocomplete results do not need to match actionOrigin.
let logins = this._searchAndDedupeLogins(formOrigin, actionOrigin, {looseActionOriginMatch: true});
log("sendLoginDataToChild:", logins.length, "deduped logins");
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
@ -270,7 +275,8 @@ var LoginManagerParent = {
} else {
log("Creating new autocomplete search result.");
logins = this._searchAndDedupeLogins(formOrigin, actionOrigin);
// Autocomplete results do not need to match actionOrigin.
logins = this._searchAndDedupeLogins(formOrigin, actionOrigin, {looseActionOriginMatch: true});
}
let matchingLogins = logins.filter(function(fullMatch) {

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

@ -38,6 +38,9 @@ skip-if = toolkit == 'android' # autocomplete
[test_autofill_autocomplete_types.html]
scheme = https
skip-if = toolkit == 'android' # bug 1533965
[test_autofill_different_formSubmitURL.html]
scheme = https
skip-if = toolkit == 'android' # Bug 1259768
[test_autofill_from_bfcache.html]
scheme = https
skip-if = toolkit == 'android' # bug 1527403
@ -61,6 +64,9 @@ skip-if = toolkit == 'android' # autocomplete
[test_basic_form_3pw_1.html]
[test_basic_form_autocomplete.html]
skip-if = toolkit == 'android' # android:autocomplete.
[test_basic_form_autocomplete_formSubmitURL.html]
skip-if = toolkit == 'android' # android:autocomplete.
scheme = https
[test_basic_form_honor_autocomplete_off.html]
scheme = https
skip-if = toolkit == 'android' # android:autocomplete.

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

@ -0,0 +1,78 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test autofill on an HTTPS page using upgraded HTTP logins wtih different formSubmitURL</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="/tests/SimpleTest/AddTask.js"></script>
<script type="text/javascript" src="pwmgr_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script>
const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html";
const chromeScript = runChecksAfterCommonInit(false);
let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
SpecialPowers.Ci.nsILoginInfo,
"init");
</script>
<p id="display"></p>
<!-- we presumably can't hide the content for this test. -->
<div id="content">
<iframe></iframe>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">
let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]);
// Check for expected username/password in form.
function checkACForm(expectedUsername, expectedPassword) {
let iframeDoc = iframe.contentDocument;
let uname = iframeDoc.getElementById("form-basic-username");
let pword = iframeDoc.getElementById("form-basic-password");
let formID = uname.parentNode.id;
is(uname.value, expectedUsername, "Checking " + formID + " username");
is(pword.value, expectedPassword, "Checking " + formID + " password");
}
async function prepareLoginsAndProcessForm(url, logins = []) {
LoginManager.removeAllLogins();
let dates = Date.now();
for (let login of logins) {
SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo);
// Force all dates to be the same so they don't affect things like deduping.
login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates;
LoginManager.addLogin(login);
}
iframe.src = url;
await promiseFormsProcessed();
}
add_task(async function test_formSubmitURL_wildcard_should_autofill() {
await prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
new nsLoginInfo("https://example.com", "", null,
"name2", "pass2", "uname", "pword"),
]);
checkACForm("name2", "pass2");
});
add_task(async function test_formSubmitURL_different_shouldnt_autofill() {
await prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [
new nsLoginInfo("https://example.com", "https://another.domain", null,
"name2", "pass2", "uname", "pword"),
]);
checkACForm("", "");
});
</script>
</pre>
</body>
</html>

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

@ -0,0 +1,112 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test that logins with non-matching formSubmitURL appear in autocomplete dropdown</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="/tests/SimpleTest/AddTask.js"></script>
<script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
<script type="text/javascript" src="pwmgr_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
Login Manager test: logins with non-matching formSubmitURL appear in autocomplete dropdown
<script>
var chromeScript = runChecksAfterCommonInit();
runInParent(function setup() {
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
// Create some logins just for this form, since we'll be deleting them.
var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
Ci.nsILoginInfo, "init");
assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
var login1 = new nsLoginInfo("https://example.com", "https://differentFormSubmitURL", null,
"dfsu1", "dfsp1", "uname", "pword");
Services.logins.addLogin(login1);
});
</script>
<p id="display"></p>
<!-- we presumably can't hide the content for this test. -->
<div id="content">
<!-- form1 tests multiple matching logins -->
<form id="form1" action="https://autocomplete:8888/formtest.js" 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">
/** Test for Login Manager: multiple login autocomplete. **/
var uname = $_(1, "uname");
var pword = $_(1, "pword");
// Restore the form to the default state.
function restoreForm() {
uname.value = "";
pword.value = "";
uname.focus();
}
// Check for expected username/password in form.
function checkACForm(expectedUsername, expectedPassword) {
var formID = uname.parentNode.id;
is(uname.value, expectedUsername, "Checking " + formID + " username is: " + expectedUsername);
is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
}
function spinEventLoop() {
return Promise.resolve();
}
add_task(async function setup() {
listenForUnexpectedPopupShown();
});
add_task(async function test_form1_initial_empty() {
await SimpleTest.promiseFocus(window);
// Make sure initial form is empty.
checkACForm("", "");
let popupState = await getPopupState();
is(popupState.open, false, "Check popup is initially closed");
});
/* For this testcase, the only login that exists for this origin
* is one with a different formSubmitURL, so the login will appear
* in the autocomplete popup.
*/
add_task(async function test_form1_menu_shows_logins_for_different_formSubmitURL() {
await SimpleTest.promiseFocus(window);
// Trigger autocomplete popup
restoreForm();
let shownPromise = promiseACShown();
synthesizeKey("KEY_ArrowDown"); // open
let results = await shownPromise;
let popupState = await getPopupState();
is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
let expectedMenuItems = ["dfsu1"];
checkAutoCompleteResults(results, expectedMenuItems, "example.com", "Check all menuitems are displayed correctly.");
synthesizeKey("KEY_ArrowDown"); // first item
checkACForm("", ""); // value shouldn't update just by selecting
synthesizeKey("KEY_Enter");
await promiseFormsProcessed();
checkACForm("dfsu1", "dfsp1");
});
</script>
</pre>
</body>
</html>