--- a/toolkit/components/passwordmgr/LoginHelper.jsm
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -35,17 +35,17 @@ Cu.import("resource://gre/modules/XPCOMU
this.LoginHelper = {
/**
* Warning: this only updates if a logger was created.
*/
debug: Services.prefs.getBoolPref("signon.debug"),
createLogger(aLogPrefix) {
let getMaxLogLevel = () => {
- return this.debug ? "debug" : "error";
+ 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;
let consoleOptions = {
maxLogLevel: getMaxLogLevel(),
prefix: aLogPrefix,
};
@@ -129,16 +129,89 @@ this.LoginHelper = {
// eg host="foo (", realm="bar" --> "foo ( (bar)"
// vs host="foo", realm=" (bar" --> "foo ( (bar)"
if (aLogin.hostname.indexOf(" (") != -1) {
throw new Error("bad parens in hostname");
}
},
/**
+ * Returns a new XPCOM property bag with the provided properties.
+ *
+ * @param {Object} aProperties
+ * Each property of this object is copied to the property bag. This
+ * parameter can be omitted to return an empty property bag.
+ *
+ * @return A new property bag, that is an instance of nsIWritablePropertyBag,
+ * nsIWritablePropertyBag2, nsIPropertyBag, and nsIPropertyBag2.
+ */
+ newPropertyBag(aProperties) {
+ let propertyBag = Cc["@mozilla.org/hash-property-bag;1"]
+ .createInstance(Ci.nsIWritablePropertyBag);
+ if (aProperties) {
+ for (let [name, value] of Iterator(aProperties)) {
+ propertyBag.setProperty(name, value);
+ }
+ }
+ return propertyBag.QueryInterface(Ci.nsIPropertyBag)
+ .QueryInterface(Ci.nsIPropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag2);
+ },
+
+ /**
+ * Helper to avoid the `count` argument and property bags when calling
+ * Services.logins.searchLogins from JS.
+ *
+ * @param {Object} aSearchOptions - A regular JS object to copy to a property bag before searching
+ * @return {nsILoginInfo[]} - The result of calling searchLogins.
+ */
+ searchLoginsWithObject(aSearchOptions) {
+ return Services.logins.searchLogins({}, this.newPropertyBag(aSearchOptions));
+ },
+
+ /**
+ * @param {String} aLoginOrigin - An origin value from a stored login's
+ * hostname or formSubmitURL properties.
+ * @param {String} aSearchOrigin - The origin that was are looking to match
+ * with aLoginOrigin. This would normally come
+ * from a form or page that we are considering.
+ * @param {nsILoginFindOptions} aOptions - Options to affect whether the origin
+ * from the login (aLoginOrigin) is a
+ * match for the origin we're looking
+ * for (aSearchOrigin).
+ */
+ isOriginMatching(aLoginOrigin, aSearchOrigin, aOptions = {
+ schemeUpgrades: false,
+ }) {
+ if (aLoginOrigin == aSearchOrigin) {
+ return true;
+ }
+
+ if (!aOptions) {
+ return false;
+ }
+
+ if (aOptions.schemeUpgrades) {
+ try {
+ let loginURI = Services.io.newURI(aLoginOrigin, null, null);
+ let searchURI = Services.io.newURI(aSearchOrigin, null, null);
+ if (loginURI.scheme == "http" && searchURI.scheme == "https" &&
+ loginURI.hostPort == searchURI.hostPort) {
+ return true;
+ }
+ } catch (ex) {
+ // newURI will throw for some values e.g. chrome://FirefoxAccounts
+ return false;
+ }
+ }
+
+ return false;
+ },
+
+ /**
* Creates a new login object that results by modifying the given object with
* the provided data.
*
* @param aOldStoredLogin
* Existing nsILoginInfo object to modify.
* @param aNewLoginData
* The new login values, either as nsILoginInfo or nsIProperyBag.
*
--- a/toolkit/components/passwordmgr/LoginRecipes.jsm
+++ b/toolkit/components/passwordmgr/LoginRecipes.jsm
@@ -245,17 +245,17 @@ var LoginRecipesContent = {
* @return {HTMLElement|null}
*/
queryLoginField(aParent, aSelector) {
if (!aSelector) {
return null;
}
let field = aParent.ownerDocument.querySelector(aSelector);
if (!field) {
- log.warn("Login field selector wasn't matched:", aSelector);
+ log.debug("Login field selector wasn't matched:", aSelector);
return null;
}
if (!(field instanceof aParent.ownerDocument.defaultView.HTMLInputElement)) {
log.warn("Login field isn't an <input> so ignoring it:", aSelector);
return null;
}
return field;
},
--- a/toolkit/components/passwordmgr/nsLoginManager.js
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -374,16 +374,25 @@ LoginManager.prototype = {
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
* JavaScript object and decrypt the results.
*
* @return {nsILoginInfo[]} which are decrypted.
*/
searchLogins(count, matchData) {
log.debug("Searching for logins");
+ matchData.QueryInterface(Ci.nsIPropertyBag2);
+ if (!matchData.hasKey("hostname")) {
+ log.warn("searchLogins: A `hostname` is recommended");
+ }
+
+ if (!matchData.hasKey("formSubmitURL") && !matchData.hasKey("httpRealm")) {
+ log.warn("searchLogins: `formSubmitURL` or `httpRealm` is recommended");
+ }
+
return this._storage.searchLogins(count, matchData);
},
/**
* Search for the known logins for entries matching the specified criteria,
* returns only the count.
*/
--- a/toolkit/components/passwordmgr/storage-json.js
+++ b/toolkit/components/passwordmgr/storage-json.js
@@ -239,74 +239,98 @@ this.LoginManagerStorage_json.prototype
/**
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
* JavaScript object and decrypt the results.
*
* @return {nsILoginInfo[]} which are decrypted.
*/
searchLogins(count, matchData) {
let realMatchData = {};
+ let options = {};
// Convert nsIPropertyBag to normal JS object
let propEnum = matchData.enumerator;
while (propEnum.hasMoreElements()) {
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
- realMatchData[prop.name] = prop.value;
+ switch (prop.name) {
+ // Some property names aren't field names but are special options to affect the search.
+ case "schemeUpgrades": {
+ options[prop.name] = prop.value;
+ break;
+ }
+ default: {
+ realMatchData[prop.name] = prop.value;
+ break;
+ }
+ }
}
- let [logins, ids] = this._searchLogins(realMatchData);
+ let [logins, ids] = this._searchLogins(realMatchData, options);
// Decrypt entries found for the caller.
logins = this._decryptLogins(logins);
count.value = logins.length; // needed for XPCOM
return logins;
},
/**
* Private method to perform arbitrary searches on any field. Decryption is
* left to the caller.
*
* Returns [logins, ids] for logins that match the arguments, where logins
* is an array of encrypted nsLoginInfo and ids is an array of associated
* ids in the database.
*/
- _searchLogins(matchData) {
+ _searchLogins(matchData, aOptions = {
+ schemeUpgrades: false,
+ }) {
this._store.ensureDataReady();
let conditions = [];
function match(aLogin) {
for (let field in matchData) {
- let value = matchData[field];
+ let wantedValue = matchData[field];
switch (field) {
- // Historical compatibility requires this special case
case "formSubmitURL":
- if (value != null) {
- if (aLogin.formSubmitURL != "" && aLogin.formSubmitURL != value) {
+ if (wantedValue != null) {
+ // Historical compatibility requires this special case
+ if (aLogin.formSubmitURL == "") {
+ break;
+ }
+ if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
return false;
}
break;
}
+ // fall through
+ case "hostname":
+ if (wantedValue != null) { // needed for formSubmitURL fall through
+ if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
+ return false;
+ }
+ break;
+ }
+ // fall through
// Normal cases.
- case "hostname":
case "httpRealm":
case "id":
case "usernameField":
case "passwordField":
case "encryptedUsername":
case "encryptedPassword":
case "guid":
case "encType":
case "timeCreated":
case "timeLastUsed":
case "timePasswordChanged":
case "timesUsed":
- if (value == null && aLogin[field]) {
+ if (wantedValue == null && aLogin[field]) {
return false;
- } else if (aLogin[field] != value) {
+ } else if (aLogin[field] != wantedValue) {
return false;
}
break;
// Fail if caller requests an unknown property.
default:
throw new Error("Unexpected field: " + field);
}
}
@@ -330,17 +354,17 @@ this.LoginManagerStorage_json.prototype
login.timeLastUsed = loginItem.timeLastUsed;
login.timePasswordChanged = loginItem.timePasswordChanged;
login.timesUsed = loginItem.timesUsed;
foundLogins.push(login);
foundIds.push(loginItem.id);
}
}
- this.log("_searchLogins: returning", foundLogins.length, "logins");
+ this.log("_searchLogins: returning", foundLogins.length, "logins for", matchData);
return [foundLogins, foundIds];
},
/**
* Removes all logins from storage.
*
* Disabled hosts are kept, as one presumably doesn't want to erase those.
*/
--- a/toolkit/components/passwordmgr/storage-mozStorage.js
+++ b/toolkit/components/passwordmgr/storage-mozStorage.js
@@ -416,91 +416,120 @@ LoginManagerStorage_mozStorage.prototype
this.log("_getAllLogins: returning " + logins.length + " logins.");
if (count)
count.value = logins.length; // needed for XPCOM
return logins;
},
- /*
- * searchLogins
- *
+ /**
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
* JavaScript object and decrypt the results.
*
- * Returns an array of decrypted nsILoginInfo.
+ * @return {nsILoginInfo[]} which are decrypted.
*/
searchLogins : function(count, matchData) {
let realMatchData = {};
+ let options = {};
// Convert nsIPropertyBag to normal JS object
let propEnum = matchData.enumerator;
while (propEnum.hasMoreElements()) {
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
- realMatchData[prop.name] = prop.value;
+ switch (prop.name) {
+ // Some property names aren't field names but are special options to affect the search.
+ case "schemeUpgrades": {
+ options[prop.name] = prop.value;
+ break;
+ }
+ default: {
+ realMatchData[prop.name] = prop.value;
+ break;
+ }
+ }
}
- let [logins, ids] = this._searchLogins(realMatchData);
+ let [logins, ids] = this._searchLogins(realMatchData, options);
// Decrypt entries found for the caller.
logins = this._decryptLogins(logins);
count.value = logins.length; // needed for XPCOM
return logins;
},
- /*
- * _searchLogins
- *
+ /**
* Private method to perform arbitrary searches on any field. Decryption is
* left to the caller.
*
* Returns [logins, ids] for logins that match the arguments, where logins
* is an array of encrypted nsLoginInfo and ids is an array of associated
* ids in the database.
*/
- _searchLogins : function (matchData) {
+ _searchLogins : function (matchData, aOptions = {
+ schemeUpgrades: false,
+ }) {
let conditions = [], params = {};
for (let field in matchData) {
let value = matchData[field];
+ let condition = "";
switch (field) {
- // Historical compatibility requires this special case
case "formSubmitURL":
if (value != null) {
- conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
- params["formSubmitURL"] = value;
- break;
+ // Historical compatibility requires this special case
+ condition = "formSubmitURL = '' OR ";
}
+ // Fall through
+ case "hostname":
+ if (value != null) {
+ condition += `${field} = :${field}`;
+ params[field] = value;
+ let valueURI;
+ try {
+ if (aOptions.schemeUpgrades && (valueURI = Services.io.newURI(value, null, null)) &&
+ valueURI.scheme == "https") {
+ condition += ` OR ${field} = :http${field}`;
+ params["http" + field] = "http://" + valueURI.hostPort;
+ }
+ } catch (ex) {
+ // newURI will throw for some values (e.g. chrome://FirefoxAccounts)
+ // but those URLs wouldn't support upgrades anyways.
+ }
+ break;
+ }
+ // Fall through
// Normal cases.
- case "hostname":
case "httpRealm":
case "id":
case "usernameField":
case "passwordField":
case "encryptedUsername":
case "encryptedPassword":
case "guid":
case "encType":
case "timeCreated":
case "timeLastUsed":
case "timePasswordChanged":
case "timesUsed":
if (value == null) {
- conditions.push(field + " isnull");
+ condition = field + " isnull";
} else {
- conditions.push(field + " = :" + field);
- params[field] = value;
+ condition = field + " = :" + field;
+ params[field] = value;
}
break;
// Fail if caller requests an unknown property.
default:
throw new Error("Unexpected field: " + field);
}
+ if (condition) {
+ conditions.push(condition);
+ }
}
// Build query
let query = "SELECT * FROM moz_logins";
if (conditions.length) {
conditions = conditions.map(c => "(" + c + ")");
query += " WHERE " + conditions.join(" AND ");
}
--- a/toolkit/components/passwordmgr/test/unit/head.js
+++ b/toolkit/components/passwordmgr/test/unit/head.js
@@ -1,23 +1,18 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ts=2 et sw=2 tw=80: */
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
/**
* Provides infrastructure for automated login components tests.
*/
"use strict";
////////////////////////////////////////////////////////////////////////////////
//// Globals
-var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/LoginRecipes.jsm");
Cu.import("resource://gre/modules/LoginHelper.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
@@ -32,16 +27,17 @@ const LoginInfo =
Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
"nsILoginInfo", "init");
// Import LoginTestUtils.jsm as LoginTestUtils.
XPCOMUtils.defineLazyModuleGetter(this, "LoginTestUtils",
"resource://testing-common/LoginTestUtils.jsm");
LoginTestUtils.Assert = Assert;
const TestData = LoginTestUtils.testData;
+const newPropertyBag = LoginHelper.newPropertyBag;
/**
* All the tests are implemented with add_task, this starts them automatically.
*/
function run_test()
{
do_get_profile();
run_next_test();
@@ -52,17 +48,17 @@ function run_test()
// Some of these functions are already implemented in other parts of the source
// tree, see bug 946708 about sharing more code.
// While the previous test file should have deleted all the temporary files it
// used, on Windows these might still be pending deletion on the physical file
// system. Thus, start from a new base number every time, to make a collision
// with a file that is still pending deletion highly unlikely.
-var gFileCounter = Math.floor(Math.random() * 1000000);
+let gFileCounter = Math.floor(Math.random() * 1000000);
/**
* Returns a reference to a temporary file, that is guaranteed not to exist, and
* to have never been created before.
*
* @param aLeafName
* Suggested leaf name for the file to be created.
*
@@ -88,40 +84,16 @@ function getTempFile(aLeafName)
if (file.exists()) {
file.remove(false);
}
});
return file;
}
-/**
- * Returns a new XPCOM property bag with the provided properties.
- *
- * @param aProperties
- * Each property of this object is copied to the property bag. This
- * parameter can be omitted to return an empty property bag.
- *
- * @return A new property bag, that is an instance of nsIWritablePropertyBag,
- * nsIWritablePropertyBag2, nsIPropertyBag, and nsIPropertyBag2.
- */
-function newPropertyBag(aProperties)
-{
- let propertyBag = Cc["@mozilla.org/hash-property-bag;1"]
- .createInstance(Ci.nsIWritablePropertyBag);
- if (aProperties) {
- for (let [name, value] of Iterator(aProperties)) {
- propertyBag.setProperty(name, value);
- }
- }
- return propertyBag.QueryInterface(Ci.nsIPropertyBag)
- .QueryInterface(Ci.nsIPropertyBag2)
- .QueryInterface(Ci.nsIWritablePropertyBag2);
-}
-
////////////////////////////////////////////////////////////////////////////////
const RecipeHelpers = {
initNewParent() {
return (new LoginRecipesParent({ defaults: null })).initializationPromise;
},
};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_isOriginMatching.js
@@ -0,0 +1,40 @@
+/*
+ * Test LoginHelper.isOriginMatching
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+
+add_task(function test_isOriginMatching() {
+ let testcases = [
+ // Index 0 holds the expected return value followed by arguments to isOriginMatching.
+ [true, "http://example.com", "http://example.com"],
+ [true, "http://example.com:8080", "http://example.com:8080"],
+ [true, "https://example.com", "https://example.com"],
+ [true, "https://example.com:8443", "https://example.com:8443"],
+ [false, "http://example.com", "http://mozilla.org"],
+ [false, "http://example.com", "http://example.com:8080"],
+ [false, "https://example.com", "http://example.com"],
+ [false, "https://example.com", "https://mozilla.org"],
+ [false, "http://example.com", "http://sub.example.com"],
+ [false, "https://example.com", "https://sub.example.com"],
+ [false, "http://example.com", "https://example.com:8443"],
+ [false, "http://example.com:8080", "http://example.com:8081"],
+ [false, "http://example.com", ""],
+ [false, "", "http://example.com"],
+ [true, "http://example.com", "https://example.com", { schemeUpgrades: true }],
+ [true, "https://example.com", "https://example.com", { schemeUpgrades: true }],
+ [true, "http://example.com:8080", "http://example.com:8080", { schemeUpgrades: true }],
+ [true, "https://example.com:8443", "https://example.com:8443", { schemeUpgrades: true }],
+ [false, "https://example.com", "http://example.com", { schemeUpgrades: true }], // downgrade
+ [false, "http://example.com:8080", "https://example.com", { schemeUpgrades: true }], // port mismatch
+ [false, "http://example.com", "https://example.com:8443", { schemeUpgrades: true }], // port mismatch
+ [false, "http://sub.example.com", "http://example.com", { schemeUpgrades: true }],
+ ];
+ for (let tc of testcases) {
+ let expected = tc.shift();
+ Assert.strictEqual(LoginHelper.isOriginMatching(...tc), expected,
+ "Check " + JSON.stringify(tc));
+ }
+});
--- a/toolkit/components/passwordmgr/test/unit/test_logins_search.js
+++ b/toolkit/components/passwordmgr/test/unit/test_logins_search.js
@@ -1,14 +1,9 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ts=2 et sw=2 tw=80: */
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
+/*
* Tests methods that find specific logins in the store (findLogins,
* searchLogins, and countLogins).
*
* The getAllLogins method is not tested explicitly here, because it is used by
* all tests to verify additions, removals and modifications to the login store.
*/
"use strict";
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_search_schemeUpgrades.js
@@ -0,0 +1,184 @@
+/*
+ * Test Services.logins.searchLogins with the `schemeUpgrades` property.
+ */
+
+const HTTP3_ORIGIN = "http://www3.example.com";
+const HTTPS_ORIGIN = "https://www.example.com";
+const HTTP_ORIGIN = "http://www.example.com";
+
+/**
+ * Returns a list of new nsILoginInfo objects that are a subset of the test
+ * data, built to match the specified query.
+ *
+ * @param {Object} aQuery
+ * Each property and value of this object restricts the search to those
+ * entries from the test data that match the property exactly.
+ */
+function buildExpectedLogins(aQuery) {
+ return TestData.loginList().filter(
+ entry => Object.keys(aQuery).every(name => {
+ if (name == "schemeUpgrades") {
+ return true;
+ }
+ if (["hostname", "formSubmitURL"].includes(name)) {
+ return LoginHelper.isOriginMatching(entry[name], aQuery[name], {
+ schemeUpgrades: aQuery.schemeUpgrades,
+ });
+ }
+ return entry[name] === aQuery[name];
+ }));
+}
+
+/**
+ * Tests the searchLogins function.
+ *
+ * @param {Object} aQuery
+ * Each property and value of this object is translated to an entry in
+ * the nsIPropertyBag parameter of searchLogins.
+ * @param {Number} aExpectedCount
+ * Number of logins from the test data that should be found. The actual
+ * list of logins is obtained using the buildExpectedLogins helper, and
+ * this value is just used to verify that modifications to the test data
+ * don't make the current test meaningless.
+ */
+function checkSearch(aQuery, aExpectedCount) {
+ do_print("Testing searchLogins for " + JSON.stringify(aQuery));
+
+ let expectedLogins = buildExpectedLogins(aQuery);
+ do_check_eq(expectedLogins.length, aExpectedCount);
+
+ let outCount = {};
+ let logins = Services.logins.searchLogins(outCount, newPropertyBag(aQuery));
+ do_check_eq(outCount.value, expectedLogins.length);
+ LoginTestUtils.assertLoginListsEqual(logins, expectedLogins);
+}
+
+/**
+ * Prepare data for the following tests.
+ */
+add_task(function test_initialize() {
+ for (let login of TestData.loginList()) {
+ Services.logins.addLogin(login);
+ }
+});
+
+/**
+ * Tests searchLogins with the `schemeUpgrades` property
+ */
+add_task(function test_search_schemeUpgrades_hostname() {
+ // Hostname-only
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ }, 1);
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: false,
+ }, 1);
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: undefined,
+ }, 1);
+ checkSearch({
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ }, 2);
+});
+
+/**
+ * Same as above but replacing hostname with formSubmitURL.
+ */
+add_task(function test_search_schemeUpgrades_formSubmitURL() {
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ schemeUpgrades: false,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ schemeUpgrades: undefined,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ }, 4);
+});
+
+
+add_task(function test_search_schemeUpgrades_hostname_formSubmitURL() {
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: false,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: undefined,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ schemeUpgrades: true,
+ usernameField: "form_field_username",
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ passwordField: "form_field_password",
+ schemeUpgrades: true,
+ usernameField: "form_field_username",
+ }, 2);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTPS_ORIGIN,
+ httpRealm: null,
+ passwordField: "form_field_password",
+ schemeUpgrades: true,
+ usernameField: "form_field_username",
+ }, 2);
+});
+
+/**
+ * HTTP submitting to HTTPS
+ */
+add_task(function test_http_to_https() {
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTP3_ORIGIN,
+ httpRealm: null,
+ schemeUpgrades: false,
+ }, 1);
+ checkSearch({
+ formSubmitURL: HTTPS_ORIGIN,
+ hostname: HTTP3_ORIGIN,
+ httpRealm: null,
+ schemeUpgrades: true,
+ }, 2);
+});
+
+/**
+ * schemeUpgrades shouldn't cause downgrades
+ */
+add_task(function test_search_schemeUpgrades_downgrade() {
+ checkSearch({
+ formSubmitURL: HTTP_ORIGIN,
+ hostname: HTTP_ORIGIN,
+ }, 1);
+ do_print("The same number should be found with schemeUpgrades since we're searching for HTTP");
+ checkSearch({
+ formSubmitURL: HTTP_ORIGIN,
+ hostname: HTTP_ORIGIN,
+ schemeUpgrades: true,
+ }, 1);
+});
--- a/toolkit/components/passwordmgr/test/unit/xpcshell.ini
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -20,22 +20,24 @@ skip-if = true || os != "android" # Bug
[test_context_menu.js]
skip-if = os == "android" # The context menu isn't used on Android.
# LoginManagerContextMenu is only included for MOZ_BUILD_APP == 'browser'.
run-if = buildapp == "browser"
[test_disabled_hosts.js]
[test_getFormFields.js]
[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_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]
[test_storage.js]
[test_telemetry.js]