mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 03:15:11 +00:00
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:
parent
57e9dcd9e4
commit
be31af02f5
@ -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%");
|
||||
|
@ -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);
|
||||
|
@ -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 = {
|
||||
|
@ -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"
|
||||
);
|
||||
|
@ -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({
|
||||
|
534
toolkit/components/passwordmgr/NewPasswordModel.jsm
Normal file
534
toolkit/components/passwordmgr/NewPasswordModel.jsm
Normal 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),
|
||||
};
|
@ -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',
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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";
|
||||
|
@ -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>
|
@ -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);
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user