Bug 1289913 - Show autocomplete UI on password fields.; r=MattN
MozReview-Commit-ID: LGKM6igKbQB
This commit is contained in:
@@ -2171,6 +2171,9 @@ pref("security.xcto_nosniff_block_images", false);
|
||||
// OCSP must-staple
|
||||
pref("security.ssl.enable_ocsp_must_staple", true);
|
||||
|
||||
// Insecure Form Field Warning
|
||||
pref("security.insecure_field_warning.contextual.enabled", false);
|
||||
|
||||
// Disable pinning checks by default.
|
||||
pref("security.cert_pinning.enforcement_level", 0);
|
||||
// Do not process hpkp headers rooted by not built in roots by default.
|
||||
|
||||
@@ -36,6 +36,7 @@ this.LoginHelper = {
|
||||
formlessCaptureEnabled: Services.prefs.getBoolPref("signon.formlessCapture.enabled"),
|
||||
schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
|
||||
insecureAutofill: Services.prefs.getBoolPref("signon.autofillForms.http"),
|
||||
showInsecureFieldWarning: Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled"),
|
||||
|
||||
createLogger(aLogPrefix) {
|
||||
let getMaxLogLevel = () => {
|
||||
@@ -59,6 +60,10 @@ this.LoginHelper = {
|
||||
logger.maxLogLevel = getMaxLogLevel();
|
||||
}, false);
|
||||
|
||||
Services.prefs.addObserver("security.insecure_field_warning.", () => {
|
||||
this.showInsecureFieldWarning = Services.prefs.getBoolPref("security.insecure_field_warning.contextual.enabled");
|
||||
}, false);
|
||||
|
||||
return logger;
|
||||
},
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
|
||||
|
||||
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
||||
const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1;
|
||||
const PREF_INSECURE_FIELD_WARNING_ENABLED = "security.insecure_field_warning.contextual.enabled";
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
@@ -313,6 +312,7 @@ var LoginManagerContent = {
|
||||
previousResult: previousResult,
|
||||
rect: aRect,
|
||||
isSecure: InsecurePasswordUtils.isFormSecure(form),
|
||||
isPasswordField: aElement.type == "password",
|
||||
remote: remote };
|
||||
|
||||
return this._sendRequest(messageManager, requestData,
|
||||
@@ -974,6 +974,8 @@ var LoginManagerContent = {
|
||||
return;
|
||||
}
|
||||
|
||||
this._formFillService.markAsLoginManagerField(passwordField);
|
||||
|
||||
// If the password field is disabled or read-only, there's nothing to do.
|
||||
if (passwordField.disabled || passwordField.readOnly) {
|
||||
log("not filling form, password field disabled or read-only");
|
||||
@@ -1027,8 +1029,9 @@ var LoginManagerContent = {
|
||||
// Attach autocomplete stuff to the username field, if we have
|
||||
// one. This is normally used to select from multiple accounts,
|
||||
// but even with one account we should refill if the user edits.
|
||||
if (usernameField)
|
||||
if (usernameField) {
|
||||
this._formFillService.markAsLoginManagerField(usernameField);
|
||||
}
|
||||
|
||||
// Don't clobber an existing password.
|
||||
if (passwordField.value && !clobberPassword) {
|
||||
@@ -1217,7 +1220,7 @@ var LoginUtils = {
|
||||
};
|
||||
|
||||
// nsIAutoCompleteResult implementation
|
||||
function UserAutoCompleteResult (aSearchString, matchingLogins, {isSecure, messageManager}) {
|
||||
function UserAutoCompleteResult (aSearchString, matchingLogins, {isSecure, messageManager, isPasswordField}) {
|
||||
function loginSort(a, b) {
|
||||
var userA = a.username.toLowerCase();
|
||||
var userB = b.username.toLowerCase();
|
||||
@@ -1231,15 +1234,32 @@ function UserAutoCompleteResult (aSearchString, matchingLogins, {isSecure, messa
|
||||
return 0;
|
||||
}
|
||||
|
||||
let prefShowInsecureFieldWarning =
|
||||
Preferences.get(PREF_INSECURE_FIELD_WARNING_ENABLED, false);
|
||||
function findDuplicates(loginList) {
|
||||
let seen = new Set();
|
||||
let duplicates = new Set();
|
||||
for (let login of loginList) {
|
||||
if (seen.has(login.username)) {
|
||||
duplicates.add(login.username);
|
||||
}
|
||||
seen.add(login.username);
|
||||
}
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
this._showInsecureFieldWarning = (!isSecure && prefShowInsecureFieldWarning) ? 1 : 0;
|
||||
if (!LoginHelper.insecureAutofill && !isSecure) {
|
||||
matchingLogins = [];
|
||||
}
|
||||
|
||||
this._showInsecureFieldWarning = (!isSecure && LoginHelper.showInsecureFieldWarning) ? 1 : 0;
|
||||
this.searchString = aSearchString;
|
||||
this.logins = matchingLogins.sort(loginSort);
|
||||
this.matchCount = matchingLogins.length + this._showInsecureFieldWarning;
|
||||
this._messageManager = messageManager;
|
||||
this._isPasswordField = isPasswordField;
|
||||
this._stringBundle = Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
|
||||
this._duplicateUsernames = findDuplicates(matchingLogins);
|
||||
this._dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
|
||||
{ day: "numeric", month: "short", year: "numeric" });
|
||||
|
||||
if (this.matchCount > 0) {
|
||||
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
|
||||
@@ -1268,25 +1288,50 @@ UserAutoCompleteResult.prototype = {
|
||||
matchCount : 0,
|
||||
|
||||
getValueAt(index) {
|
||||
if (index < 0 || index >= this.matchCount)
|
||||
if (index < 0 || index >= this.matchCount) {
|
||||
throw new Error("Index out of range.");
|
||||
}
|
||||
|
||||
if (this._showInsecureFieldWarning && index === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return this.logins[index - this._showInsecureFieldWarning].username;
|
||||
let selectedLogin = this.logins[index - this._showInsecureFieldWarning];
|
||||
|
||||
return this._isPasswordField ? selectedLogin.password : selectedLogin.username;
|
||||
},
|
||||
|
||||
getLabelAt(index) {
|
||||
if (index < 0 || index >= this.matchCount)
|
||||
if (index < 0 || index >= this.matchCount) {
|
||||
throw new Error("Index out of range.");
|
||||
}
|
||||
|
||||
if (this._showInsecureFieldWarning && index === 0) {
|
||||
return this._stringBundle.GetStringFromName("insecureFieldWarningDescription");
|
||||
}
|
||||
|
||||
return this.logins[index - this._showInsecureFieldWarning].username;
|
||||
let that = this;
|
||||
|
||||
function getLocalizedString(key, formatArgs) {
|
||||
if (formatArgs) {
|
||||
return that._stringBundle.formatStringFromName(key, formatArgs, formatArgs.length);
|
||||
}
|
||||
return that._stringBundle.GetStringFromName(key);
|
||||
}
|
||||
|
||||
let login = this.logins[index - this._showInsecureFieldWarning];
|
||||
let username = login.username;
|
||||
// If login is empty or duplicated we want to append a modification date to it.
|
||||
if (!username || this._duplicateUsernames.has(username)) {
|
||||
if (!username) {
|
||||
username = getLocalizedString("noUsername");
|
||||
}
|
||||
let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
|
||||
let time = this._dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
|
||||
username = getLocalizedString("loginHostAge", [username, time]);
|
||||
}
|
||||
|
||||
return username;
|
||||
},
|
||||
|
||||
getCommentAt(index) {
|
||||
@@ -1309,8 +1354,9 @@ UserAutoCompleteResult.prototype = {
|
||||
},
|
||||
|
||||
removeValueAt(index, removeFromDB) {
|
||||
if (index < 0 || index >= this.matchCount)
|
||||
throw new Error("Index out of range.");
|
||||
if (index < 0 || index >= this.matchCount) {
|
||||
throw new Error("Index out of range.");
|
||||
}
|
||||
|
||||
if (this._showInsecureFieldWarning && index === 0) {
|
||||
// Ignore the warning message item.
|
||||
|
||||
@@ -227,7 +227,8 @@ var LoginManagerParent = {
|
||||
|
||||
doAutocompleteSearch: function({ formOrigin, actionOrigin,
|
||||
searchString, previousResult,
|
||||
rect, requestId, isSecure, remote }, target) {
|
||||
rect, requestId, isSecure, isPasswordField,
|
||||
remote }, target) {
|
||||
// Note: previousResult is a regular object, not an
|
||||
// nsIAutoCompleteResult.
|
||||
|
||||
@@ -260,7 +261,11 @@ var LoginManagerParent = {
|
||||
let match = fullMatch.username;
|
||||
|
||||
// Remove results that are too short, or have different prefix.
|
||||
// Also don't offer empty usernames as possible results.
|
||||
// Also don't offer empty usernames as possible results except
|
||||
// for password field.
|
||||
if (isPasswordField) {
|
||||
return true;
|
||||
}
|
||||
return match && match.toLowerCase().startsWith(searchStringLower);
|
||||
});
|
||||
|
||||
|
||||
@@ -484,6 +484,11 @@ LoginManager.prototype = {
|
||||
|
||||
let form = LoginFormFactory.createFromField(aElement);
|
||||
let isSecure = InsecurePasswordUtils.isFormSecure(form);
|
||||
let isPasswordField = aElement.type == "password";
|
||||
if (isPasswordField) {
|
||||
// The login items won't be filtered for password field.
|
||||
aSearchString = "";
|
||||
}
|
||||
|
||||
if (!this._remember) {
|
||||
setTimeout(function() {
|
||||
@@ -518,6 +523,7 @@ LoginManager.prototype = {
|
||||
new UserAutoCompleteResult(aSearchString, logins, {
|
||||
messageManager,
|
||||
isSecure,
|
||||
isPasswordField,
|
||||
});
|
||||
aCallback.onSearchCompletion(results);
|
||||
})
|
||||
|
||||
@@ -27,6 +27,8 @@ skip-if = toolkit == 'android' # Bug 1259768
|
||||
skip-if = toolkit == 'android' # android:autocomplete.
|
||||
[test_insecure_form_field_autocomplete.html]
|
||||
skip-if = toolkit == 'android' # android:autocomplete.
|
||||
[test_password_field_autocomplete.html]
|
||||
skip-if = toolkit == 'android' # android:autocomplete.
|
||||
[test_basic_form_html5.html]
|
||||
[test_basic_form_pwevent.html]
|
||||
[test_basic_form_pwonly.html]
|
||||
|
||||
@@ -209,7 +209,8 @@ function spinEventLoop() {
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", false]]});
|
||||
yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", false],
|
||||
["signon.autofillForms.http", true]]});
|
||||
listenForUnexpectedPopupShown();
|
||||
});
|
||||
|
||||
@@ -222,6 +223,29 @@ add_task(function* test_form1_initial_empty() {
|
||||
is(popupState.open, false, "Check popup is initially closed");
|
||||
});
|
||||
|
||||
add_task(function* test_form1_menuitems() {
|
||||
yield SimpleTest.promiseFocus(window);
|
||||
// Trigger autocomplete popup
|
||||
restoreForm();
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down"); // open
|
||||
let results = yield shownPromise;
|
||||
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
|
||||
|
||||
let expectedMenuItems = ["tempuser1",
|
||||
"testuser2",
|
||||
"testuser3",
|
||||
"zzzuser4"];
|
||||
checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
|
||||
|
||||
checkACForm("", ""); // value shouldn't update just by selecting
|
||||
doKey("return"); // not "enter"!
|
||||
yield spinEventLoop(); // let focus happen
|
||||
checkACForm("", "");
|
||||
});
|
||||
|
||||
add_task(function* test_form1_first_entry() {
|
||||
yield SimpleTest.promiseFocus(window);
|
||||
// Trigger autocomplete popup
|
||||
|
||||
@@ -209,7 +209,8 @@ function spinEventLoop() {
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", true]]});
|
||||
yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", true],
|
||||
["signon.autofillForms.http", true]]});
|
||||
listenForUnexpectedPopupShown();
|
||||
});
|
||||
|
||||
@@ -228,11 +229,18 @@ add_task(function* test_form1_warning_entry() {
|
||||
restoreForm();
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down"); // open
|
||||
yield shownPromise;
|
||||
let results = yield shownPromise;
|
||||
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
|
||||
|
||||
let expectedMenuItems = ["This connection is not secure. Logins entered here could be compromised.",
|
||||
"tempuser1",
|
||||
"testuser2",
|
||||
"testuser3",
|
||||
"zzzuser4"];
|
||||
checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
|
||||
|
||||
doKey("down"); // select insecure warning
|
||||
checkACForm("", ""); // value shouldn't update just by selecting
|
||||
doKey("return"); // not "enter"!
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test basic login autocomplete</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/SpawnTask.js"></script>
|
||||
<script type="text/javascript" src="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: multiple login autocomplete
|
||||
|
||||
<script>
|
||||
var chromeScript = runChecksAfterCommonInit();
|
||||
|
||||
var setupScript = runInParent(function setup() {
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
Cu.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");
|
||||
|
||||
// login0 has no username, so should be filtered out from the autocomplete list.
|
||||
var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"", "user0pass", "", "pword");
|
||||
|
||||
var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"tempuser1", "temppass1", "uname", "pword");
|
||||
|
||||
var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"testuser2", "testpass2", "uname", "pword");
|
||||
|
||||
var login3 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"testuser3", "testpass3", "uname", "pword");
|
||||
|
||||
var login4 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"zzzuser4", "zzzpass4", "uname", "pword");
|
||||
|
||||
|
||||
// try/catch in case someone runs the tests manually, twice.
|
||||
try {
|
||||
Services.logins.addLogin(login0);
|
||||
Services.logins.addLogin(login1);
|
||||
Services.logins.addLogin(login2);
|
||||
Services.logins.addLogin(login3);
|
||||
Services.logins.addLogin(login4);
|
||||
} catch (e) {
|
||||
assert.ok(false, "addLogin threw: " + e);
|
||||
}
|
||||
|
||||
addMessageListener("addLogin", loginVariableName => {
|
||||
let login = eval(loginVariableName);
|
||||
assert.ok(!!login, "Login to add is defined: " + loginVariableName);
|
||||
Services.logins.addLogin(login);
|
||||
});
|
||||
addMessageListener("removeLogin", loginVariableName => {
|
||||
let login = eval(loginVariableName);
|
||||
assert.ok(!!login, "Login to delete is defined: " + loginVariableName);
|
||||
Services.logins.removeLogin(login);
|
||||
});
|
||||
});
|
||||
</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="http://autocomplete:8888/formtest.js" onsubmit="return false;">
|
||||
<input type="text" name="uname">
|
||||
<input type="password" name="pword">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<form id="form2" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
|
||||
<input type="text" name="uname">
|
||||
<input type="password" name="pword" readonly="true">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<form id="form3" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
|
||||
<input type="text" name="uname">
|
||||
<input type="password" name="pword" disabled="true">
|
||||
<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");
|
||||
const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
|
||||
|
||||
// Restore the form to the default state.
|
||||
function restoreForm(index) {
|
||||
// Using innerHTML is for creating the autocomplete popup again, so the
|
||||
// preference value will be applied to the constructor of
|
||||
// UserAutoCompleteResult.
|
||||
let form = document.getElementById("form" + index);
|
||||
let temp = form.innerHTML;
|
||||
form.innerHTML = "";
|
||||
form.innerHTML = temp;
|
||||
uname = $_(index, "uname");
|
||||
pword = $_(index, "pword");
|
||||
|
||||
uname.value = "";
|
||||
pword.value = "";
|
||||
pword.focus();
|
||||
}
|
||||
|
||||
function generateDateString(date) {
|
||||
let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
|
||||
{ day: "numeric", month: "short", year: "numeric" });
|
||||
return dateAndTimeFormatter.format(date);
|
||||
}
|
||||
|
||||
const DATE_NOW_STRING = generateDateString(new Date());
|
||||
|
||||
// Check for expected username/password in form.
|
||||
function checkACFormPasswordField(expectedPassword) {
|
||||
var formID = uname.parentNode.id;
|
||||
is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
|
||||
}
|
||||
|
||||
function spinEventLoop() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
add_task(function* setup() {
|
||||
listenForUnexpectedPopupShown();
|
||||
});
|
||||
|
||||
add_task(function* test_form1_initial_empty() {
|
||||
yield SimpleTest.promiseFocus(window);
|
||||
|
||||
// Make sure initial form is empty.
|
||||
checkACFormPasswordField("");
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.open, false, "Check popup is initially closed");
|
||||
});
|
||||
|
||||
add_task(function* test_form1_enabledInsecureFieldWarning_enabledInsecureAutoFillForm() {
|
||||
yield SpecialPowers.pushPrefEnv({"set": [
|
||||
["security.insecure_field_warning.contextual.enabled", true],
|
||||
["signon.autofillForms.http", true]
|
||||
]});
|
||||
restoreForm(1);
|
||||
yield SimpleTest.promiseFocus(window);
|
||||
// Trigger autocomplete popup
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down"); // open
|
||||
let results = yield shownPromise;
|
||||
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
|
||||
|
||||
let expectedMenuItems = ["This connection is not secure. Logins entered here could be compromised.",
|
||||
"No username (" + DATE_NOW_STRING + ")",
|
||||
"tempuser1",
|
||||
"testuser2",
|
||||
"testuser3",
|
||||
"zzzuser4"];
|
||||
checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
|
||||
|
||||
doKey("down"); // select insecure warning
|
||||
checkACFormPasswordField(""); // value shouldn't update just by selecting
|
||||
doKey("return"); // not "enter"!
|
||||
yield spinEventLoop(); // let focus happen
|
||||
checkACFormPasswordField("");
|
||||
});
|
||||
|
||||
add_task(function* test_form1_disabledInsecureFieldWarning_enabledInsecureAutoFillForm() {
|
||||
yield SpecialPowers.pushPrefEnv({"set": [
|
||||
["security.insecure_field_warning.contextual.enabled", false],
|
||||
["signon.autofillForms.http", true]
|
||||
]});
|
||||
restoreForm(1);
|
||||
yield SimpleTest.promiseFocus(window);
|
||||
// Trigger autocomplete popup
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down"); // open
|
||||
let results = yield shownPromise;
|
||||
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
|
||||
|
||||
let expectedMenuItems = ["No username (" + DATE_NOW_STRING + ")",
|
||||
"tempuser1",
|
||||
"testuser2",
|
||||
"testuser3",
|
||||
"zzzuser4"];
|
||||
checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
|
||||
|
||||
doKey("down"); // select first item
|
||||
checkACFormPasswordField(""); // value shouldn't update just by selecting
|
||||
doKey("return"); // not "enter"!
|
||||
yield spinEventLoop(); // let focus happen
|
||||
checkACFormPasswordField("user0pass");
|
||||
});
|
||||
|
||||
add_task(function* test_form1_enabledInsecureFieldWarning_disabledInsecureAutoFillForm() {
|
||||
yield SpecialPowers.pushPrefEnv({"set": [
|
||||
["security.insecure_field_warning.contextual.enabled", true],
|
||||
["signon.autofillForms.http", false]
|
||||
]});
|
||||
restoreForm(1);
|
||||
yield SimpleTest.promiseFocus(window);
|
||||
// Trigger autocomplete popup
|
||||
let shownPromise = promiseACShown();
|
||||
doKey("down"); // open
|
||||
let results = yield shownPromise;
|
||||
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
|
||||
|
||||
let expectedMenuItems = ["This connection is not secure. Logins entered here could be compromised."];
|
||||
checkArrayValues(results, expectedMenuItems, "Check all menuitems are displayed correctly.");
|
||||
|
||||
doKey("down"); // select insecure warning
|
||||
checkACFormPasswordField(""); // value shouldn't update just by selecting
|
||||
doKey("return"); // not "enter"!
|
||||
yield spinEventLoop(); // let focus happen
|
||||
checkACFormPasswordField("");
|
||||
});
|
||||
|
||||
add_task(function* test_form1_disabledInsecureFieldWarning_disabledInsecureAutoFillForm() {
|
||||
yield SpecialPowers.pushPrefEnv({"set": [
|
||||
["security.insecure_field_warning.contextual.enabled", false],
|
||||
["signon.autofillForms.http", false]
|
||||
]});
|
||||
restoreForm(1);
|
||||
yield SimpleTest.promiseFocus(window);
|
||||
// Trigger autocomplete popup
|
||||
doKey("down"); // open
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.open, false, "Check popup is closed with no AutoFillForms.");
|
||||
});
|
||||
|
||||
add_task(function* test_form2_password_readonly() {
|
||||
// Trigger autocomplete popup
|
||||
restoreForm(2);
|
||||
doKey("down"); // open
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.open, false, "Check popup is closed for a readonly field.");
|
||||
});
|
||||
|
||||
add_task(function* test_form3_password_disabled() {
|
||||
// Trigger autocomplete popup
|
||||
restoreForm(3);
|
||||
doKey("down"); // open
|
||||
let popupState = yield getPopupState();
|
||||
is(popupState.open, false, "Check popup is closed for a disabled field.");
|
||||
});
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,448 @@
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
Cu.import("resource://gre/modules/LoginManagerContent.jsm");
|
||||
var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
|
||||
Ci.nsILoginInfo, "init");
|
||||
|
||||
const PREF_INSECURE_FIELD_WARNING_ENABLED = "security.insecure_field_warning.contextual.enabled";
|
||||
const PREF_INSECURE_AUTOFILLFORMS_ENABLED = "signon.autofillForms.http";
|
||||
|
||||
let matchingLogins = [];
|
||||
matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"", "emptypass1", "uname", "pword"));
|
||||
|
||||
matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"tempuser1", "temppass1", "uname", "pword"));
|
||||
|
||||
matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"testuser2", "testpass2", "uname", "pword"));
|
||||
|
||||
matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"testuser3", "testpass3", "uname", "pword"));
|
||||
|
||||
matchingLogins.push(new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
|
||||
"zzzuser4", "zzzpass4", "uname", "pword"));
|
||||
|
||||
let meta = matchingLogins[0].QueryInterface(Ci.nsILoginMetaInfo);
|
||||
let dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
|
||||
{ day: "numeric", month: "short", year: "numeric" });
|
||||
let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
|
||||
const LABEL_NO_USERNAME = "No username (" + time + ")";
|
||||
|
||||
let expectedResults = [
|
||||
{
|
||||
insecureFieldWarningEnabled: true,
|
||||
insecureAutoFillFormsEnabled: true,
|
||||
isSecure: true,
|
||||
isPasswordField: false,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "tempuser1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzuser4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: true,
|
||||
insecureAutoFillFormsEnabled: true,
|
||||
isSecure: false,
|
||||
isPasswordField: false,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "",
|
||||
label: "This connection is not secure. Logins entered here could be compromised.",
|
||||
style: "insecureWarning"
|
||||
}, {
|
||||
value: "",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "tempuser1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzuser4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: true,
|
||||
insecureAutoFillFormsEnabled: true,
|
||||
isSecure: true,
|
||||
isPasswordField: true,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "emptypass1",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "temppass1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzpass4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: true,
|
||||
insecureAutoFillFormsEnabled: true,
|
||||
isSecure: false,
|
||||
isPasswordField: true,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "",
|
||||
label: "This connection is not secure. Logins entered here could be compromised.",
|
||||
style: "insecureWarning"
|
||||
}, {
|
||||
value: "emptypass1",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "temppass1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzpass4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: false,
|
||||
insecureAutoFillFormsEnabled: true,
|
||||
isSecure: true,
|
||||
isPasswordField: false,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "tempuser1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzuser4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: false,
|
||||
insecureAutoFillFormsEnabled: true,
|
||||
isSecure: false,
|
||||
isPasswordField: false,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "tempuser1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzuser4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: false,
|
||||
insecureAutoFillFormsEnabled: true,
|
||||
isSecure: true,
|
||||
isPasswordField: true,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "emptypass1",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "temppass1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzpass4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: false,
|
||||
insecureAutoFillFormsEnabled: true,
|
||||
isSecure: false,
|
||||
isPasswordField: true,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "emptypass1",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "temppass1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzpass4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: true,
|
||||
insecureAutoFillFormsEnabled: false,
|
||||
isSecure: true,
|
||||
isPasswordField: false,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "tempuser1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzuser4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: true,
|
||||
insecureAutoFillFormsEnabled: false,
|
||||
isSecure: false,
|
||||
isPasswordField: false,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "",
|
||||
label: "This connection is not secure. Logins entered here could be compromised.",
|
||||
style: "insecureWarning"
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: true,
|
||||
insecureAutoFillFormsEnabled: false,
|
||||
isSecure: true,
|
||||
isPasswordField: true,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "emptypass1",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "temppass1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzpass4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: true,
|
||||
insecureAutoFillFormsEnabled: false,
|
||||
isSecure: false,
|
||||
isPasswordField: true,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "",
|
||||
label: "This connection is not secure. Logins entered here could be compromised.",
|
||||
style: "insecureWarning"
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: false,
|
||||
insecureAutoFillFormsEnabled: false,
|
||||
isSecure: true,
|
||||
isPasswordField: false,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "tempuser1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testuser3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzuser4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: false,
|
||||
insecureAutoFillFormsEnabled: false,
|
||||
isSecure: false,
|
||||
isPasswordField: false,
|
||||
matchingLogins: matchingLogins,
|
||||
items: []
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: false,
|
||||
insecureAutoFillFormsEnabled: false,
|
||||
isSecure: true,
|
||||
isPasswordField: true,
|
||||
matchingLogins: matchingLogins,
|
||||
items: [{
|
||||
value: "emptypass1",
|
||||
label: LABEL_NO_USERNAME,
|
||||
style: ""
|
||||
}, {
|
||||
value: "temppass1",
|
||||
label: "tempuser1",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass2",
|
||||
label: "testuser2",
|
||||
style: ""
|
||||
}, {
|
||||
value: "testpass3",
|
||||
label: "testuser3",
|
||||
style: ""
|
||||
}, {
|
||||
value: "zzzpass4",
|
||||
label: "zzzuser4",
|
||||
style: ""
|
||||
}]
|
||||
},
|
||||
{
|
||||
insecureFieldWarningEnabled: false,
|
||||
insecureAutoFillFormsEnabled: false,
|
||||
isSecure: false,
|
||||
isPasswordField: true,
|
||||
matchingLogins: matchingLogins,
|
||||
items: []
|
||||
},
|
||||
];
|
||||
|
||||
add_task(function* test_all_patterns() {
|
||||
LoginHelper.createLogger("UserAutoCompleteResult");
|
||||
expectedResults.forEach(pattern => {
|
||||
Services.prefs.setBoolPref(PREF_INSECURE_FIELD_WARNING_ENABLED,
|
||||
pattern.insecureFieldWarningEnabled);
|
||||
Services.prefs.setBoolPref(PREF_INSECURE_AUTOFILLFORMS_ENABLED,
|
||||
pattern.insecureAutoFillFormsEnabled);
|
||||
let actual = new UserAutoCompleteResult("", pattern.matchingLogins,
|
||||
{
|
||||
isSecure: pattern.isSecure,
|
||||
isPasswordField: pattern.isPasswordField
|
||||
});
|
||||
pattern.items.forEach((item, index) => {
|
||||
equal(actual.getValueAt(index), item.value);
|
||||
equal(actual.getLabelAt(index), item.label);
|
||||
equal(actual.getStyleAt(index), item.style);
|
||||
});
|
||||
|
||||
if (pattern.items.length != 0) {
|
||||
Assert.throws(() => actual.getValueAt(pattern.items.length),
|
||||
/Index out of range\./);
|
||||
|
||||
Assert.throws(() => actual.getLabelAt(pattern.items.length),
|
||||
/Index out of range\./);
|
||||
|
||||
Assert.throws(() => actual.removeValueAt(pattern.items.length, true),
|
||||
/Index out of range\./);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,7 @@ run-if = buildapp == "browser"
|
||||
[test_logins_change.js]
|
||||
[test_logins_decrypt_failure.js]
|
||||
skip-if = os == "android" # Bug 1171687: Needs fixing on Android
|
||||
[test_user_autocomplete_result.js]
|
||||
[test_logins_metainfo.js]
|
||||
[test_logins_search.js]
|
||||
[test_notifications.js]
|
||||
|
||||
@@ -900,7 +900,7 @@ nsFormFillController::MaybeStartControllingInput(nsIDOMHTMLInputElement* aInput)
|
||||
return;
|
||||
|
||||
nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(aInput);
|
||||
if (!formControl || !formControl->IsSingleLineTextControl(true))
|
||||
if (!formControl || !formControl->IsSingleLineTextControl(false))
|
||||
return;
|
||||
|
||||
bool isReadOnly = false;
|
||||
|
||||
Reference in New Issue
Block a user