--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2166,16 +2166,19 @@ pref("security.sri.enable", true);
pref("security.block_script_with_wrong_mime", true);
// Block images of wrong MIME for XCTO: nosniff.
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.
// This is to prevent accidental pinning from MITM devices and is used
// for tests.
pref("security.cert_pinning.process_headers_from_non_builtin_roots", false);
// If set to true, allow view-source URIs to be opened from URIs that share
--- a/toolkit/components/passwordmgr/LoginHelper.jsm
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -31,16 +31,17 @@ Cu.import("resource://gre/modules/XPCOMU
this.LoginHelper = {
/**
* Warning: these only update if a logger was created.
*/
debug: Services.prefs.getBoolPref("signon.debug"),
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 = () => {
return this.debug ? "debug" : "warn";
};
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
@@ -54,16 +55,20 @@ this.LoginHelper = {
Services.prefs.addObserver("signon.", () => {
this.debug = Services.prefs.getBoolPref("signon.debug");
this.formlessCaptureEnabled = Services.prefs.getBoolPref("signon.formlessCapture.enabled");
this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
this.insecureAutofill = Services.prefs.getBoolPref("signon.autofillForms.http");
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;
},
/**
* Due to the way the signons2.txt file is formatted, we need to make
* sure certain field values or characters do not cause the file to
* be parsed incorrectly. Reject hostnames that we can't store correctly.
*
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -5,17 +5,16 @@
"use strict";
this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
"LoginFormFactory",
"UserAutoCompleteResult" ];
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");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/InsecurePasswordUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
@@ -308,16 +307,17 @@ var LoginManagerContent = {
let requestData = {};
let messageData = { formOrigin: formOrigin,
actionOrigin: actionOrigin,
searchString: aSearchString,
previousResult: previousResult,
rect: aRect,
isSecure: InsecurePasswordUtils.isFormSecure(form),
+ isPasswordField: aElement.type == "password",
remote: remote };
return this._sendRequest(messageManager, requestData,
"RemoteLogins:autoCompleteLogins",
messageData);
},
setupProgressListener(window) {
@@ -969,16 +969,18 @@ var LoginManagerContent = {
// Need a valid password field to do anything.
if (passwordField == null) {
log("not filling form, no password field found");
recordAutofillResult(AUTOFILL_RESULT.NO_PASSWORD_FIELD);
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");
recordAutofillResult(AUTOFILL_RESULT.PASSWORD_DISABLED_READONLY);
return;
}
// Prevent autofilling insecure forms.
@@ -1022,18 +1024,19 @@ var LoginManagerContent = {
log("form not filled, none of the logins fit in the field");
recordAutofillResult(AUTOFILL_RESULT.NO_LOGINS_FIT);
return;
}
// 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) {
log("form not filled, the password field was already filled");
recordAutofillResult(AUTOFILL_RESULT.EXISTING_PASSWORD);
return;
}
@@ -1212,86 +1215,142 @@ var LoginUtils = {
if (uriString == "")
uriString = form.baseURI; // ala bug 297761
return this._getPasswordOrigin(uriString, true);
},
};
// nsIAutoCompleteResult implementation
-function UserAutoCompleteResult (aSearchString, matchingLogins, {isSecure, messageManager}) {
- function loginSort(a, b) {
- var userA = a.username.toLowerCase();
- var userB = b.username.toLowerCase();
+function UserAutoCompleteResult (aSearchString, matchingLogins, {isSecure, messageManager, isPasswordField}) {
+ this.searchString = aSearchString;
- if (userA < userB)
- return -1;
-
- if (userA > userB)
- return 1;
-
- return 0;
- }
+ this._stringBundle = Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+ this._dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
+ { day: "numeric", month: "short", year: "numeric" });
- let prefShowInsecureFieldWarning =
- Preferences.get(PREF_INSECURE_FIELD_WARNING_ENABLED, false);
+ this._messageManager = messageManager;
+ this._matchingLogins = matchingLogins;
+ this._isPasswordField = isPasswordField;
+ this._isSecure = isSecure;
- this._showInsecureFieldWarning = (!isSecure && prefShowInsecureFieldWarning) ? 1 : 0;
- this.searchString = aSearchString;
- this.logins = matchingLogins.sort(loginSort);
- this.matchCount = matchingLogins.length + this._showInsecureFieldWarning;
- this._messageManager = messageManager;
- this._stringBundle = Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+ Services.prefs.addObserver("security.insecure_field_warning.contextual.enabled",
+ this.updateWithPrefChange.bind(this), false);
- if (this.matchCount > 0) {
- this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
- this.defaultIndex = 0;
- }
+ Services.prefs.addObserver("signon.autofillForms.http",
+ this.updateWithPrefChange.bind(this), false);
+
+ this.updateWithPrefChange();
}
UserAutoCompleteResult.prototype = {
QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
Ci.nsISupportsWeakReference]),
// private
logins : null,
// Allow autoCompleteSearch to get at the JS object so it can
// modify some readonly properties for internal use.
get wrappedJSObject() {
return this;
},
+ updateWithPrefChange() {
+ function loginSort(a, b) {
+ var userA = a.username.toLowerCase();
+ var userB = b.username.toLowerCase();
+
+ if (userA < userB)
+ return -1;
+
+ if (userA > userB)
+ return 1;
+
+ return 0;
+ }
+
+ 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;
+ }
+
+ let currentMatchingLogins = (!LoginHelper.insecureAutofill && !this._isSecure) ?
+ [] : this._matchingLogins;
+
+ this._showInsecureFieldWarning = (!this._isSecure && LoginHelper.showInsecureFieldWarning) ? 1 : 0;
+ this.logins = currentMatchingLogins.sort(loginSort);
+ this.matchCount = currentMatchingLogins.length + this._showInsecureFieldWarning;
+ this._duplicateUsernames = findDuplicates(currentMatchingLogins);
+
+ if (this.matchCount > 0) {
+ this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ this.defaultIndex = 0;
+ }
+ },
+
// Interfaces from idl...
searchString : null,
searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
defaultIndex : -1,
errorDescription : "",
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) {
return "";
},
getStyleAt(index) {
if (index == 0 && this._showInsecureFieldWarning) {
@@ -1304,18 +1363,19 @@ UserAutoCompleteResult.prototype = {
return "";
},
getFinalCompleteValueAt(index) {
return this.getValueAt(index);
},
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.
return;
}
if (this._showInsecureFieldWarning) {
index--;
}
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -222,17 +222,18 @@ var LoginManagerParent = {
requestId: requestId,
logins: jsLogins,
recipes,
});
}),
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.
let searchStringLower = searchString.toLowerCase();
let logins;
if (previousResult &&
searchStringLower.startsWith(previousResult.searchString.toLowerCase())) {
log("Using previous autocomplete result");
@@ -255,17 +256,21 @@ var LoginManagerParent = {
];
logins = LoginHelper.dedupeLogins(logins, ["username"], resolveBy, formOrigin);
}
let matchingLogins = logins.filter(function(fullMatch) {
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);
});
// XXX In the E10S case, we're responsible for showing our own
// autocomplete popup here because the autocomplete protocol hasn't
// been e10s-ized yet. In the non-e10s case, our caller is responsible
// for showing the autocomplete popup (via the regular
// nsAutoCompleteController).
--- a/toolkit/components/passwordmgr/nsLoginManager.js
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -479,16 +479,21 @@ LoginManager.prototype = {
*/
autoCompleteSearchAsync(aSearchString, aPreviousResult,
aElement, aCallback) {
// aPreviousResult is an nsIAutoCompleteResult, aElement is
// nsIDOMHTMLInputElement
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() {
aCallback.onSearchCompletion(new UserAutoCompleteResult(aSearchString, [], {isSecure}));
}, 0);
return;
}
@@ -513,16 +518,17 @@ LoginManager.prototype = {
return;
}
this._autoCompleteLookupPromise = null;
let results =
new UserAutoCompleteResult(aSearchString, logins, {
messageManager,
isSecure,
+ isPasswordField,
});
aCallback.onSearchCompletion(results);
})
.then(null, Cu.reportError);
},
stopSearch() {
this._autoCompleteLookupPromise = null;
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -22,16 +22,18 @@ skip-if = toolkit == 'android' # Bug 125
[test_basic_form_1pw_2.html]
[test_basic_form_2pw_1.html]
[test_basic_form_2pw_2.html]
[test_basic_form_3pw_1.html]
[test_basic_form_autocomplete.html]
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]
[test_bug_627616.html]
skip-if = toolkit == 'android' # Tests desktop prompts
[test_bug_776171.html]
[test_case_differences.html]
skip-if = toolkit == 'android' # autocomplete
--- a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
@@ -204,29 +204,53 @@ function sendFakeAutocompleteEvent(eleme
element.dispatchEvent(acEvent);
}
function spinEventLoop() {
return Promise.resolve();
}
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();
});
add_task(function* test_form1_initial_empty() {
yield SimpleTest.promiseFocus(window);
// Make sure initial form is empty.
checkACForm("", "");
let popupState = yield getPopupState();
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
restoreForm();
let shownPromise = promiseACShown();
doKey("down"); // open
yield shownPromise;
--- a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
@@ -204,17 +204,18 @@ function sendFakeAutocompleteEvent(eleme
element.dispatchEvent(acEvent);
}
function spinEventLoop() {
return Promise.resolve();
}
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();
});
add_task(function* test_form1_initial_empty() {
yield SimpleTest.promiseFocus(window);
// Make sure initial form is empty.
checkACForm("", "");
@@ -223,21 +224,28 @@ add_task(function* test_form1_initial_em
});
add_task(function* test_form1_warning_entry() {
yield SimpleTest.promiseFocus(window);
// Trigger autocomplete popup
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"!
yield spinEventLoop(); // let focus happen
checkACForm("", "");
});
add_task(function* test_form1_first_entry() {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html
@@ -0,0 +1,275 @@
+<!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
+ is(SpecialPowers.getBoolPref("security.insecure_field_warning.contextual.enabled"), true, "verify pref security.insecure_field_warning.contextual.enabled");
+ is(SpecialPowers.getBoolPref("signon.autofillForms.http"), true, "verify pref signon.autofillForms.http");
+ 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
+ is(SpecialPowers.getBoolPref("security.insecure_field_warning.contextual.enabled"), false, "verify pref security.insecure_field_warning.contextual.enabled");
+ is(SpecialPowers.getBoolPref("signon.autofillForms.http"), true, "verify pref signon.autofillForms.http");
+ 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
+ is(SpecialPowers.getBoolPref("security.insecure_field_warning.contextual.enabled"), true, "verify pref security.insecure_field_warning.contextual.enabled");
+ is(SpecialPowers.getBoolPref("signon.autofillForms.http"), false, "verify pref signon.autofillForms.http");
+ 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
+ is(SpecialPowers.getBoolPref("security.insecure_field_warning.contextual.enabled"), false, "verify pref security.insecure_field_warning.contextual.enabled");
+ is(SpecialPowers.getBoolPref("signon.autofillForms.http"), false, "verify pref signon.autofillForms.http");
+ 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>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_user_autocomplete_result.js
@@ -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\./);
+ }
+ });
+});
--- a/toolkit/components/passwordmgr/test/unit/xpcshell.ini
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -26,16 +26,18 @@ run-if = buildapp == "browser"
[test_getPasswordFields.js]
[test_getPasswordOrigin.js]
[test_isOriginMatching.js]
[test_legacy_empty_formSubmitURL.js]
[test_legacy_validation.js]
[test_logins_change.js]
[test_logins_decrypt_failure.js]
skip-if = os == "android" # Bug 1171687: Needs fixing on Android
+[test_user_autocomplete_result.js]
+skip-if = os == "android"
[test_logins_metainfo.js]
[test_logins_search.js]
[test_notifications.js]
[test_OSCrypto_win.js]
skip-if = os != "win"
[test_recipes_add.js]
[test_recipes_content.js]
[test_search_schemeUpgrades.js]
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -211,17 +211,17 @@ nsFormFillController::NodeWillBeDestroye
mFocusedInput = nullptr;
}
}
void
nsFormFillController::MaybeRemoveMutationObserver(nsINode* aNode)
{
// Nodes being tracked in mPwmgrInputs will have their observers removed when
- // they stop being tracked.
+ // they stop being tracked.
if (!mPwmgrInputs.Get(aNode)) {
aNode->RemoveMutationObserver(this);
}
}
////////////////////////////////////////////////////////////////////////
//// nsIFormFillController
@@ -895,17 +895,17 @@ nsFormFillController::RemoveForDocument(
void
nsFormFillController::MaybeStartControllingInput(nsIDOMHTMLInputElement* aInput)
{
nsCOMPtr<nsINode> inputNode = do_QueryInterface(aInput);
if (!inputNode)
return;
nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(aInput);
- if (!formControl || !formControl->IsSingleLineTextControl(true))
+ if (!formControl || !formControl->IsSingleLineTextControl(false))
return;
bool isReadOnly = false;
aInput->GetReadOnly(&isReadOnly);
if (isReadOnly)
return;
bool autocomplete = nsContentUtils::IsAutocompleteEnabled(aInput);