mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-28 15:23:51 +00:00
Bug 1699705 - Allow autocomplete to use related realm credentials. r=sfoster,leplatrem
Differential Revision: https://phabricator.services.mozilla.com/D91205
This commit is contained in:
parent
3f5687ca08
commit
20c7eb2867
@ -1935,6 +1935,7 @@ pref("signon.management.page.breachAlertUrl",
|
||||
"https://monitor.firefox.com/breach-details/");
|
||||
pref("signon.management.page.showPasswordSyncNotification", true);
|
||||
pref("signon.passwordEditCapture.enabled", true);
|
||||
pref("signon.relatedRealms.enabled", false);
|
||||
pref("signon.showAutoCompleteFooter", true);
|
||||
pref("signon.showAutoCompleteImport", "import");
|
||||
pref("signon.suggestImportCount", 3);
|
||||
|
@ -3675,6 +3675,7 @@ pref("signon.userInputRequiredToCapture.enabled", true);
|
||||
pref("signon.debug", false);
|
||||
pref("signon.recipes.path", "resource://app/defaults/settings/main/password-recipes.json");
|
||||
pref("signon.recipes.remoteRecipesEnabled", true);
|
||||
pref("signon.relatedRealms.enabled", false);
|
||||
|
||||
pref("signon.schemeUpgrades", true);
|
||||
pref("signon.includeOtherSubdomainsInLookup", true);
|
||||
|
@ -371,6 +371,8 @@ this.LoginHelper = {
|
||||
privateBrowsingCaptureEnabled: null,
|
||||
remoteRecipesEnabled: null,
|
||||
remoteRecipesCollection: "password-recipes",
|
||||
relatedRealmsEnabled: null,
|
||||
relatedRealmsCollection: "websites-with-shared-credential-backends",
|
||||
schemeUpgrades: null,
|
||||
showAutoCompleteFooter: null,
|
||||
showAutoCompleteImport: null,
|
||||
@ -470,6 +472,9 @@ this.LoginHelper = {
|
||||
this.remoteRecipesEnabled = Services.prefs.getBoolPref(
|
||||
"signon.recipes.remoteRecipesEnabled"
|
||||
);
|
||||
this.relatedRealmsEnabled = Services.prefs.getBoolPref(
|
||||
"signon.relatedRealms.enabled"
|
||||
);
|
||||
},
|
||||
|
||||
createLogger(aLogPrefix) {
|
||||
@ -682,6 +687,8 @@ this.LoginHelper = {
|
||||
schemeUpgrades: false,
|
||||
acceptWildcardMatch: false,
|
||||
acceptDifferentSubdomains: false,
|
||||
acceptRelatedRealms: false,
|
||||
relatedRealms: [],
|
||||
}
|
||||
) {
|
||||
if (aLoginOrigin == aSearchOrigin) {
|
||||
@ -718,6 +725,18 @@ this.LoginHelper = {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
aOptions.acceptRelatedRealms &&
|
||||
aOptions.relatedRealms.length &&
|
||||
(loginURI.scheme == searchURI.scheme ||
|
||||
(aOptions.schemeUpgrades && schemeMatches))
|
||||
) {
|
||||
for (let relatedOrigin of aOptions.relatedRealms) {
|
||||
if (Services.eTLD.hasRootDomain(loginURI.host, relatedOrigin)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -322,7 +322,6 @@ LoginManager.prototype = {
|
||||
if (matchingLogin) {
|
||||
throw LoginHelper.createLoginAlreadyExistsError(matchingLogin.guid);
|
||||
}
|
||||
|
||||
log.debug("Adding login");
|
||||
return this._storage.addLogin(login);
|
||||
},
|
||||
|
@ -22,6 +22,13 @@ XPCOMUtils.defineLazyGetter(this, "autocompleteFeature", () => {
|
||||
return new ExperimentFeature("password-autocomplete");
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "LoginRelatedRealmsParent", () => {
|
||||
const { LoginRelatedRealmsParent } = ChromeUtils.import(
|
||||
"resource://gre/modules/LoginRelatedRealms.jsm"
|
||||
);
|
||||
return new LoginRelatedRealmsParent();
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
@ -186,6 +193,7 @@ class LoginManagerParent extends JSWindowActorParent {
|
||||
* @param {origin?} options.httpRealm To match on. Omit this argument to match all realms.
|
||||
* @param {boolean} options.acceptDifferentSubdomains Include results for eTLD+1 matches
|
||||
* @param {boolean} options.ignoreActionAndRealm Include all form and HTTP auth logins for the site
|
||||
* @param {string[]} options.relatedRealms Related realms to match against when searching
|
||||
*/
|
||||
static async searchAndDedupeLogins(
|
||||
formOrigin,
|
||||
@ -194,6 +202,7 @@ class LoginManagerParent extends JSWindowActorParent {
|
||||
formActionOrigin,
|
||||
httpRealm,
|
||||
ignoreActionAndRealm,
|
||||
relatedRealms,
|
||||
} = {}
|
||||
) {
|
||||
let logins;
|
||||
@ -209,6 +218,10 @@ class LoginManagerParent extends JSWindowActorParent {
|
||||
matchData.httpRealm = httpRealm;
|
||||
}
|
||||
}
|
||||
if (LoginHelper.relatedRealmsEnabled) {
|
||||
matchData.acceptRelatedRealms = LoginHelper.relatedRealmsEnabled;
|
||||
matchData.relatedRealms = relatedRealms;
|
||||
}
|
||||
try {
|
||||
logins = await Services.logins.searchLoginsAsync(matchData);
|
||||
} catch (e) {
|
||||
@ -535,11 +548,22 @@ class LoginManagerParent extends JSWindowActorParent {
|
||||
origin: formOrigin,
|
||||
});
|
||||
} else {
|
||||
let relatedRealmsOrigins = [];
|
||||
if (LoginHelper.relatedRealmsEnabled) {
|
||||
relatedRealmsOrigins = await LoginRelatedRealmsParent.findRelatedRealms(
|
||||
formOrigin
|
||||
);
|
||||
}
|
||||
logins = await LoginManagerParent.searchAndDedupeLogins(formOrigin, {
|
||||
formActionOrigin: actionOrigin,
|
||||
ignoreActionAndRealm: true,
|
||||
acceptDifferentSubdomains: LoginHelper.includeOtherSubdomainsInLookup,
|
||||
relatedRealms: relatedRealmsOrigins,
|
||||
});
|
||||
debug(
|
||||
"Adding related logins on page load",
|
||||
logins.map(l => l.origin)
|
||||
);
|
||||
}
|
||||
|
||||
log("sendLoginDataToChild:", logins.length, "deduped logins");
|
||||
@ -606,12 +630,18 @@ class LoginManagerParent extends JSWindowActorParent {
|
||||
logins = LoginHelper.vanillaObjectsToLogins(previousResult.logins);
|
||||
} else {
|
||||
log("Creating new autocomplete search result.");
|
||||
|
||||
let relatedRealmsOrigins = [];
|
||||
if (LoginHelper.relatedRealmsEnabled) {
|
||||
relatedRealmsOrigins = await LoginRelatedRealmsParent.findRelatedRealms(
|
||||
formOrigin
|
||||
);
|
||||
}
|
||||
// Autocomplete results do not need to match actionOrigin or exact origin.
|
||||
logins = await LoginManagerParent.searchAndDedupeLogins(formOrigin, {
|
||||
formActionOrigin: actionOrigin,
|
||||
ignoreActionAndRealm: true,
|
||||
acceptDifferentSubdomains: LoginHelper.includeOtherSubdomainsInLookup,
|
||||
relatedRealms: relatedRealmsOrigins,
|
||||
});
|
||||
}
|
||||
|
||||
|
118
toolkit/components/passwordmgr/LoginRelatedRealms.jsm
Normal file
118
toolkit/components/passwordmgr/LoginRelatedRealms.jsm
Normal file
@ -0,0 +1,118 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
LoginHelper: "resource://gre/modules/LoginHelper.jsm",
|
||||
RemoteSettings: "resource://services-settings/remote-settings.js",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
let logger = LoginHelper.createLogger("LoginRelatedRealms");
|
||||
return logger;
|
||||
});
|
||||
|
||||
const EXPORTED_SYMBOLS = ["LoginRelatedRealmsParent"];
|
||||
|
||||
class LoginRelatedRealmsParent extends JSWindowActorParent {
|
||||
/**
|
||||
* @type RemoteSettingsClient
|
||||
*
|
||||
* @memberof LoginRelatedRealmsParent
|
||||
*/
|
||||
_sharedCredentialsClient = null;
|
||||
/**
|
||||
* @type string[][]
|
||||
*
|
||||
* @memberof LoginRelatedRealmsParent
|
||||
*/
|
||||
_relatedDomainsList = [[]];
|
||||
|
||||
/**
|
||||
* Handles the Remote Settings sync event
|
||||
*
|
||||
* @param {Object} aEvent
|
||||
* @param {Array} aEvent.current Records that are currently in the collection after the sync event
|
||||
* @param {Array} aEvent.created Records that were created
|
||||
* @param {Array} aEvent.updated Records that were updated
|
||||
* @param {Array} aEvent.deleted Records that were deleted
|
||||
* @memberof LoginRelatedRealmsParent
|
||||
*/
|
||||
onRemoteSettingsSync(aEvent) {
|
||||
let {
|
||||
data: { current },
|
||||
} = aEvent;
|
||||
this._relatedDomainsList = current;
|
||||
}
|
||||
|
||||
async getSharedCredentialsCollection() {
|
||||
if (!this._sharedCredentialsClient) {
|
||||
this._sharedCredentialsClient = RemoteSettings(
|
||||
LoginHelper.relatedRealmsCollection
|
||||
);
|
||||
this._sharedCredentialsClient.on("sync", event =>
|
||||
this.onRemoteSettingsSync(event)
|
||||
);
|
||||
this._relatedDomainsList = await this._sharedCredentialsClient.get();
|
||||
log.debug("Initialized related realms", this._relatedDomainsList);
|
||||
}
|
||||
log.debug("this._relatedDomainsList", this._relatedDomainsList);
|
||||
return this._relatedDomainsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if there are any related realms of this `formOrigin` using the related realms collection
|
||||
* @param {string} formOrigin A form origin
|
||||
* @return {string[]} filteredRealms An array of domains related to the `formOrigin`
|
||||
* @async
|
||||
* @memberof LoginRelatedRealmsParent
|
||||
*/
|
||||
async findRelatedRealms(formOrigin) {
|
||||
try {
|
||||
let formOriginURI = Services.io.newURI(formOrigin);
|
||||
let originDomain = formOriginURI.host;
|
||||
let [
|
||||
{ relatedRealms } = {},
|
||||
] = await this.getSharedCredentialsCollection();
|
||||
if (!relatedRealms) {
|
||||
return [];
|
||||
}
|
||||
let filterOriginIndex;
|
||||
let shouldInclude = false;
|
||||
let filteredRealms = relatedRealms.filter(_realms => {
|
||||
for (let relatedOrigin of _realms) {
|
||||
// We can't have an origin that matches multiple entries in our related realms collection
|
||||
// so we exit the loop early
|
||||
if (shouldInclude) {
|
||||
return false;
|
||||
}
|
||||
if (Services.eTLD.hasRootDomain(originDomain, relatedOrigin)) {
|
||||
shouldInclude = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return shouldInclude;
|
||||
});
|
||||
// * Filtered realms is a nested array due to its structure in Remote Settings
|
||||
filteredRealms = filteredRealms.flat();
|
||||
|
||||
filterOriginIndex = filteredRealms.indexOf(originDomain);
|
||||
// Removing the current formOrigin match if it exists in the related realms
|
||||
// so that we don't return duplicates when we search for logins
|
||||
if (filterOriginIndex !== -1) {
|
||||
filteredRealms.splice(filterOriginIndex, 1);
|
||||
}
|
||||
return filteredRealms;
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
@ -41,6 +41,7 @@ EXTRA_JS_MODULES += [
|
||||
"LoginManagerParent.jsm",
|
||||
"LoginManagerPrompter.jsm",
|
||||
"LoginRecipes.jsm",
|
||||
"LoginRelatedRealms.jsm",
|
||||
"NewPasswordModel.jsm",
|
||||
"OSCrypto.jsm",
|
||||
"PasswordGenerator.jsm",
|
||||
|
@ -475,7 +475,9 @@ class LoginManagerStorage_json {
|
||||
// Some property names aren't field names but are special options to
|
||||
// affect the search.
|
||||
case "acceptDifferentSubdomains":
|
||||
case "schemeUpgrades": {
|
||||
case "schemeUpgrades":
|
||||
case "acceptRelatedRealms":
|
||||
case "relatedRealms": {
|
||||
options[prop.name] = prop.value;
|
||||
break;
|
||||
}
|
||||
@ -508,6 +510,8 @@ class LoginManagerStorage_json {
|
||||
aOptions = {
|
||||
schemeUpgrades: false,
|
||||
acceptDifferentSubdomains: false,
|
||||
acceptRelatedRealms: false,
|
||||
relatedRealms: [],
|
||||
},
|
||||
candidateLogins = this._store.data.logins
|
||||
) {
|
||||
|
@ -9,6 +9,14 @@
|
||||
|
||||
const EXPORTED_SYMBOLS = ["LoginTestUtils"];
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
RemoteSettings: "resource://services-settings/remote-settings.js",
|
||||
});
|
||||
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
let { Assert: AssertCls } = ChromeUtils.import(
|
||||
@ -616,3 +624,27 @@ LoginTestUtils.file = {
|
||||
return tmpFile;
|
||||
},
|
||||
};
|
||||
|
||||
LoginTestUtils.remoteSettings = {
|
||||
relatedRealmsCollection: "websites-with-shared-credential-backends",
|
||||
async setupWebsitesWithSharedCredentials(
|
||||
relatedRealms = [["other-example.com", "example.com", "example.co.uk"]]
|
||||
) {
|
||||
let db = await RemoteSettings(this.relatedRealmsCollection).db;
|
||||
await db.clear();
|
||||
await db.create({
|
||||
id: "some-fake-ID-abc",
|
||||
relatedRealms,
|
||||
});
|
||||
await db.importChanges({}, 1234567);
|
||||
},
|
||||
async cleanWebsitesWithSharedCredentials() {
|
||||
let db = await RemoteSettings(this.relatedRealmsCollection).db;
|
||||
await db.clear();
|
||||
await db.importChanges({}, 1234);
|
||||
},
|
||||
async updateTimestamp() {
|
||||
let db = await RemoteSettings(this.relatedRealmsCollection).db;
|
||||
await db.importChanges({}, 12345678);
|
||||
},
|
||||
};
|
||||
|
@ -28,6 +28,12 @@ add_task(async function common_initialize() {
|
||||
["toolkit.telemetry.ipcBatchTimeout", 0],
|
||||
],
|
||||
});
|
||||
if (LoginHelper.relatedRealmsEnabled) {
|
||||
LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials();
|
||||
registerCleanupFunction(function() {
|
||||
LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
registerCleanupFunction(
|
||||
|
@ -7,6 +7,8 @@ prefs =
|
||||
signon.testOnlyUserHasInteractedWithDocument=true
|
||||
security.insecure_field_warning.contextual.enabled=false
|
||||
network.auth.non-web-content-triggered-resources-http-auth-allow=true
|
||||
# signon.relatedRealms.enabled pref needed until Bug 1699698 lands
|
||||
signon.relatedRealms.enabled=true
|
||||
|
||||
support-files =
|
||||
../../../prompts/test/chromeScript.js
|
||||
@ -38,6 +40,8 @@ skip-if = toolkit == 'android' && !is_fennec # Don't run on GeckoView
|
||||
|
||||
# Note: new tests should use scheme = https unless they have a specific reason not to
|
||||
|
||||
[test_autocomplete_autofill_related_realms_no_dupes.html]
|
||||
scheme = https
|
||||
[test_autocomplete_basic_form.html]
|
||||
skip-if = toolkit == 'android' || debug && (os == 'linux' || os == 'win') || os == 'linux' && tsan # android:autocomplete. Bug 1541945, Bug 1590928
|
||||
scheme = https
|
||||
@ -46,6 +50,8 @@ skip-if = toolkit == 'android' || os == 'linux' # android:autocomplete., linux:
|
||||
[test_autocomplete_basic_form_formActionOrigin.html]
|
||||
skip-if = toolkit == 'android' # android:autocomplete.
|
||||
scheme = https
|
||||
[test_autocomplete_basic_form_related_realms.html]
|
||||
scheme = https
|
||||
[test_autocomplete_basic_form_subdomain.html]
|
||||
skip-if = toolkit == 'android' # android:autocomplete.
|
||||
scheme = https
|
||||
|
@ -667,6 +667,23 @@ function resetRecipes() {
|
||||
});
|
||||
}
|
||||
|
||||
function resetWebsitesWithSharedCredential() {
|
||||
info("Resetting the 'websites-with-shared-credential-backend' collection");
|
||||
return new Promise(resolve => {
|
||||
PWMGR_COMMON_PARENT.addMessageListener(
|
||||
"resetWebsitesWithSharedCredential",
|
||||
function reset() {
|
||||
PWMGR_COMMON_PARENT.removeMessageListener(
|
||||
"resetWebsitesWithSharedCredential",
|
||||
reset
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
PWMGR_COMMON_PARENT.sendAsyncMessage("resetWebsitesWithSharedCredential");
|
||||
});
|
||||
}
|
||||
|
||||
function promiseStorageChanged(expectedChangeTypes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function onStorageChanged({ topic, data }) {
|
||||
|
@ -21,6 +21,12 @@ var { LoginManagerParent } = ChromeUtils.import(
|
||||
const { LoginTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/LoginTestUtils.jsm"
|
||||
);
|
||||
if (LoginHelper.relatedRealmsEnabled) {
|
||||
let rsPromise = LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials();
|
||||
async () => {
|
||||
await rsPromise;
|
||||
};
|
||||
}
|
||||
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
/**
|
||||
@ -127,6 +133,7 @@ addMessageListener("cleanup", () => {
|
||||
Services.obs.removeObserver(onStorageChanged, "passwordmgr-storage-changed");
|
||||
Services.obs.removeObserver(onPrompt, "passwordmgr-prompt-change");
|
||||
Services.obs.removeObserver(onPrompt, "passwordmgr-prompt-save");
|
||||
Services.logins.removeAllUserFacingLogins();
|
||||
});
|
||||
|
||||
// Begin message listeners
|
||||
|
@ -25,7 +25,7 @@ let FILE_PATH = "/tests/toolkit/components/passwordmgr/test/mochitest/slow_image
|
||||
runInParent(function removeAll() {
|
||||
let {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
Services.logins.removeAllUserFacingLogins();
|
||||
})
|
||||
});
|
||||
|
||||
let readyPromise = registerRunTests();
|
||||
|
||||
|
@ -0,0 +1,101 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test no duplicate logins using autofill/autocomplete with related realms</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
|
||||
<script type="text/javascript" src="pwmgr_common.js"></script>
|
||||
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
Login Manager test: no duplicate logins when using autofill and autocomplete with related realms
|
||||
<script>
|
||||
addLoginsInParent(
|
||||
// Simple related domain relationship where example.com and other-example.com are in the related domains list
|
||||
["https://other-example.com", "https://other-example.com", null, "relatedUser1", "relatedPass1", "uname", "pword"],
|
||||
|
||||
// Example.com and example.co.uk are related, so sub.example.co.uk should appear on example.com's autocomplete dropdown
|
||||
// The intent is to cover the ebay.com/ebay.co.uk and all other country TLD cases
|
||||
// where the sign in page is actually signin.ebay.com/signin.ebay.co.uk but credentials could have manually been entered
|
||||
// for ebay.com/ebay.co.uk or automatically stored as signin.ebay.com/sigin.ebay.co.uk
|
||||
["https://sub.example.co.uk", "https://sub.example.co.uk", null, "subUser1", "subPass1", "uname", "pword"],
|
||||
|
||||
// Ensures there are no duplicates for the exact domain that the user is on
|
||||
["https://example.com", "https://example.com", null, "exactUser1", "exactPass1", "uname", "pword"],
|
||||
["https://www.example.com", "https://www.example.com", null, "exactWWWUser1", "exactWWWPass1", "uname", "pword"],
|
||||
);
|
||||
</script>
|
||||
<p id="display"></p>
|
||||
<div id="content">
|
||||
<form id="form1" action="https://www.example.com" onsubmit="return false;">
|
||||
<input type="text" name="uname">
|
||||
<input type="password" name="pword">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
/** Test for Login Manager: related realms autofill/autocomplete. **/
|
||||
|
||||
let uname = $_(1, "uname");
|
||||
let pword = $_(1, "pword");
|
||||
|
||||
function restoreForm() {
|
||||
uname.value = "";
|
||||
pword.value = "";
|
||||
uname.focus();
|
||||
}
|
||||
|
||||
function sendFakeAutocompleteEvent(element) {
|
||||
var acEvent = document.createEvent("HTMLEvents");
|
||||
acEvent.initEvent("DOMAutoComplete", true, false);
|
||||
element.dispatchEvent(acEvent);
|
||||
}
|
||||
|
||||
function spinEventLoop() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function promiseACPopupClosed() {
|
||||
return SimpleTest.promiseWaitForCondition(async () => {
|
||||
let popupState = await getPopupState();
|
||||
return !popupState.open;
|
||||
}, "Wait for AC popup to be closed");
|
||||
}
|
||||
|
||||
add_task(async function setup() {
|
||||
listenForUnexpectedPopupShown();
|
||||
});
|
||||
|
||||
add_task(async function test_no_duplicates_autocomplete_autofill() {
|
||||
await promiseFormsProcessedInSameProcess();
|
||||
await SimpleTest.promiseFocus(window);
|
||||
let shownPromise = promiseACShown();
|
||||
checkLoginForm(uname, "exactUser1", pword, "exactPass1")
|
||||
restoreForm();
|
||||
let results = await shownPromise;
|
||||
|
||||
let popupState = await getPopupState();
|
||||
is(popupState.selectedIndex, -1, "Check no entires are selected upon opening");
|
||||
let expectedMenuItems = ["exactUser1", "exactWWWUser1", "relatedUser1", "subUser1"];
|
||||
checkAutoCompleteResults(results, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly");
|
||||
|
||||
let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
|
||||
is(acEvents.length, 1, "One autocomplete event");
|
||||
checkACTelemetryEvent(acEvents[0], uname, {
|
||||
"hadPrevious": "0",
|
||||
"login": expectedMenuItems.length + "",
|
||||
"loginsFooter": "1"
|
||||
});
|
||||
restoreForm();
|
||||
synthesizeKey("KEY_Escape");
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,131 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test login autocomplete with related realms</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
|
||||
<script type="text/javascript" src="pwmgr_common.js"></script>
|
||||
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
Login Manager test: related realms autocomplete
|
||||
|
||||
<script>
|
||||
addLoginsInParent(
|
||||
// Simple related domain relationship where example.com and other-example.com are in the related domains list
|
||||
["https://other-example.com", "https://other-example.com", null, "relatedUser1", "relatedPass1", "uname", "pword"],
|
||||
|
||||
// Example.com and example.co.uk are related, so sub.example.co.uk should appear on example.com's autocomplete dropdown
|
||||
// The intent is to cover the ebay.com/ebay.co.uk and all other country TLD cases
|
||||
// where the sign in page is actually signin.ebay.com/signin.ebay.co.uk but credentials could have manually been entered
|
||||
// for ebay.com/ebay.co.uk or automatically stored as signin.ebay.com/sigin.ebay.co.uk
|
||||
["https://sub.example.co.uk", "https://sub.example.co.uk", null, "subUser1", "subPass1", "uname", "pword"],
|
||||
);
|
||||
</script>
|
||||
<p id="display"></p>
|
||||
<div id="content">
|
||||
<form id="form1" onsubmit="return false;">
|
||||
<input type="text" name="uname">
|
||||
<input type="password" name="pword">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
/** Test for Login Manager: related realms autocomplete. **/
|
||||
|
||||
let uname = $_(1, "uname");
|
||||
let pword = $_(1, "pword");
|
||||
|
||||
function restoreForm() {
|
||||
uname.value = "";
|
||||
pword.value = "";
|
||||
uname.focus();
|
||||
}
|
||||
|
||||
function sendFakeAutocompleteEvent(element) {
|
||||
var acEvent = document.createEvent("HTMLEvents");
|
||||
acEvent.initEvent("DOMAutoComplete", true, false);
|
||||
element.dispatchEvent(acEvent);
|
||||
}
|
||||
|
||||
function spinEventLoop() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function promiseACPopupClosed() {
|
||||
return SimpleTest.promiseWaitForCondition(async () => {
|
||||
let popupState = await getPopupState();
|
||||
return !popupState.open;
|
||||
}, "Wait for AC popup to be closed");
|
||||
}
|
||||
|
||||
add_task(async function setup() {
|
||||
listenForUnexpectedPopupShown();
|
||||
});
|
||||
|
||||
add_task(async function test_form1_initial_empty() {
|
||||
await SimpleTest.promiseFocus(window);
|
||||
|
||||
// Make sure initial form is empty.
|
||||
checkLoginForm(uname, "", pword, "");
|
||||
let popupState = await getPopupState();
|
||||
is(popupState.open, false, "Check popup is initially closed");
|
||||
});
|
||||
|
||||
add_task(async function test_form_related_domain_menuitems() {
|
||||
await SimpleTest.promiseFocus(window);
|
||||
let shownPromise = promiseACShown();
|
||||
// Trigger autocomplete popup
|
||||
restoreForm();
|
||||
let results = await shownPromise;
|
||||
let popupState = await getPopupState();
|
||||
is(popupState.selectedIndex, -1, "Check no entires are selected upon opening");
|
||||
|
||||
let expectedMenuItems = ["relatedUser1", "subUser1"];
|
||||
checkAutoCompleteResults(results, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly");
|
||||
|
||||
let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true });
|
||||
is(acEvents.length, 1, "One autocomplete event");
|
||||
checkACTelemetryEvent(acEvents[0], uname, {
|
||||
"hadPrevious": "0",
|
||||
"login": expectedMenuItems.length + "",
|
||||
"loginsFooter": "1"
|
||||
});
|
||||
checkLoginForm(uname, "", pword, ""); // value shouldn't update just by opening
|
||||
|
||||
synthesizeKey("KEY_ArrowDown"); // first item
|
||||
checkLoginForm(uname, "", pword, ""); // value shouldn't update just by selecting
|
||||
|
||||
synthesizeKey("KEY_Enter");
|
||||
await promiseFormsProcessedInSameProcess();
|
||||
is(pword.value, "relatedPass1", "password should match the login that was selected");
|
||||
checkLoginForm(uname, "relatedUser1", pword, "relatedPass1");
|
||||
|
||||
restoreForm();
|
||||
|
||||
shownPromise = promiseACShown();
|
||||
synthesizeKey("KEY_ArrowDown"); // open
|
||||
await shownPromise;
|
||||
|
||||
synthesizeKey("KEY_ArrowDown"); // first item
|
||||
synthesizeKey("KEY_ArrowDown"); // second item
|
||||
checkLoginForm(uname, "", pword, ""); // value shouldn't update just by selecting
|
||||
|
||||
synthesizeKey("KEY_Enter");
|
||||
await promiseFormsProcessedInSameProcess();
|
||||
is(pword.value, "subPass1", "password should match the login that was selected");
|
||||
checkLoginForm(uname, "subUser1", pword, "subPass1");
|
||||
|
||||
restoreForm();
|
||||
});
|
||||
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -240,10 +240,6 @@ add_task(async function test_multiple_prefilled_focused_dynamic() {
|
||||
usernameField.focus();
|
||||
await shownPromise;
|
||||
});
|
||||
|
||||
add_task(async function cleanup() {
|
||||
removeFocus();
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
|
@ -52,7 +52,7 @@ const newPropertyBag = LoginHelper.newPropertyBag;
|
||||
|
||||
const NEW_PASSWORD_HEURISTIC_ENABLED_PREF =
|
||||
"signon.generation.confidenceThreshold";
|
||||
|
||||
const RELATED_REALMS_ENABLED_PREF = "signon.relatedRealms.enabled";
|
||||
/**
|
||||
* All the tests are implemented with add_task, this starts them automatically.
|
||||
*/
|
||||
@ -91,6 +91,12 @@ add_task(async function test_common_initialize() {
|
||||
|
||||
// Ensure that the service and the storage module are initialized.
|
||||
await Services.logins.initializationPromise;
|
||||
Services.prefs.setBoolPref(RELATED_REALMS_ENABLED_PREF, true);
|
||||
if (LoginHelper.relatedRealmsEnabled) {
|
||||
// Ensure that there is a mocked Remote Settings database for the
|
||||
// "websites-with-shared-credential-backends" collection
|
||||
await LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials();
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_common_prefs() {
|
||||
|
@ -0,0 +1,157 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { LoginRelatedRealmsParent } = ChromeUtils.import(
|
||||
"resource://gre/modules/LoginRelatedRealms.jsm"
|
||||
);
|
||||
const { RemoteSettings } = ChromeUtils.import(
|
||||
"resource://services-settings/remote-settings.js",
|
||||
{}
|
||||
);
|
||||
|
||||
const REMOTE_SETTINGS_COLLECTION = "websites-with-shared-credential-backends";
|
||||
|
||||
add_task(async function test_related_domain_matching() {
|
||||
const client = RemoteSettings(REMOTE_SETTINGS_COLLECTION);
|
||||
const records = await client.get();
|
||||
console.log(records);
|
||||
|
||||
// Assumes that the test collection is a 2D array with one subarray
|
||||
let relatedRealms = records[0].relatedRealms;
|
||||
relatedRealms = relatedRealms.flat();
|
||||
ok(relatedRealms);
|
||||
|
||||
let LRR = new LoginRelatedRealmsParent();
|
||||
|
||||
// We should not return unrelated realms
|
||||
let result = await LRR.findRelatedRealms("https://not-example.com");
|
||||
equal(result.length, 0, "Check that there were no related realms found");
|
||||
|
||||
// We should not return unrelated realms given an unrelated subdomain
|
||||
result = await LRR.findRelatedRealms("https://sub.not-example.com");
|
||||
equal(result.length, 0, "Check that there were no related realms found");
|
||||
// We should return the related realms collection
|
||||
result = await LRR.findRelatedRealms("https://sub.example.com");
|
||||
equal(
|
||||
result.length,
|
||||
relatedRealms.length,
|
||||
"Ensure that three related realms were found"
|
||||
);
|
||||
|
||||
// We should return the related realms collection minus the base domain that we searched with
|
||||
result = await LRR.findRelatedRealms("https://example.co.uk");
|
||||
equal(
|
||||
result.length,
|
||||
relatedRealms.length - 1,
|
||||
"Ensure that two related realms were found"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_newly_synced_collection() {
|
||||
// Initialize LoginRelatedRealmsParent so the sync handler is enabled
|
||||
let LRR = new LoginRelatedRealmsParent();
|
||||
await LRR.getSharedCredentialsCollection();
|
||||
|
||||
const client = RemoteSettings(REMOTE_SETTINGS_COLLECTION);
|
||||
let records = await client.get();
|
||||
const record1 = {
|
||||
id: records[0].id,
|
||||
relatedRealms: records[0].relatedRealms,
|
||||
};
|
||||
|
||||
// Assumes that the test collection is a 2D array with one subarray
|
||||
let originalRelatedRealms = records[0].relatedRealms;
|
||||
originalRelatedRealms = originalRelatedRealms.flat();
|
||||
ok(originalRelatedRealms);
|
||||
|
||||
const updatedRelatedRealms = ["completely-different.com", "example.com"];
|
||||
const record2 = {
|
||||
id: "some-other-ID",
|
||||
relatedRealms: [updatedRelatedRealms],
|
||||
};
|
||||
const payload = {
|
||||
current: [record2],
|
||||
created: [record2],
|
||||
updated: [],
|
||||
deleted: [record1],
|
||||
};
|
||||
await RemoteSettings(REMOTE_SETTINGS_COLLECTION).emit("sync", {
|
||||
data: payload,
|
||||
});
|
||||
|
||||
let [{ id, relatedRealms }] = await LRR.getSharedCredentialsCollection();
|
||||
equal(id, record2.id, "internal collection ID should be updated");
|
||||
equal(
|
||||
relatedRealms,
|
||||
record2.relatedRealms,
|
||||
"internal collection related realms should be updated"
|
||||
);
|
||||
|
||||
// We should return only one result, and that result should be example.com
|
||||
// NOT other-example.com or example.co.uk
|
||||
let result = await LRR.findRelatedRealms("https://completely-different.com");
|
||||
equal(
|
||||
result.length,
|
||||
updatedRelatedRealms.length - 1,
|
||||
"Check that there is only one related realm found"
|
||||
);
|
||||
equal(
|
||||
result[0],
|
||||
"example.com",
|
||||
"Ensure that the updated collection should only match example.com"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_no_related_domains() {
|
||||
await LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials();
|
||||
|
||||
const client = RemoteSettings(REMOTE_SETTINGS_COLLECTION);
|
||||
let records = await client.get();
|
||||
|
||||
equal(records.length, 0, "Check that there are no related realms");
|
||||
|
||||
let LRR = new LoginRelatedRealmsParent();
|
||||
|
||||
ok(LRR.findRelatedRealms, "Ensure findRelatedRealms exists");
|
||||
|
||||
let result = await LRR.findRelatedRealms("https://example.com");
|
||||
equal(result.length, 0, "Assert that there were no related realms found");
|
||||
});
|
||||
|
||||
add_task(async function test_unrelated_subdomains() {
|
||||
await LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials();
|
||||
let testCollection = [
|
||||
["slpl.bibliocommons.com", "slpl.overdrive.com"],
|
||||
["springfield.overdrive.com", "coolcat.org"],
|
||||
];
|
||||
await LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials(
|
||||
testCollection
|
||||
);
|
||||
|
||||
let LRR = new LoginRelatedRealmsParent();
|
||||
let result = await LRR.findRelatedRealms("https://evil.overdrive.com");
|
||||
equal(result.length, 0, "Assert that there were no related realms found");
|
||||
|
||||
result = await LRR.findRelatedRealms("https://abc.slpl.bibliocommons.com");
|
||||
equal(result.length, 2, "Assert that two related realms were found");
|
||||
equal(result[0], testCollection[0][0]);
|
||||
equal(result[1], testCollection[0][1]);
|
||||
|
||||
result = await LRR.findRelatedRealms("https://slpl.overdrive.com");
|
||||
console.log("what is result: " + result);
|
||||
equal(result.length, 1, "Assert that one related realm was found");
|
||||
for (let item of result) {
|
||||
notEqual(
|
||||
item,
|
||||
"coolcat.org",
|
||||
"coolcat.org is not related to slpl.overdrive.com"
|
||||
);
|
||||
notEqual(
|
||||
item,
|
||||
"springfield.overdrive.com",
|
||||
"springfield.overdrive.com is not related to slpl.overdrive.com"
|
||||
);
|
||||
}
|
||||
});
|
@ -3,6 +3,7 @@
|
||||
|
||||
/**
|
||||
* Tests retrieving remote LoginRecipes in the parent process.
|
||||
* See https://firefox-source-docs.mozilla.org/services/settings/#unit-tests for explanation of db.importChanges({}, 42);
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
@ -16,13 +17,14 @@ const REMOTE_SETTINGS_COLLECTION = "password-recipes";
|
||||
|
||||
add_task(async function test_init_remote_recipe() {
|
||||
const db = await RemoteSettings(REMOTE_SETTINGS_COLLECTION).db;
|
||||
await db.clear();
|
||||
const record1 = {
|
||||
id: "some-fake-ID",
|
||||
hosts: ["www.testDomain.com"],
|
||||
description: "Some description here",
|
||||
usernameSelector: "#username",
|
||||
};
|
||||
await db.create(record1);
|
||||
await db.importChanges({}, 42, [record1], { clear: true });
|
||||
let parent = new LoginRecipesParent({ defaults: true });
|
||||
|
||||
let recipesParent = await parent.initializationPromise;
|
||||
@ -54,6 +56,7 @@ add_task(async function test_init_remote_recipe() {
|
||||
"Initially 1 recipe based on our test record"
|
||||
);
|
||||
await db.clear();
|
||||
await db.importChanges({}, 42);
|
||||
});
|
||||
|
||||
add_task(async function test_add_recipe_sync() {
|
||||
@ -64,7 +67,7 @@ add_task(async function test_add_recipe_sync() {
|
||||
description: "Some description here",
|
||||
usernameSelector: "#username",
|
||||
};
|
||||
await db.create(record1);
|
||||
await db.importChanges({}, 42, [record1], { clear: true });
|
||||
let parent = new LoginRecipesParent({ defaults: true });
|
||||
let recipesParent = await parent.initializationPromise;
|
||||
|
||||
@ -89,6 +92,7 @@ add_task(async function test_add_recipe_sync() {
|
||||
"New recipe from sync event added successfully"
|
||||
);
|
||||
await db.clear();
|
||||
await db.importChanges({}, 42);
|
||||
});
|
||||
|
||||
add_task(async function test_remove_recipe_sync() {
|
||||
@ -99,7 +103,7 @@ add_task(async function test_remove_recipe_sync() {
|
||||
description: "Some description here",
|
||||
usernameSelector: "#username",
|
||||
};
|
||||
await db.create(record1);
|
||||
await db.importChanges({}, 42, [record1], { clear: true });
|
||||
let parent = new LoginRecipesParent({ defaults: true });
|
||||
let recipesParent = await parent.initializationPromise;
|
||||
|
||||
@ -129,7 +133,7 @@ add_task(async function test_malformed_recipes_in_db() {
|
||||
usernameSelector: "#username",
|
||||
fieldThatDoesNotExist: "value",
|
||||
};
|
||||
await db.create(malformedRecord);
|
||||
await db.importChanges({}, 42, [malformedRecord], { clear: true });
|
||||
let parent = new LoginRecipesParent({ defaults: true });
|
||||
try {
|
||||
await parent.initializationPromise;
|
||||
@ -146,7 +150,7 @@ add_task(async function test_malformed_recipes_in_db() {
|
||||
description: "Some description here",
|
||||
usernameSelector: "#username",
|
||||
};
|
||||
await db.create(missingHostsRecord);
|
||||
await db.importChanges({}, 42, [missingHostsRecord], { clear: true });
|
||||
parent = new LoginRecipesParent({ defaults: true });
|
||||
try {
|
||||
await parent.initializationPromise;
|
||||
|
@ -18,6 +18,7 @@ run-if = buildapp == "browser"
|
||||
[test_disabled_hosts.js]
|
||||
[test_displayOrigin.js]
|
||||
[test_doLoginsMatch.js]
|
||||
[test_findRelatedRealms.js]
|
||||
[test_getFormFields.js]
|
||||
[test_getPasswordFields.js]
|
||||
[test_getPasswordOrigin.js]
|
||||
|
Loading…
Reference in New Issue
Block a user