Bug 1595244 - Use field labels and attributes to determine new-password field types r=MattN

* Add a new module, NewPasswordModel.jsm, which imports the Fathom library from Bug 1618956, and copies over the Fathom model at [bff6995c32e86...](bff6995c32/new-password/rulesets.js (L6-L272)) into it.
  * This module is intended to make it as easy as possible to import model updates from the upstream GitHub repo, though there are a couple extra steps beyond a simple copy and paste.
* The module is imported into LoginAutoComplete.jsm, which has a new helper, _isProbablyANewPasswordField, which runs the model against the provided input element and returns true if the element's confidence score is greater than or equal to a threshold.
  * The confidence threshold, specified by the new signon.generation.confidenceThreshold string preference, is currently set to 0.5 based on [this comment](https://bugzilla.mozilla.org/show_bug.cgi?id=1595244#c12).
  * This should result in a dramatic reduction in false negative rates compared to the existing implementation of this feature (at the expense of an increase in the false positive rate of the existing feature from ~0% to hopefully around 2%).
  * Use of the model is gated behind the same preference used for the confidence threshold, signon.generation.confidenceThreshold.
    * This is a string pref that disables the model if its value is "-1". Otherwise, its value should be a string representation of a float [0,1] (e.g. "0.5") which indicates that the model should be enabled with the given confidence threshold.
    * Using the model is enabled by default on desktop but disabled by default on mobile (GeckoView) due to Bug 1618058.
* Fixed some existing tests that were broken as a result of this change.
* New test(s) will be added in a subsequent commit.

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Bianca Danforth 2020-03-21 20:53:09 +00:00
parent 57e9dcd9e4
commit be31af02f5
13 changed files with 629 additions and 33 deletions

View File

@ -1724,6 +1724,9 @@ pref("extensions.pocket.enabled", true);
pref("extensions.pocket.oAuthConsumerKey", "40249-e88c401e1b1f2242d9e441c4");
pref("extensions.pocket.site", "getpocket.com");
// Can be removed once Bug 1618058 is resolved.
pref("signon.generation.confidenceThreshold", "0.5");
pref("signon.management.page.breach-alerts.enabled", true);
pref("signon.management.page.sort", "name");
pref("signon.management.overrideURI", "about:logins?filter=%DOMAIN%");

View File

@ -3943,6 +3943,8 @@ pref("signon.autofillForms.http", false);
pref("signon.autologin.proxy", false);
pref("signon.formlessCapture.enabled", true);
pref("signon.generation.available", true);
// A value of "-1" disables new-password heuristics. Can be updated once Bug 1618058 is resolved.
pref("signon.generation.confidenceThreshold", "-1");
pref("signon.generation.enabled", true);
pref("signon.passwordEditCapture.enabled", false);
pref("signon.privateBrowsingCapture.enabled", true);

View File

@ -46,6 +46,12 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/LoginManagerChild.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"NewPasswordModel",
"resource://gre/modules/NewPasswordModel.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"formFillController",
@ -59,7 +65,7 @@ XPCOMUtils.defineLazyPreferenceGetter(
);
XPCOMUtils.defineLazyGetter(this, "log", () => {
return LoginHelper.createLogger("LoginAutoCompleteResult");
return LoginHelper.createLogger("LoginAutoComplete");
});
XPCOMUtils.defineLazyGetter(this, "passwordMgrBundle", () => {
return Services.strings.createBundle(
@ -425,12 +431,16 @@ LoginAutoCompleteResult.prototype = {
},
};
function LoginAutoComplete() {}
function LoginAutoComplete() {
// HTMLInputElement to number, the element's new-password heuristic confidence score
this._cachedNewPasswordScore = new WeakMap();
}
LoginAutoComplete.prototype = {
classID: Components.ID("{2bdac17c-53f1-4896-a521-682ccdeef3a8}"),
QueryInterface: ChromeUtils.generateQI([Ci.nsILoginAutoCompleteSearch]),
_autoCompleteLookupPromise: null,
_cachedNewPasswordScore: null,
/**
* Yuck. This is called directly by satchel:
@ -550,8 +560,6 @@ LoginAutoComplete.prototype = {
return;
}
log.debug("AutoCompleteSearch invoked. Search is:", aSearchString);
let previousResult;
if (aPreviousResult) {
previousResult = {
@ -594,14 +602,19 @@ LoginAutoComplete.prototype = {
inputElement.ownerGlobal
);
let forcePasswordGeneration = false;
let isProbablyANewPasswordField = false;
if (isPasswordField) {
forcePasswordGeneration = loginManagerActor.isPasswordGenerationForcedOn(
inputElement
);
// Run the Fathom model only if the password field does not have the
// autocomplete="new-password" attribute.
isProbablyANewPasswordField =
autocompleteInfo.fieldName == "new-password" ||
this._isProbablyANewPasswordField(inputElement);
}
let messageData = {
autocompleteInfo,
formOrigin,
actionOrigin,
searchString,
@ -609,12 +622,23 @@ LoginAutoComplete.prototype = {
forcePasswordGeneration,
isSecure: InsecurePasswordUtils.isFormSecure(form),
isPasswordField,
isProbablyANewPasswordField,
};
if (LoginHelper.showAutoCompleteFooter) {
gAutoCompleteListener.init();
}
log.debug("LoginAutoComplete search:", {
forcePasswordGeneration,
isSecure: messageData.isSecure,
isPasswordField,
isProbablyANewPasswordField,
searchString: isPasswordField
? "*".repeat(searchString.length)
: searchString,
});
let result = await loginManagerActor.sendQuery(
"PasswordManager:autoCompleteLogins",
messageData
@ -626,6 +650,25 @@ LoginAutoComplete.prototype = {
willAutoSaveGeneratedPassword: result.willAutoSaveGeneratedPassword,
};
},
_isProbablyANewPasswordField(inputElement) {
const threshold = LoginHelper.generationConfidenceThreshold;
if (threshold == -1) {
// Fathom is disabled
return false;
}
let score = this._cachedNewPasswordScore.get(inputElement);
if (score) {
return score >= threshold;
}
const { rules, type } = NewPasswordModel;
const results = rules.against(inputElement);
score = results.get(inputElement).scoreFor(type);
this._cachedNewPasswordScore.set(inputElement, score);
return score >= threshold;
},
};
let gAutoCompleteListener = {

View File

@ -30,6 +30,7 @@ this.LoginHelper = {
storageEnabled: null,
formlessCaptureEnabled: null,
generationAvailable: null,
generationConfidenceThreshold: null,
generationEnabled: null,
includeOtherSubdomainsInLookup: null,
insecureAutofill: null,
@ -63,6 +64,9 @@ this.LoginHelper = {
this.generationAvailable = Services.prefs.getBoolPref(
"signon.generation.available"
);
this.generationConfidenceThreshold = parseFloat(
Services.prefs.getStringPref("signon.generation.confidenceThreshold")
);
this.generationEnabled = Services.prefs.getBoolPref(
"signon.generation.enabled"
);

View File

@ -412,7 +412,6 @@ class LoginManagerParent extends JSWindowActorParent {
}
async doAutocompleteSearch({
autocompleteInfo,
formOrigin,
actionOrigin,
searchString,
@ -420,6 +419,7 @@ class LoginManagerParent extends JSWindowActorParent {
forcePasswordGeneration,
isSecure,
isPasswordField,
isProbablyANewPasswordField,
}) {
// Note: previousResult is a regular object, not an
// nsIAutoCompleteResult.
@ -486,10 +486,11 @@ class LoginManagerParent extends JSWindowActorParent {
let generatedPassword = null;
let willAutoSaveGeneratedPassword = false;
if (
forcePasswordGeneration ||
(isPasswordField &&
autocompleteInfo.fieldName == "new-password" &&
Services.logins.getLoginSavingEnabled(formOrigin))
// If MP was cancelled above, don't try to offer pwgen or access storage again (causing a new MP prompt).
Services.logins.isLoggedIn &&
(forcePasswordGeneration ||
(isProbablyANewPasswordField &&
Services.logins.getLoginSavingEnabled(formOrigin)))
) {
generatedPassword = this.getGeneratedPassword();
let potentialConflictingLogins = LoginHelper.searchLoginsWithObject({

View File

@ -0,0 +1,534 @@
/* 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/. */
/**
* Machine learning model for identifying new password input elements
* using Fathom.
*/
const EXPORTED_SYMBOLS = ["NewPasswordModel"];
ChromeUtils.defineModuleGetter(
this,
"fathom",
"resource://gre/modules/third_party/fathom/fathom.jsm"
);
const {
dom,
element,
out,
rule,
ruleset,
score,
type,
utils: { identity, isVisible, min },
clusters: { euclidean },
} = fathom;
/**
* ----- Start of model -----
*
* Everything below this comment up to the "End of model" comment is copied from:
* https://github.com/mozilla-services/fathom-login-forms/blob/bff6995c32e86b6b5810ff013632f68db8747346/new-password/rulesets.js#L6-L272
* Deviations from that file:
* - Remove import statements, instead using ``ChromeUtils.defineModuleGetter`` and destructuring assignments above.
* - Set ``DEVELOPMENT`` constant to ``false``.
*/
// Whether this is running in the Vectorizer, rather than in-application, in a
// privileged Chrome context
const DEVELOPMENT = false;
const coefficients = {
new: [
["hasPasswordLabel", 2.6237754821777344],
["hasNewLabel", 0.8804616928100586],
["hasConfirmLabel", 2.19775390625],
["hasConfirmEmailLabel", -2.51192307472229],
["closestLabelMatchesPassword", 0.8376041054725647],
["closestLabelMatchesNew", -0.3952447175979614],
["closestLabelMatchesConfirm", 1.6602904796600342],
["closestLabelMatchesConfirmEmail", -2.828238010406494],
["hasPasswordAriaLabel", 1.3898684978485107],
["hasNewAriaLabel", 0.25765886902809143],
["hasConfirmAriaLabel", 3.3060154914855957],
["hasPasswordPlaceholder", 1.9736918210983276],
["hasNewPlaceholder", 0.20559890568256378],
["hasConfirmPlaceholder", 3.1848690509796143],
["hasConfirmEmailPlaceholder", 0.07635760307312012],
["forgotPasswordInFormLinkInnerText", -2.125054359436035],
["forgotPasswordInFormLinkHref", -2.310042381286621],
["forgotPasswordInFormLinkTitle", -3.322892427444458],
["forgotPasswordInFormButtonInnerText", -4.083425521850586],
["forgotPasswordOnPageLinkInnerText", -0.851978600025177],
["forgotPasswordOnPageLinkHref", -1.1582040786743164],
["forgotPasswordOnPageLinkTitle", -1.5192012786865234],
["forgotPasswordOnPageButtonInnerText", 1.4093186855316162],
["idIsPassword1Or2", 1.994940161705017],
["nameIsPassword1Or2", 3.0895347595214844],
["idMatchesPassword", -0.8498945236206055],
["nameMatchesPassword", 0.9636800289154053],
["idMatchesPasswordy", 2.0097758769989014],
["nameMatchesPasswordy", 3.0705039501190186],
["classMatchesPasswordy", -0.2314695119857788],
["idMatchesLogin", -2.941408157348633],
["nameMatchesLogin", 0.9627101421356201],
["classMatchesLogin", -3.4055135250091553],
["containingFormHasLoginId", -2.660820245742798],
["formButtonIsRegistery", -0.07571711391210556],
["formButtonIsLoginy", -3.7707366943359375],
["hasAutocompleteCurrentPassword", -0.029503464698791504],
],
};
const biases = [["new", -3.197509765625]];
const passwordRegex = /password|passwort|رمز عبور|mot de passe|パスワード|비밀번호|암호|wachtwoord|senha|Пароль|parol|密码|contraseña|heslo/i;
const newRegex = /erstellen|create|choose|設定|신규/i;
const confirmRegex = /wiederholen|wiederholung|confirm|repeat|confirmation|verify|retype|repite|確認|の確認|تکرار|re-enter|확인|bevestigen|confirme|Повторите|tassyklamak|再次输入/i;
const emailRegex = /e-mail|email|ایمیل|メールアドレス|이메일|邮箱/i;
const forgotPasswordInnerTextRegex = /vergessen|forgot|oublié|dimenticata|Esqueceu|esqueci|Забыли|忘记|找回|Zapomenuté|lost|忘れた|忘れられた|忘れの方|재설정|찾기/i;
const forgotPasswordHrefRegex = /forgot|reset|recovery|change|lost|reminder|find/i;
const password1Or2Regex = /password1|password2/i;
const passwordyRegex = /pw|pwd|passwd|pass/i;
const loginRegex = /login|Войти|sign in|ورود|登录|Přihlásit se|Авторизоваться|signin|log in|sign\/in|sign-in|entrar|ログインする|로그인/i;
const registerButtonRegex = /create account|Zugang anlegen|Angaben prüfen|Konto erstellen|register|sign up|create an account|create my account|ثبت نام|登録|Cadastrar|Зарегистрироваться|Bellige alynmak/i;
function makeRuleset(coeffs, biases) {
/**
* Don't bother with the fairly expensive isVisible() call when we're in
* production. We fire only when the user clicks an <input> field. They can't
* very well click an invisible one.
*/
function isVisibleInDev(fnodeOrElement) {
return DEVELOPMENT ? isVisible(fnodeOrElement) : true;
}
function hasConfirmLabel(fnode) {
return hasLabelMatchingRegex(fnode.element, confirmRegex);
}
function hasConfirmEmailLabel(fnode) {
return (
hasConfirmLabel(fnode) && hasLabelMatchingRegex(fnode.element, emailRegex)
);
}
function hasLabelMatchingRegex(element, regex) {
// Check element.labels
const labels = element.labels;
// TODO: Should I be concerned with multiple labels?
if (labels !== null && labels.length) {
return regex.test(labels[0].innerText);
}
// Check element.aria-labelledby
let labelledBy = element.getAttribute("aria-labelledby");
if (labelledBy !== null) {
labelledBy = labelledBy
.split(" ")
.map(id => element.ownerDocument.getElementById(id));
if (labelledBy.length === 1) {
return regex.test(labelledBy[0].innerText);
} else if (labelledBy.length > 1) {
return regex.test(
min(labelledBy, node => euclidean(node, element)).innerText
);
}
}
const parentElement = element.parentElement;
// Check if the input is in a <td>, and, if so, check the innerText of the containing <tr>
if (parentElement.tagName === "TD") {
// TODO: How bad is the assumption that the <tr> won't be the parent of the <td>?
return regex.test(parentElement.parentElement.innerText);
}
// Check if the input is in a <dd>, and, if so, check the innerText of the preceding <dt>
if (parentElement.tagName === "DD") {
return regex.test(parentElement.previousElementSibling.innerText);
}
return false;
}
function closestLabelMatchesConfirm(fnode) {
return closestLabelMatchesRegex(fnode.element, confirmRegex);
}
function closestLabelMatchesConfirmEmail(fnode) {
return (
closestLabelMatchesConfirm(fnode) &&
closestLabelMatchesRegex(fnode.element, emailRegex)
);
}
function closestLabelMatchesRegex(element, regex) {
const previousElementSibling = element.previousElementSibling;
if (
previousElementSibling !== null &&
previousElementSibling.tagName === "LABEL"
) {
return regex.test(previousElementSibling.innerText);
}
const nextElementSibling = element.nextElementSibling;
if (nextElementSibling !== null && nextElementSibling.tagName === "LABEL") {
return regex.test(nextElementSibling.innerText);
}
const closestLabelWithinForm = closestSelectorElementWithinElement(
element,
element.form,
"label"
);
return containsRegex(
regex,
closestLabelWithinForm,
closestLabelWithinForm => closestLabelWithinForm.innerText
);
}
function containsRegex(regex, thingOrNull, thingToString = identity) {
return thingOrNull !== null && regex.test(thingToString(thingOrNull));
}
function closestSelectorElementWithinElement(
toElement,
withinElement,
querySelector
) {
if (withinElement !== null) {
let nodeList = Array.from(withinElement.querySelectorAll(querySelector));
if (nodeList.length) {
return min(nodeList, node => euclidean(node, toElement));
}
}
return null;
}
function hasAriaLabelMatchingRegex(element, regex) {
return containsRegex(regex, element.getAttribute("aria-label"));
}
function hasConfirmPlaceholder(fnode) {
return hasPlaceholderMatchingRegex(fnode.element, confirmRegex);
}
function hasConfirmEmailPlaceholder(fnode) {
return (
hasConfirmPlaceholder(fnode) &&
hasPlaceholderMatchingRegex(fnode.element, emailRegex)
);
}
function hasPlaceholderMatchingRegex(element, regex) {
return containsRegex(regex, element.getAttribute("placeholder"));
}
function forgotPasswordInAnchorPropertyWithinElement(
property,
element,
...regexes
) {
return hasAnchorMatchingPredicateWithinElement(element, anchor => {
const propertyValue = anchor[property];
return regexes.every(regex => regex.test(propertyValue));
});
}
function hasAnchorMatchingPredicateWithinElement(element, matchingPredicate) {
if (element !== null) {
const anchors = Array.from(element.querySelectorAll("a"));
return anchors.some(matchingPredicate);
}
return false;
}
function forgotPasswordButtonWithinElement(element) {
if (element !== null) {
const buttons = Array.from(element.querySelectorAll("button"));
return buttons.some(button => {
const innerText = button.innerText;
return (
passwordRegex.test(innerText) &&
forgotPasswordInnerTextRegex.test(innerText)
);
});
}
return false;
}
function containingFormHasLoginId(fnode) {
const form = fnode.element.form;
return containsRegex(loginRegex, form, form => form.id);
}
function testFormButtonsAgainst(element, stringRegex) {
const form = element.form;
if (form !== null) {
let inputs = Array.from(
form.querySelectorAll("input[type=submit],input[type=button]")
);
inputs = inputs.filter(input => {
return stringRegex.test(input.value);
});
if (inputs.length) {
return true;
}
let buttons = Array.from(form.querySelectorAll("button"));
return buttons.some(button => {
return (
stringRegex.test(button.value) ||
stringRegex.test(button.innerText) ||
stringRegex.test(button.id)
);
});
}
return false;
}
function hasAutocompleteCurrentPassword(fnode) {
return fnode.element.autocomplete === "current-password";
}
return ruleset(
[
rule(
(DEVELOPMENT ? dom : element)(
'input[type=text],input[type=password],input[type=""],input:not([type])'
).when(isVisibleInDev),
type("new")
),
rule(
type("new"),
score(fnode => hasLabelMatchingRegex(fnode.element, passwordRegex)),
{ name: "hasPasswordLabel" }
),
rule(
type("new"),
score(fnode => hasLabelMatchingRegex(fnode.element, newRegex)),
{ name: "hasNewLabel" }
),
rule(type("new"), score(hasConfirmLabel), { name: "hasConfirmLabel" }),
rule(type("new"), score(hasConfirmEmailLabel), {
name: "hasConfirmEmailLabel",
}),
rule(
type("new"),
score(fnode => closestLabelMatchesRegex(fnode.element, passwordRegex)),
{ name: "closestLabelMatchesPassword" }
),
rule(
type("new"),
score(fnode => closestLabelMatchesRegex(fnode.element, newRegex)),
{ name: "closestLabelMatchesNew" }
),
rule(type("new"), score(closestLabelMatchesConfirm), {
name: "closestLabelMatchesConfirm",
}),
rule(type("new"), score(closestLabelMatchesConfirmEmail), {
name: "closestLabelMatchesConfirmEmail",
}),
rule(
type("new"),
score(fnode => hasAriaLabelMatchingRegex(fnode.element, passwordRegex)),
{ name: "hasPasswordAriaLabel" }
),
rule(
type("new"),
score(fnode => hasAriaLabelMatchingRegex(fnode.element, newRegex)),
{ name: "hasNewAriaLabel" }
),
rule(
type("new"),
score(fnode => hasAriaLabelMatchingRegex(fnode.element, confirmRegex)),
{ name: "hasConfirmAriaLabel" }
),
rule(
type("new"),
score(fnode =>
hasPlaceholderMatchingRegex(fnode.element, passwordRegex)
),
{ name: "hasPasswordPlaceholder" }
),
rule(
type("new"),
score(fnode => hasPlaceholderMatchingRegex(fnode.element, newRegex)),
{ name: "hasNewPlaceholder" }
),
rule(type("new"), score(hasConfirmPlaceholder), {
name: "hasConfirmPlaceholder",
}),
rule(type("new"), score(hasConfirmEmailPlaceholder), {
name: "hasConfirmEmailPlaceholder",
}),
rule(
type("new"),
score(fnode =>
forgotPasswordInAnchorPropertyWithinElement(
"innerText",
fnode.element.form,
passwordRegex,
forgotPasswordInnerTextRegex
)
),
{ name: "forgotPasswordInFormLinkInnerText" }
),
rule(
type("new"),
score(fnode =>
forgotPasswordInAnchorPropertyWithinElement(
"href",
fnode.element.form,
new RegExp(passwordRegex.source + "|" + passwordyRegex.source, "i"),
forgotPasswordHrefRegex
)
),
{ name: "forgotPasswordInFormLinkHref" }
),
rule(
type("new"),
score(fnode =>
forgotPasswordInAnchorPropertyWithinElement(
"title",
fnode.element.form,
passwordRegex,
forgotPasswordInnerTextRegex
)
),
{ name: "forgotPasswordInFormLinkTitle" }
),
rule(
type("new"),
score(fnode => forgotPasswordButtonWithinElement(fnode.element.form)),
{ name: "forgotPasswordInFormButtonInnerText" }
),
rule(
type("new"),
score(fnode =>
forgotPasswordInAnchorPropertyWithinElement(
"innerText",
fnode.element.ownerDocument,
passwordRegex,
forgotPasswordInnerTextRegex
)
),
{ name: "forgotPasswordOnPageLinkInnerText" }
),
rule(
type("new"),
score(fnode =>
forgotPasswordInAnchorPropertyWithinElement(
"href",
fnode.element.ownerDocument,
new RegExp(passwordRegex.source + "|" + passwordyRegex.source, "i"),
forgotPasswordHrefRegex
)
),
{ name: "forgotPasswordOnPageLinkHref" }
),
rule(
type("new"),
score(fnode =>
forgotPasswordInAnchorPropertyWithinElement(
"title",
fnode.element.ownerDocument,
passwordRegex,
forgotPasswordInnerTextRegex
)
),
{ name: "forgotPasswordOnPageLinkTitle" }
),
rule(
type("new"),
score(fnode =>
forgotPasswordButtonWithinElement(fnode.element.ownerDocument)
),
{ name: "forgotPasswordOnPageButtonInnerText" }
),
rule(
type("new"),
score(fnode => password1Or2Regex.test(fnode.element.id)),
{ name: "idIsPassword1Or2" }
),
rule(
type("new"),
score(fnode => password1Or2Regex.test(fnode.element.name)),
{ name: "nameIsPassword1Or2" }
),
rule(
type("new"),
score(fnode => passwordRegex.test(fnode.element.id)),
{ name: "idMatchesPassword" }
),
rule(
type("new"),
score(fnode => passwordRegex.test(fnode.element.name)),
{ name: "nameMatchesPassword" }
),
rule(
type("new"),
score(fnode => passwordyRegex.test(fnode.element.id)),
{ name: "idMatchesPasswordy" }
),
rule(
type("new"),
score(fnode => passwordyRegex.test(fnode.element.name)),
{ name: "nameMatchesPasswordy" }
),
rule(
type("new"),
score(fnode => passwordyRegex.test(fnode.element.className)),
{ name: "classMatchesPasswordy" }
),
rule(
type("new"),
score(fnode => loginRegex.test(fnode.element.id)),
{ name: "idMatchesLogin" }
),
rule(
type("new"),
score(fnode => loginRegex.test(fnode.element.name)),
{ name: "nameMatchesLogin" }
),
rule(
type("new"),
score(fnode => loginRegex.test(fnode.element.className)),
{ name: "classMatchesLogin" }
),
rule(type("new"), score(containingFormHasLoginId), {
name: "containingFormHasLoginId",
}),
rule(
type("new"),
score(fnode =>
testFormButtonsAgainst(fnode.element, registerButtonRegex)
),
{ name: "formButtonIsRegistery" }
),
rule(
type("new"),
score(fnode => testFormButtonsAgainst(fnode.element, loginRegex)),
{ name: "formButtonIsLoginy" }
),
rule(type("new"), score(hasAutocompleteCurrentPassword), {
name: "hasAutocompleteCurrentPassword",
}),
rule(type("new"), out("new")),
],
coeffs,
biases
);
}
/*
* ----- End of model -----
*/
this.NewPasswordModel = {
type: "new",
rules: makeRuleset([...coefficients.new], biases),
};

View File

@ -12,8 +12,6 @@ BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
TESTING_JS_MODULES += [
# Make this file available from the "resource:" URI of the test environment.
'test/browser/form_basic.html',
'test/LoginTestUtils.jsm',
]
@ -43,6 +41,7 @@ EXTRA_JS_MODULES += [
'LoginManagerParent.jsm',
'LoginManagerPrompter.jsm',
'LoginRecipes.jsm',
'NewPasswordModel.jsm',
'OSCrypto.jsm',
'storage-json.js',
]

View File

@ -4,6 +4,7 @@ support-files =
authenticate.sjs
form_basic.html
form_basic_iframe.html
form_basic_login.html
form_basic_no_username.html
formless_basic.html
form_same_origin_action.html

View File

@ -96,19 +96,13 @@ add_task(async function test_mpAutocompleteUIBusy() {
gBrowser.selectedBrowser.browsingContext.currentWindowGlobal;
let loginManagerParent = windowGlobal.getActor("LoginManager");
let arg1 = {
autocompleteInfo: {
section: "",
addressType: "",
contactType: "",
fieldName: "",
canAutomaticallyPersist: false,
},
formOrigin: "https://www.example.com",
actionOrigin: "",
searchString: "",
previousResult: null,
isSecure: false,
isPasswordField: true,
isProbablyANewPasswordField: true,
};
function dialogObserver(subject, topic, data) {

View File

@ -9,7 +9,7 @@
// The origin for the test URIs.
const TEST_ORIGIN = "https://example.com";
const FORM_PAGE_PATH =
"/browser/toolkit/components/passwordmgr/test/browser/form_basic.html";
"/browser/toolkit/components/passwordmgr/test/browser/form_basic_login.html";
const CONTEXT_MENU = document.getElementById("contentAreaContextMenu");
const passwordInputSelector = "#form-basic-password";

View File

@ -0,0 +1,12 @@
<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!-- Simplest login form with username and password fields. -->
<form id="form-basic-login">
<input id="form-basic-username" name="username">
<input id="form-basic-password" name="password" type="password">
<input id="form-basic-submit" type="submit" value="sign in">
</form>
</body></html>

View File

@ -11,13 +11,6 @@ const { LoginManagerParent } = ChromeUtils.import(
// new-password to the happy path
const NEW_PASSWORD_TEMPLATE_ARG = {
autocompleteInfo: {
section: "",
addressType: "",
contactType: "",
fieldName: "new-password",
canAutomaticallyPersist: false,
},
formOrigin: "https://example.com",
actionOrigin: "https://mozilla.org",
searchString: "",
@ -25,6 +18,7 @@ const NEW_PASSWORD_TEMPLATE_ARG = {
requestId: "foo",
isSecure: true,
isPasswordField: true,
isProbablyANewPasswordField: true,
};
add_task(async function setup() {
@ -77,7 +71,10 @@ add_task(async function test_generated_noLogins() {
let result3 = await LMP.doAutocompleteSearch({
...NEW_PASSWORD_TEMPLATE_ARG,
...{ isPasswordField: false },
...{
isPasswordField: false,
isProbablyANewPasswordField: false,
},
});
equal(
result3.generatedPassword,
@ -85,14 +82,18 @@ add_task(async function test_generated_noLogins() {
"no generated password when not a pw. field"
);
// Deep copy since we need to modify a property of autocompleteInfo.
let arg1_2 = JSON.parse(JSON.stringify(NEW_PASSWORD_TEMPLATE_ARG));
arg1_2.autocompleteInfo.fieldName = "";
let result4 = await LMP.doAutocompleteSearch(arg1_2);
let result4 = await LMP.doAutocompleteSearch({
...NEW_PASSWORD_TEMPLATE_ARG,
...{
// This is false when there is no autocomplete="new-password" attribute &&
// LoginAutoComplete._isProbablyANewPasswordField returns false
isProbablyANewPasswordField: false,
},
});
equal(
result4.generatedPassword,
null,
"no generated password when not autocomplete=new-password"
"no generated password when isProbablyANewPasswordField is false"
);
LMP.useBrowsingContext(999);

View File

@ -232,6 +232,8 @@ EXTRA_JS_MODULES.sessionstore += [
'sessionstore/Utils.jsm',
]
EXTRA_JS_MODULES.third_party.fathom += ['third_party/fathom/fathom.jsm']
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk'):
DEFINES['MENUBAR_CAN_AUTOHIDE'] = 1