зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1760617 - Refactor LoginAutoComplete.jsm r=tgiles
Differential Revision: https://phabricator.services.mozilla.com/D141625
This commit is contained in:
Родитель
151983da17
Коммит
e9fe3dd979
|
@ -45,13 +45,11 @@ ChromeUtils.defineModuleGetter(
|
||||||
"LoginManagerChild",
|
"LoginManagerChild",
|
||||||
"resource://gre/modules/LoginManagerChild.jsm"
|
"resource://gre/modules/LoginManagerChild.jsm"
|
||||||
);
|
);
|
||||||
|
|
||||||
ChromeUtils.defineModuleGetter(
|
ChromeUtils.defineModuleGetter(
|
||||||
this,
|
this,
|
||||||
"NewPasswordModel",
|
"NewPasswordModel",
|
||||||
"resource://gre/modules/NewPasswordModel.jsm"
|
"resource://gre/modules/NewPasswordModel.jsm"
|
||||||
);
|
);
|
||||||
|
|
||||||
XPCOMUtils.defineLazyServiceGetter(
|
XPCOMUtils.defineLazyServiceGetter(
|
||||||
this,
|
this,
|
||||||
"formFillController",
|
"formFillController",
|
||||||
|
@ -63,7 +61,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
"SHOULD_SHOW_ORIGIN",
|
"SHOULD_SHOW_ORIGIN",
|
||||||
"signon.showAutoCompleteOrigins"
|
"signon.showAutoCompleteOrigins"
|
||||||
);
|
);
|
||||||
|
|
||||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||||
return LoginHelper.createLogger("LoginAutoComplete");
|
return LoginHelper.createLogger("LoginAutoComplete");
|
||||||
});
|
});
|
||||||
|
@ -124,8 +121,8 @@ function findDuplicates(loginList) {
|
||||||
return duplicates;
|
return duplicates;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLocalizedString(key, formatArgs = null) {
|
function getLocalizedString(key, ...formatArgs) {
|
||||||
if (formatArgs) {
|
if (formatArgs.length) {
|
||||||
return passwordMgrBundle.formatStringFromName(key, formatArgs);
|
return passwordMgrBundle.formatStringFromName(key, formatArgs);
|
||||||
}
|
}
|
||||||
return passwordMgrBundle.GetStringFromName(key);
|
return passwordMgrBundle.GetStringFromName(key);
|
||||||
|
@ -149,14 +146,18 @@ class InsecureLoginFormAutocompleteItem extends AutocompleteItem {
|
||||||
|
|
||||||
XPCOMUtils.defineLazyGetter(this, "label", () => {
|
XPCOMUtils.defineLazyGetter(this, "label", () => {
|
||||||
let learnMoreString = getLocalizedString("insecureFieldWarningLearnMore");
|
let learnMoreString = getLocalizedString("insecureFieldWarningLearnMore");
|
||||||
return getLocalizedString("insecureFieldWarningDescription2", [
|
return getLocalizedString(
|
||||||
learnMoreString,
|
"insecureFieldWarningDescription2",
|
||||||
]);
|
learnMoreString
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoginAutocompleteItem extends AutocompleteItem {
|
class LoginAutocompleteItem extends AutocompleteItem {
|
||||||
|
login;
|
||||||
|
#actor;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
login,
|
login,
|
||||||
hasBeenTypePassword,
|
hasBeenTypePassword,
|
||||||
|
@ -165,23 +166,23 @@ class LoginAutocompleteItem extends AutocompleteItem {
|
||||||
isOriginMatched
|
isOriginMatched
|
||||||
) {
|
) {
|
||||||
super(SHOULD_SHOW_ORIGIN ? "loginWithOrigin" : "login");
|
super(SHOULD_SHOW_ORIGIN ? "loginWithOrigin" : "login");
|
||||||
this._login = login.QueryInterface(Ci.nsILoginMetaInfo);
|
this.login = login.QueryInterface(Ci.nsILoginMetaInfo);
|
||||||
this._actor = actor;
|
this.#actor = actor;
|
||||||
|
|
||||||
this._isDuplicateUsername =
|
let isDuplicateUsername =
|
||||||
login.username && duplicateUsernames.has(login.username);
|
login.username && duplicateUsernames.has(login.username);
|
||||||
|
|
||||||
XPCOMUtils.defineLazyGetter(this, "label", () => {
|
XPCOMUtils.defineLazyGetter(this, "label", () => {
|
||||||
let username = login.username;
|
let username = login.username;
|
||||||
// If login is empty or duplicated we want to append a modification date to it.
|
// If login is empty or duplicated we want to append a modification date to it.
|
||||||
if (!username || this._isDuplicateUsername) {
|
if (!username || isDuplicateUsername) {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
username = getLocalizedString("noUsername");
|
username = getLocalizedString("noUsername");
|
||||||
}
|
}
|
||||||
let time = dateAndTimeFormatter.format(
|
let time = dateAndTimeFormatter.format(
|
||||||
new Date(login.timePasswordChanged)
|
new Date(login.timePasswordChanged)
|
||||||
);
|
);
|
||||||
username = getLocalizedString("loginHostAge", [username, time]);
|
username = getLocalizedString("loginHostAge", username, time);
|
||||||
}
|
}
|
||||||
return username;
|
return username;
|
||||||
});
|
});
|
||||||
|
@ -194,7 +195,7 @@ class LoginAutocompleteItem extends AutocompleteItem {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
guid: login.guid,
|
guid: login.guid,
|
||||||
login,
|
login,
|
||||||
isDuplicateUsername: this._isDuplicateUsername,
|
isDuplicateUsername,
|
||||||
isOriginMatched,
|
isOriginMatched,
|
||||||
comment:
|
comment:
|
||||||
isOriginMatched && login.httpRealm === null
|
isOriginMatched && login.httpRealm === null
|
||||||
|
@ -205,13 +206,13 @@ class LoginAutocompleteItem extends AutocompleteItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromStorage() {
|
removeFromStorage() {
|
||||||
if (this._actor) {
|
if (this.#actor) {
|
||||||
let vanilla = LoginHelper.loginToVanillaObject(this._login);
|
let vanilla = LoginHelper.loginToVanillaObject(this.login);
|
||||||
this._actor.sendAsyncMessage("PasswordManager:removeLogin", {
|
this.#actor.sendAsyncMessage("PasswordManager:removeLogin", {
|
||||||
login: vanilla,
|
login: vanilla,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Services.logins.removeLogin(this._login);
|
Services.logins.removeLogin(this.login);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,22 +241,24 @@ class ImportableLearnMoreAutocompleteItem extends AutocompleteItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImportableLoginsAutocompleteItem extends AutocompleteItem {
|
class ImportableLoginsAutocompleteItem extends AutocompleteItem {
|
||||||
|
#actor;
|
||||||
|
|
||||||
constructor(browserId, hostname, actor) {
|
constructor(browserId, hostname, actor) {
|
||||||
super("importableLogins");
|
super("importableLogins");
|
||||||
this.label = browserId;
|
this.label = browserId;
|
||||||
this.comment = hostname;
|
this.comment = hostname;
|
||||||
this._actor = actor;
|
this.#actor = actor;
|
||||||
|
|
||||||
// This is sent for every item (re)shown, but the parent will debounce to
|
// This is sent for every item (re)shown, but the parent will debounce to
|
||||||
// reduce the count by 1 total.
|
// reduce the count by 1 total.
|
||||||
this._actor.sendAsyncMessage(
|
this.#actor.sendAsyncMessage(
|
||||||
"PasswordManager:decreaseSuggestImportCount",
|
"PasswordManager:decreaseSuggestImportCount",
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromStorage() {
|
removeFromStorage() {
|
||||||
this._actor.sendAsyncMessage(
|
this.#actor.sendAsyncMessage(
|
||||||
"PasswordManager:decreaseSuggestImportCount",
|
"PasswordManager:decreaseSuggestImportCount",
|
||||||
100
|
100
|
||||||
);
|
);
|
||||||
|
@ -283,217 +286,213 @@ class LoginsFooterAutocompleteItem extends AutocompleteItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
// nsIAutoCompleteResult implementation
|
// nsIAutoCompleteResult implementation
|
||||||
function LoginAutoCompleteResult(
|
class LoginAutoCompleteResult {
|
||||||
aSearchString,
|
#rows = [];
|
||||||
matchingLogins,
|
|
||||||
formOrigin,
|
|
||||||
{
|
|
||||||
generatedPassword,
|
|
||||||
willAutoSaveGeneratedPassword,
|
|
||||||
importable,
|
|
||||||
isSecure,
|
|
||||||
actor,
|
|
||||||
hasBeenTypePassword,
|
|
||||||
hostname,
|
|
||||||
telemetryEventData,
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
let hidingFooterOnPWFieldAutoOpened = false;
|
|
||||||
const importableBrowsers =
|
|
||||||
importable?.state === "import" && importable?.browsers;
|
|
||||||
function isFooterEnabled() {
|
|
||||||
// We need to check LoginHelper.enabled here since the insecure warning should
|
|
||||||
// appear even if pwmgr is disabled but the footer should never appear in that case.
|
|
||||||
if (!LoginHelper.showAutoCompleteFooter || !LoginHelper.enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't show the footer on non-empty password fields as it's not providing
|
constructor(
|
||||||
// value and only adding noise since a password was already filled.
|
aSearchString,
|
||||||
if (hasBeenTypePassword && aSearchString && !generatedPassword) {
|
matchingLogins,
|
||||||
log.debug("Hiding footer: non-empty password field");
|
formOrigin,
|
||||||
return false;
|
{
|
||||||
}
|
generatedPassword,
|
||||||
|
willAutoSaveGeneratedPassword,
|
||||||
if (
|
importable,
|
||||||
!importableBrowsers &&
|
isSecure,
|
||||||
!matchingLogins.length &&
|
|
||||||
!generatedPassword &&
|
|
||||||
hasBeenTypePassword &&
|
|
||||||
formFillController.passwordPopupAutomaticallyOpened
|
|
||||||
) {
|
|
||||||
hidingFooterOnPWFieldAutoOpened = true;
|
|
||||||
log.debug(
|
|
||||||
"Hiding footer: no logins and the popup was opened upon focus of the pw. field"
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchString = aSearchString;
|
|
||||||
|
|
||||||
// Build up the array of autocomplete rows to display.
|
|
||||||
this._rows = [];
|
|
||||||
|
|
||||||
// Insecure field warning comes first if it applies and is enabled.
|
|
||||||
if (!isSecure && LoginHelper.showInsecureFieldWarning) {
|
|
||||||
this._rows.push(new InsecureLoginFormAutocompleteItem());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saved login items
|
|
||||||
let formHostPort = LoginHelper.maybeGetHostPortForURL(formOrigin);
|
|
||||||
let logins = matchingLogins.sort(loginSort.bind(null, formHostPort));
|
|
||||||
let duplicateUsernames = findDuplicates(matchingLogins);
|
|
||||||
|
|
||||||
for (let login of logins) {
|
|
||||||
let item = new LoginAutocompleteItem(
|
|
||||||
login,
|
|
||||||
hasBeenTypePassword,
|
|
||||||
duplicateUsernames,
|
|
||||||
actor,
|
actor,
|
||||||
LoginHelper.isOriginMatching(login.origin, formOrigin, {
|
hasBeenTypePassword,
|
||||||
schemeUpgrades: LoginHelper.schemeUpgrades,
|
hostname,
|
||||||
})
|
telemetryEventData,
|
||||||
);
|
}
|
||||||
this._rows.push(item);
|
) {
|
||||||
}
|
let hidingFooterOnPWFieldAutoOpened = false;
|
||||||
|
const importableBrowsers =
|
||||||
|
importable?.state === "import" && importable?.browsers;
|
||||||
|
|
||||||
// The footer comes last if it's enabled
|
function isFooterEnabled() {
|
||||||
if (isFooterEnabled()) {
|
// We need to check LoginHelper.enabled here since the insecure warning should
|
||||||
if (generatedPassword) {
|
// appear even if pwmgr is disabled but the footer should never appear in that case.
|
||||||
this._rows.push(
|
if (!LoginHelper.showAutoCompleteFooter || !LoginHelper.enabled) {
|
||||||
new GeneratedPasswordAutocompleteItem(
|
return false;
|
||||||
generatedPassword,
|
}
|
||||||
willAutoSaveGeneratedPassword
|
|
||||||
)
|
// Don't show the footer on non-empty password fields as it's not providing
|
||||||
|
// value and only adding noise since a password was already filled.
|
||||||
|
if (hasBeenTypePassword && aSearchString && !generatedPassword) {
|
||||||
|
log.debug("Hiding footer: non-empty password field");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!importableBrowsers &&
|
||||||
|
!matchingLogins.length &&
|
||||||
|
!generatedPassword &&
|
||||||
|
hasBeenTypePassword &&
|
||||||
|
formFillController.passwordPopupAutomaticallyOpened
|
||||||
|
) {
|
||||||
|
hidingFooterOnPWFieldAutoOpened = true;
|
||||||
|
log.debug(
|
||||||
|
"Hiding footer: no logins and the popup was opened upon focus of the pw. field"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchString = aSearchString;
|
||||||
|
|
||||||
|
// Insecure field warning comes first if it applies and is enabled.
|
||||||
|
if (!isSecure && LoginHelper.showInsecureFieldWarning) {
|
||||||
|
this.#rows.push(new InsecureLoginFormAutocompleteItem());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saved login items
|
||||||
|
let formHostPort = LoginHelper.maybeGetHostPortForURL(formOrigin);
|
||||||
|
let logins = matchingLogins.sort(loginSort.bind(null, formHostPort));
|
||||||
|
let duplicateUsernames = findDuplicates(matchingLogins);
|
||||||
|
|
||||||
|
for (let login of logins) {
|
||||||
|
let item = new LoginAutocompleteItem(
|
||||||
|
login,
|
||||||
|
hasBeenTypePassword,
|
||||||
|
duplicateUsernames,
|
||||||
|
actor,
|
||||||
|
LoginHelper.isOriginMatching(login.origin, formOrigin, {
|
||||||
|
schemeUpgrades: LoginHelper.schemeUpgrades,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.#rows.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The footer comes last if it's enabled
|
||||||
|
if (isFooterEnabled()) {
|
||||||
|
if (generatedPassword) {
|
||||||
|
this.#rows.push(
|
||||||
|
new GeneratedPasswordAutocompleteItem(
|
||||||
|
generatedPassword,
|
||||||
|
willAutoSaveGeneratedPassword
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggest importing logins if there are none found.
|
||||||
|
if (!logins.length && importableBrowsers) {
|
||||||
|
this.#rows.push(
|
||||||
|
...importableBrowsers.map(
|
||||||
|
browserId =>
|
||||||
|
new ImportableLoginsAutocompleteItem(browserId, hostname, actor)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.#rows.push(new ImportableLearnMoreAutocompleteItem());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#rows.push(
|
||||||
|
new LoginsFooterAutocompleteItem(hostname, telemetryEventData)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suggest importing logins if there are none found.
|
// Determine the result code and default index.
|
||||||
if (!logins.length && importableBrowsers) {
|
if (this.matchCount > 0) {
|
||||||
this._rows.push(
|
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
|
||||||
...importableBrowsers.map(
|
this.defaultIndex = 0;
|
||||||
browserId =>
|
} else if (hidingFooterOnPWFieldAutoOpened) {
|
||||||
new ImportableLoginsAutocompleteItem(browserId, hostname, actor)
|
// We use a failure result so that the empty results aren't re-used for when
|
||||||
)
|
// the user tries to manually open the popup (we want the footer in that case).
|
||||||
);
|
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_FAILURE;
|
||||||
this._rows.push(new ImportableLearnMoreAutocompleteItem());
|
this.defaultIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._rows.push(
|
|
||||||
new LoginsFooterAutocompleteItem(hostname, telemetryEventData)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the result code and default index.
|
QueryInterface = ChromeUtils.generateQI([
|
||||||
if (this.matchCount > 0) {
|
|
||||||
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
|
|
||||||
this.defaultIndex = 0;
|
|
||||||
} else if (hidingFooterOnPWFieldAutoOpened) {
|
|
||||||
// We use a failure result so that the empty results aren't re-used for when
|
|
||||||
// the user tries to manually open the popup (we want the footer in that case).
|
|
||||||
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_FAILURE;
|
|
||||||
this.defaultIndex = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LoginAutoCompleteResult.prototype = {
|
|
||||||
QueryInterface: ChromeUtils.generateQI([
|
|
||||||
"nsIAutoCompleteResult",
|
"nsIAutoCompleteResult",
|
||||||
"nsISupportsWeakReference",
|
"nsISupportsWeakReference",
|
||||||
]),
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accessed via .wrappedJSObject
|
* Accessed via .wrappedJSObject
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
get logins() {
|
get logins() {
|
||||||
return this._rows
|
return this.#rows
|
||||||
.filter(item => {
|
.filter(item => item instanceof LoginAutocompleteItem)
|
||||||
return item.constructor === LoginAutocompleteItem;
|
.map(item => item.login);
|
||||||
})
|
}
|
||||||
.map(item => item._login);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Allow autoCompleteSearch to get at the JS object so it can
|
// Allow autoCompleteSearch to get at the JS object so it can
|
||||||
// modify some readonly properties for internal use.
|
// modify some readonly properties for internal use.
|
||||||
get wrappedJSObject() {
|
get wrappedJSObject() {
|
||||||
return this;
|
return this;
|
||||||
},
|
}
|
||||||
|
|
||||||
// Interfaces from idl...
|
// Interfaces from idl...
|
||||||
searchString: null,
|
searchString = null;
|
||||||
searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
|
searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
|
||||||
defaultIndex: -1,
|
defaultIndex = -1;
|
||||||
errorDescription: "",
|
errorDescription = "";
|
||||||
|
|
||||||
get matchCount() {
|
get matchCount() {
|
||||||
return this._rows.length;
|
return this.#rows.length;
|
||||||
},
|
}
|
||||||
|
|
||||||
|
#throwOnBadIndex(index) {
|
||||||
|
if (index < 0 || index >= this.matchCount) {
|
||||||
|
throw new Error("Index out of range.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getValueAt(index) {
|
getValueAt(index) {
|
||||||
if (index < 0 || index >= this.matchCount) {
|
this.#throwOnBadIndex(index);
|
||||||
throw new Error("Index out of range.");
|
return this.#rows[index].value;
|
||||||
}
|
}
|
||||||
return this._rows[index].value;
|
|
||||||
},
|
|
||||||
|
|
||||||
getLabelAt(index) {
|
getLabelAt(index) {
|
||||||
if (index < 0 || index >= this.matchCount) {
|
this.#throwOnBadIndex(index);
|
||||||
throw new Error("Index out of range.");
|
return this.#rows[index].label;
|
||||||
}
|
}
|
||||||
return this._rows[index].label;
|
|
||||||
},
|
|
||||||
|
|
||||||
getCommentAt(index) {
|
getCommentAt(index) {
|
||||||
if (index < 0 || index >= this.matchCount) {
|
this.#throwOnBadIndex(index);
|
||||||
throw new Error("Index out of range.");
|
return this.#rows[index].comment;
|
||||||
}
|
}
|
||||||
return this._rows[index].comment;
|
|
||||||
},
|
|
||||||
|
|
||||||
getStyleAt(index) {
|
getStyleAt(index) {
|
||||||
return this._rows[index].style;
|
this.#throwOnBadIndex(index);
|
||||||
},
|
return this.#rows[index].style;
|
||||||
|
}
|
||||||
|
|
||||||
getImageAt(index) {
|
getImageAt(index) {
|
||||||
|
this.#throwOnBadIndex(index);
|
||||||
return "";
|
return "";
|
||||||
},
|
}
|
||||||
|
|
||||||
getFinalCompleteValueAt(index) {
|
getFinalCompleteValueAt(index) {
|
||||||
return this.getValueAt(index);
|
return this.getValueAt(index);
|
||||||
},
|
}
|
||||||
|
|
||||||
isRemovableAt(index) {
|
isRemovableAt(index) {
|
||||||
|
this.#throwOnBadIndex(index);
|
||||||
return true;
|
return true;
|
||||||
},
|
}
|
||||||
|
|
||||||
removeValueAt(index) {
|
removeValueAt(index) {
|
||||||
if (index < 0 || index >= this.matchCount) {
|
this.#throwOnBadIndex(index);
|
||||||
throw new Error("Index out of range.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let [removedItem] = this._rows.splice(index, 1);
|
let [removedItem] = this.#rows.splice(index, 1);
|
||||||
|
|
||||||
if (this.defaultIndex > this._rows.length) {
|
if (this.defaultIndex > this.#rows.length) {
|
||||||
this.defaultIndex--;
|
this.defaultIndex--;
|
||||||
}
|
}
|
||||||
|
|
||||||
removedItem.removeFromStorage();
|
removedItem.removeFromStorage();
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
|
||||||
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(["nsILoginAutoCompleteSearch"]),
|
|
||||||
|
|
||||||
_autoCompleteLookupPromise: null,
|
class LoginAutoComplete {
|
||||||
_cachedNewPasswordScore: null,
|
// HTMLInputElement to number, the element's new-password heuristic confidence score
|
||||||
|
#cachedNewPasswordScore = new WeakMap();
|
||||||
|
#autoCompleteLookupPromise = null;
|
||||||
|
classID = Components.ID("{2bdac17c-53f1-4896-a521-682ccdeef3a8}");
|
||||||
|
QueryInterface = ChromeUtils.generateQI(["nsILoginAutoCompleteSearch"]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Yuck. This is called directly by satchel:
|
* Yuck. This is called directly by satchel:
|
||||||
|
@ -519,7 +518,6 @@ LoginAutoComplete.prototype = {
|
||||||
let searchStartTimeMS = Services.telemetry.msSystemNow();
|
let searchStartTimeMS = Services.telemetry.msSystemNow();
|
||||||
|
|
||||||
// Show the insecure login warning in the passwords field on null principal documents.
|
// Show the insecure login warning in the passwords field on null principal documents.
|
||||||
let isSecure = !isNullPrincipal;
|
|
||||||
// Avoid loading InsecurePasswordUtils.jsm in a sandboxed document (e.g. an ad. frame) if we
|
// Avoid loading InsecurePasswordUtils.jsm in a sandboxed document (e.g. an ad. frame) if we
|
||||||
// already know it has a null principal and will therefore get the insecure autocomplete
|
// already know it has a null principal and will therefore get the insecure autocomplete
|
||||||
// treatment.
|
// treatment.
|
||||||
|
@ -533,20 +531,16 @@ LoginAutoComplete.prototype = {
|
||||||
// document is sandboxing a document, it probably doesn't want that sandboxed document to be
|
// document is sandboxing a document, it probably doesn't want that sandboxed document to be
|
||||||
// able to affect the identity icon in the address bar by adding a password field.
|
// able to affect the identity icon in the address bar by adding a password field.
|
||||||
let form = LoginFormFactory.createFromField(aElement);
|
let form = LoginFormFactory.createFromField(aElement);
|
||||||
if (isSecure) {
|
let isSecure = !isNullPrincipal && InsecurePasswordUtils.isFormSecure(form);
|
||||||
isSecure = InsecurePasswordUtils.isFormSecure(form);
|
|
||||||
}
|
|
||||||
let { hasBeenTypePassword } = aElement;
|
let { hasBeenTypePassword } = aElement;
|
||||||
let hostname = aElement.ownerDocument.documentURIObject.host;
|
let hostname = aElement.ownerDocument.documentURIObject.host;
|
||||||
let formOrigin = LoginHelper.getLoginOrigin(
|
let formOrigin = LoginHelper.getLoginOrigin(
|
||||||
aElement.ownerDocument.documentURI
|
aElement.ownerDocument.documentURI
|
||||||
);
|
);
|
||||||
|
|
||||||
let loginManagerActor = LoginManagerChild.forWindow(aElement.ownerGlobal);
|
let loginManagerActor = LoginManagerChild.forWindow(aElement.ownerGlobal);
|
||||||
|
|
||||||
let completeSearch = async autoCompleteLookupPromise => {
|
let completeSearch = async autoCompleteLookupPromise => {
|
||||||
// Assign to the member synchronously before awaiting the Promise.
|
// Assign to the member synchronously before awaiting the Promise.
|
||||||
this._autoCompleteLookupPromise = autoCompleteLookupPromise;
|
this.#autoCompleteLookupPromise = autoCompleteLookupPromise;
|
||||||
|
|
||||||
let {
|
let {
|
||||||
generatedPassword,
|
generatedPassword,
|
||||||
|
@ -559,7 +553,7 @@ LoginAutoComplete.prototype = {
|
||||||
// results, don't bother reporting them.
|
// results, don't bother reporting them.
|
||||||
// N.B. This check must occur after the `await` above for it to be
|
// N.B. This check must occur after the `await` above for it to be
|
||||||
// effective.
|
// effective.
|
||||||
if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) {
|
if (this.#autoCompleteLookupPromise !== autoCompleteLookupPromise) {
|
||||||
log.debug("ignoring result from previous search");
|
log.debug("ignoring result from previous search");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -573,7 +567,7 @@ LoginAutoComplete.prototype = {
|
||||||
stringLength: aSearchString.length,
|
stringLength: aSearchString.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._autoCompleteLookupPromise = null;
|
this.#autoCompleteLookupPromise = null;
|
||||||
let results = new LoginAutoCompleteResult(
|
let results = new LoginAutoCompleteResult(
|
||||||
aSearchString,
|
aSearchString,
|
||||||
logins,
|
logins,
|
||||||
|
@ -627,7 +621,7 @@ LoginAutoComplete.prototype = {
|
||||||
previousResult = null;
|
previousResult = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let acLookupPromise = this._requestAutoCompleteResultsFromParent({
|
let acLookupPromise = this.#requestAutoCompleteResultsFromParent({
|
||||||
searchString: aSearchString,
|
searchString: aSearchString,
|
||||||
previousResult,
|
previousResult,
|
||||||
inputElement: aElement,
|
inputElement: aElement,
|
||||||
|
@ -635,13 +629,13 @@ LoginAutoComplete.prototype = {
|
||||||
hasBeenTypePassword,
|
hasBeenTypePassword,
|
||||||
});
|
});
|
||||||
completeSearch(acLookupPromise).catch(log.error.bind(log));
|
completeSearch(acLookupPromise).catch(log.error.bind(log));
|
||||||
},
|
}
|
||||||
|
|
||||||
stopSearch() {
|
stopSearch() {
|
||||||
this._autoCompleteLookupPromise = null;
|
this.#autoCompleteLookupPromise = null;
|
||||||
},
|
}
|
||||||
|
|
||||||
async _requestAutoCompleteResultsFromParent({
|
async #requestAutoCompleteResultsFromParent({
|
||||||
searchString,
|
searchString,
|
||||||
previousResult,
|
previousResult,
|
||||||
inputElement,
|
inputElement,
|
||||||
|
@ -664,7 +658,7 @@ LoginAutoComplete.prototype = {
|
||||||
// autocomplete="new-password" attribute.
|
// autocomplete="new-password" attribute.
|
||||||
isProbablyANewPasswordField =
|
isProbablyANewPasswordField =
|
||||||
autocompleteInfo.fieldName == "new-password" ||
|
autocompleteInfo.fieldName == "new-password" ||
|
||||||
this._isProbablyANewPasswordField(inputElement);
|
this.isProbablyANewPasswordField(inputElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
let messageData = {
|
let messageData = {
|
||||||
|
@ -686,9 +680,7 @@ LoginAutoComplete.prototype = {
|
||||||
isSecure: messageData.isSecure,
|
isSecure: messageData.isSecure,
|
||||||
hasBeenTypePassword,
|
hasBeenTypePassword,
|
||||||
isProbablyANewPasswordField,
|
isProbablyANewPasswordField,
|
||||||
searchString: hasBeenTypePassword
|
searchStringLength: searchString.length,
|
||||||
? "*".repeat(searchString.length)
|
|
||||||
: searchString,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = await loginManagerActor.sendQuery(
|
let result = await loginManagerActor.sendQuery(
|
||||||
|
@ -702,16 +694,16 @@ LoginAutoComplete.prototype = {
|
||||||
logins: LoginHelper.vanillaObjectsToLogins(result.logins),
|
logins: LoginHelper.vanillaObjectsToLogins(result.logins),
|
||||||
willAutoSaveGeneratedPassword: result.willAutoSaveGeneratedPassword,
|
willAutoSaveGeneratedPassword: result.willAutoSaveGeneratedPassword,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
_isProbablyANewPasswordField(inputElement) {
|
isProbablyANewPasswordField(inputElement) {
|
||||||
const threshold = LoginHelper.generationConfidenceThreshold;
|
const threshold = LoginHelper.generationConfidenceThreshold;
|
||||||
if (threshold == -1) {
|
if (threshold == -1) {
|
||||||
// Fathom is disabled
|
// Fathom is disabled
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let score = this._cachedNewPasswordScore.get(inputElement);
|
let score = this.#cachedNewPasswordScore.get(inputElement);
|
||||||
if (score) {
|
if (score) {
|
||||||
return score >= threshold;
|
return score >= threshold;
|
||||||
}
|
}
|
||||||
|
@ -719,10 +711,10 @@ LoginAutoComplete.prototype = {
|
||||||
const { rules, type } = NewPasswordModel;
|
const { rules, type } = NewPasswordModel;
|
||||||
const results = rules.against(inputElement);
|
const results = rules.against(inputElement);
|
||||||
score = results.get(inputElement).scoreFor(type);
|
score = results.get(inputElement).scoreFor(type);
|
||||||
this._cachedNewPasswordScore.set(inputElement, score);
|
this.#cachedNewPasswordScore.set(inputElement, score);
|
||||||
return score >= threshold;
|
return score >= threshold;
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let gAutoCompleteListener = {
|
let gAutoCompleteListener = {
|
||||||
// Input element on which enter keydown event was fired.
|
// Input element on which enter keydown event was fired.
|
||||||
|
|
|
@ -94,7 +94,7 @@ add_task(async function test_generated_noLogins() {
|
||||||
...NEW_PASSWORD_TEMPLATE_ARG,
|
...NEW_PASSWORD_TEMPLATE_ARG,
|
||||||
...{
|
...{
|
||||||
// This is false when there is no autocomplete="new-password" attribute &&
|
// This is false when there is no autocomplete="new-password" attribute &&
|
||||||
// LoginAutoComplete._isProbablyANewPasswordField returns false
|
// LoginAutoComplete.isProbablyANewPasswordField returns false
|
||||||
isProbablyANewPasswordField: false,
|
isProbablyANewPasswordField: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Test for LoginAutoComplete._isProbablyANewPasswordField.
|
* Test for LoginAutoComplete.isProbablyANewPasswordField.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
@ -43,7 +43,7 @@ const LABELLEDBY_SHADOW_TESTCASE = labelledByDocument();
|
||||||
|
|
||||||
const TESTCASES = [
|
const TESTCASES = [
|
||||||
// Note there is no test case for `<input type="password" autocomplete="new-password">`
|
// Note there is no test case for `<input type="password" autocomplete="new-password">`
|
||||||
// since _isProbablyANewPasswordField explicitly does not run in that case.
|
// since isProbablyANewPasswordField explicitly does not run in that case.
|
||||||
{
|
{
|
||||||
description: "Basic login form",
|
description: "Basic login form",
|
||||||
document: `
|
document: `
|
||||||
|
@ -146,7 +146,7 @@ add_task(async function test_returns_false_when_pref_disabled() {
|
||||||
);
|
);
|
||||||
for (let [i, input] of testcase.inputs ||
|
for (let [i, input] of testcase.inputs ||
|
||||||
document.querySelectorAll(`input[type="password"]`).entries()) {
|
document.querySelectorAll(`input[type="password"]`).entries()) {
|
||||||
const result = LoginAutoComplete._isProbablyANewPasswordField(input);
|
const result = LoginAutoComplete.isProbablyANewPasswordField(input);
|
||||||
Assert.strictEqual(
|
Assert.strictEqual(
|
||||||
result,
|
result,
|
||||||
false,
|
false,
|
||||||
|
@ -177,7 +177,7 @@ for (let testcase of TESTCASES) {
|
||||||
const results = [];
|
const results = [];
|
||||||
for (let input of testcase.inputs ||
|
for (let input of testcase.inputs ||
|
||||||
document.querySelectorAll(`input[type="password"]`)) {
|
document.querySelectorAll(`input[type="password"]`)) {
|
||||||
const result = LoginAutoComplete._isProbablyANewPasswordField(input);
|
const result = LoginAutoComplete.isProbablyANewPasswordField(input);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче