Bug 1618058 - [2.c.7] Implement Autocomplete API backend. r=geckoview-reviewers,MattN,aklotz

Differential Revision: https://phabricator.services.mozilla.com/D73753
This commit is contained in:
Eugen Sawin 2020-05-25 18:45:25 +00:00
Родитель 1e6ebcef70
Коммит 077dbf3832
9 изменённых файлов: 242 добавлений и 53 удалений

Просмотреть файл

@ -82,3 +82,8 @@ pref("media.eme.require-app-approval", true);
// Enable the Process Priority Manager
pref("dom.ipc.processPriorityManager.enabled", true);
pref("signon.debug", false);
pref("signon.showAutoCompleteFooter", true);
pref("security.insecure_field_warning.contextual.enabled", true);
pref("toolkit.autocomplete.delegate", true);

Просмотреть файл

@ -7,7 +7,6 @@
const EXPORTED_SYMBOLS = [
"GeckoViewAutocomplete",
"LoginEntry",
"SelectLabel",
"SelectOption",
];
@ -21,7 +20,7 @@ const { GeckoViewUtils } = ChromeUtils.import(
XPCOMUtils.defineLazyModuleGetters(this, {
EventDispatcher: "resource://gre/modules/Messaging.jsm",
PromptDelegate: "resource://gre/modules/GeckoViewPrompt.jsm",
GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.jsm",
});
XPCOMUtils.defineLazyGetter(this, "LoginInfo", () =>
@ -33,11 +32,20 @@ XPCOMUtils.defineLazyGetter(this, "LoginInfo", () =>
);
class LoginEntry {
constructor({ origin, formActionOrigin, httpRealm, username, password,
guid, timeCreated, timeLastUsed, timePasswordChanged,
timesUsed }) {
constructor({
origin,
formActionOrigin,
httpRealm,
username,
password,
guid,
timeCreated,
timeLastUsed,
timePasswordChanged,
timesUsed,
}) {
this.origin = origin ?? null;
this.formActionOrigin = formActionOrigin ?? null;
this.formActionOrigin = formActionOrigin ?? null;
this.httpRealm = httpRealm ?? null;
this.username = username ?? null;
this.password = password ?? null;
@ -98,36 +106,19 @@ class LoginEntry {
}
}
class SelectLabel {
// Sync with Autocomplete.SelectOption.Label.Type in Autocomplete.java.
static Type = {
NONE: 0,
PRIMARY: 1,
SECONDARY: 2
};
constructor({ type, value }) {
this.type = Object.values(SelectLabel.Type).includes(type)
? type
: SelectLabel.Type.NONE;
this.value = value ?? null;
}
}
class SelectOption {
// Sync with Autocomplete.LoginSelectOption.Hint in Autocomplete.java.
static Hint = {
NONE: 0,
GENERATED: (1 << 0),
INSECURE_FORM: (1 << 1),
GENERATED: 1 << 0,
INSECURE_FORM: 1 << 1,
DUPLICATE_USERNAME: 1 << 2,
MATCHING_ORIGIN: 1 << 3,
};
constructor({ value, hint, labels }) {
constructor({ value, hint }) {
this.value = value ?? null;
this.hint = Object.values(SelectOption.Hint).includes(hint)
? hint
: SelectOption.Hint.NONE;
this.labels = labels ?? [];
this.hint = hint ?? SelectOption.Hint.NONE;
}
}
@ -190,32 +181,155 @@ const GeckoViewAutocomplete = {
});
},
_numActiveOnLoginSelect: 0,
/**
* Delegates login entry selection.
* Call this when there are multiple login entry option for a form to delegate
* the selection.
*
* @param aBrowser The browser instance the triggered the selection.
* @param aOptions The list of {SelectOption} depicting viable options.
*/
onLoginSelect(aBrowser, aOptions) {
debug`onLoginSelect ${aOptions}`;
return new Promise((resolve, reject) => {
if (!aBrowser || !aOptions) {
debug`onLoginSelect Rejecting - no browser or options provided`;
reject();
return;
}
const prompt = new PromptDelegate(aBrowser.ownerGlobal);
const prompt = new GeckoViewPrompter(aBrowser.ownerGlobal);
prompt.asyncShowPrompt(
{
type: "Autocomplete:Select:Login",
options: aOptions,
},
result => {
if (!result || result.value === undefined) {
if (!result || !result.selection) {
reject();
return;
}
const loginInfo = LoginEntry.parse(result.value).toLoginInfo();
resolve(loginInfo);
const option = new SelectOption({
value: LoginEntry.parse(result.selection.value),
hint: result.selection.hint,
});
resolve(option);
}
);
});
},
async delegateSelection({
browsingContext,
options,
inputElementIdentifier,
formOrigin,
}) {
debug`delegateSelection ${options}`;
if (!options.length) {
return;
}
let insecureHint = SelectOption.Hint.NONE;
let loginStyle = null;
const selectOptions = [];
for (const option of options) {
switch (option.style) {
case "insecureWarning": {
// We depend on the insecure warning to be the first option.
insecureHint = SelectOption.Hint.INSECURE_FORM;
break;
}
case "generatedPassword": {
const comment = JSON.parse(option.comment);
selectOptions.push(
new SelectOption({
value: new LoginEntry({
password: comment.generatedPassword,
}),
hint: SelectOption.Hint.GENERATED | insecureHint,
})
);
break;
}
case "login":
// Fallthrough.
case "loginWithOrigin": {
loginStyle = option.style;
const comment = JSON.parse(option.comment);
let hint = SelectOption.Hint.NONE | insecureHint;
if (comment.isDuplicateUsername) {
hint |= SelectOption.Hint.DUPLICATE_USERNAME;
}
if (comment.isOriginMatched) {
hint |= SelectOption.Hint.MATCHING_ORIGIN;
}
selectOptions.push(
new SelectOption({
value: LoginEntry.parse(comment.login),
hint,
})
);
break;
}
}
}
if (selectOptions.length < 1) {
debug`Abort delegateSelection - no valid options provided`;
return;
}
if (this._numActiveOnLoginSelect > 0) {
debug`Abort delegateSelection - there is already one delegation active`;
return;
}
++this._numActiveOnLoginSelect;
const browser = browsingContext.top.embedderElement;
const selectedOption = await this.onLoginSelect(
browser,
selectOptions
).catch(_ => {
debug`No GV delegate attached`;
});
--this._numActiveOnLoginSelect;
debug`delegateSelection selected option: ${selectedOption}`;
const selectedLogin = selectedOption?.value?.toLoginInfo();
if (!selectedLogin) {
debug`Abort delegateSelection - no login entry selected`;
return;
}
debug`delegateSelection - filling form`;
const actor = browsingContext.currentWindowGlobal.getActor("LoginManager");
await actor.fillForm({
browser,
inputElementIdentifier,
loginFormOrigin: formOrigin,
login: selectedLogin,
style:
selectedOption.hint & SelectOption.Hint.GENERATED
? "generatedPassword"
: loginStyle,
});
debug`delegateSelection - form filled`;
},
};
const { debug } = GeckoViewUtils.initLogging("GeckoViewAutocomplete");

Просмотреть файл

@ -18,6 +18,18 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/BrowserUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"ContentDOMReference",
"resource://gre/modules/ContentDOMReference.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"LoginHelper",
"resource://gre/modules/LoginHelper.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"formFill",
@ -211,11 +223,17 @@ class AutoCompleteChild extends JSWindowActorChild {
let window = element.ownerGlobal;
let dir = window.getComputedStyle(element).direction;
let results = this.getResultsFromController(input);
let formOrigin = LoginHelper.getLoginOrigin(
element.ownerDocument.documentURI
);
let inputElementIdentifier = ContentDOMReference.get(element);
this.sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", {
results,
rect,
dir,
inputElementIdentifier,
formOrigin,
});
this._input = input;

Просмотреть файл

@ -8,6 +8,21 @@ var EXPORTED_SYMBOLS = ["AutoCompleteParent"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.jsm",
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"DELEGATE_AUTOCOMPLETE",
"toolkit.autocomplete.delegate",
false
);
// Stores the browser and actor that has the active popup, used by formfill
let currentBrowserWeakRef = null;
let currentActor = null;
@ -358,7 +373,8 @@ class AutoCompleteParent extends JSWindowActorParent {
receiveMessage(message) {
let browser = this.browsingContext.top.embedderElement;
if (!browser || !browser.autoCompletePopup) {
if (!browser || (!DELEGATE_AUTOCOMPLETE && !browser.autoCompletePopup)) {
// If there is no browser or popup, just make sure that the popup has been closed.
if (this.openedPopup) {
this.openedPopup.closePopup();
@ -379,14 +395,24 @@ class AutoCompleteParent extends JSWindowActorParent {
}
case "FormAutoComplete:MaybeOpenPopup": {
let { results, rect, dir } = message.data;
this.showPopupWithResults({
let {
results,
rect,
dir,
results,
});
this.notifyListeners();
inputElementIdentifier,
formOrigin,
} = message.data;
if (DELEGATE_AUTOCOMPLETE) {
GeckoViewAutocomplete.delegateSelection({
browsingContext: this.browsingContext,
options: results,
inputElementIdentifier,
formOrigin,
});
} else {
this.showPopupWithResults({ results, rect, dir });
this.notifyListeners();
}
break;
}

Просмотреть файл

@ -168,10 +168,13 @@ class LoginAutocompleteItem extends AutocompleteItem {
this._login = login.QueryInterface(Ci.nsILoginMetaInfo);
this._actor = actor;
this._isDuplicateUsername =
login.username && duplicateUsernames.has(login.username);
XPCOMUtils.defineLazyGetter(this, "label", () => {
let username = login.username;
// If login is empty or duplicated we want to append a modification date to it.
if (!username || duplicateUsernames.has(username)) {
if (!username || this._isDuplicateUsername) {
if (!username) {
username = getLocalizedString("noUsername");
}
@ -190,6 +193,9 @@ class LoginAutocompleteItem extends AutocompleteItem {
XPCOMUtils.defineLazyGetter(this, "comment", () => {
return JSON.stringify({
guid: login.guid,
login,
isDuplicateUsername: this._isDuplicateUsername,
isOriginMatched,
comment:
isOriginMatched && login.httpRealm === null
? getLocalizedString("displaySameOrigin")

Просмотреть файл

@ -193,13 +193,7 @@ const observer = {
);
loginManagerChild.onFieldAutoComplete(focusedInput, details.guid);
} else if (style == "generatedPassword") {
loginManagerChild._highlightFilledField(focusedInput);
loginManagerChild._passwordEditedOrGenerated(focusedInput, {
triggeredByFillingGenerated: true,
});
loginManagerChild._fillConfirmFieldWithGeneratedPassword(
focusedInput
);
loginManagerChild._filledWithGeneratedPassword(focusedInput);
}
break;
}
@ -518,6 +512,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
recipes: msg.data.recipes,
inputElementIdentifier: msg.data.inputElementIdentifier,
originMatches: msg.data.originMatches,
style: msg.data.style,
});
break;
}
@ -951,6 +946,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
recipes,
inputElementIdentifier,
originMatches,
style,
}) {
if (!inputElementIdentifier) {
log("fillForm: No input element specified");
@ -993,6 +989,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
clobberUsername,
clobberPassword: true,
userTriggered: true,
style,
});
}
@ -1734,6 +1731,19 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
this._togglePasswordFieldMasking(passwordField, false);
}
/**
* The password field has been filled with a generated password, ensure the
* field is handled accordingly.
* @param {HTMLInputElement} passwordField
*/
_filledWithGeneratedPassword(passwordField) {
this._highlightFilledField(passwordField);
this._passwordEditedOrGenerated(passwordField, {
triggeredByFillingGenerated: true,
});
this._fillConfirmFieldWithGeneratedPassword(passwordField);
}
/**
* Notify the parent that a generated password was filled into a field or
* edited so that it can potentially be saved.
@ -1977,6 +1987,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
clobberUsername = false,
clobberPassword = false,
userTriggered = false,
style = null,
} = {}
) {
if (ChromeUtils.getClassName(form) === "HTMLFormElement") {
@ -2289,6 +2300,10 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
this._highlightFilledField(passwordField);
if (style && style === "generatedPassword") {
this._filledWithGeneratedPassword(passwordField);
}
log("_fillForm succeeded");
autofillResult = AUTOFILL_RESULT.FILLED;
} catch (ex) {

Просмотреть файл

@ -312,7 +312,13 @@ class LoginManagerParent extends JSWindowActorParent {
* Trigger a login form fill and send relevant data (e.g. logins and recipes)
* to the child process (LoginManagerChild).
*/
async fillForm({ browser, loginFormOrigin, login, inputElementIdentifier }) {
async fillForm({
browser,
loginFormOrigin,
login,
inputElementIdentifier,
style,
}) {
let recipes = [];
if (loginFormOrigin) {
let formHost;
@ -339,6 +345,7 @@ class LoginManagerParent extends JSWindowActorParent {
originMatches,
logins: jsLogins,
recipes,
style,
});
}
@ -517,7 +524,7 @@ class LoginManagerParent extends JSWindowActorParent {
Services.logins.getLoginSavingEnabled(formOrigin)))
) {
generatedPassword = this.getGeneratedPassword();
let potentialConflictingLogins = LoginHelper.searchLoginsWithObject({
let potentialConflictingLogins = await Services.logins.searchLoginsAsync({
origin: formOrigin,
formActionOrigin: actionOrigin,
httpRealm: null,

Просмотреть файл

@ -43,6 +43,7 @@ EXTRA_JS_MODULES += [
'LoginRecipes.jsm',
'NewPasswordModel.jsm',
'OSCrypto.jsm',
'PasswordGenerator.jsm',
'storage-json.js',
]
@ -54,7 +55,6 @@ else:
EXTRA_JS_MODULES += [
'LoginImport.jsm',
'LoginStore.jsm',
'PasswordGenerator.jsm',
]
if CONFIG['OS_TARGET'] == 'WINNT':

Просмотреть файл

@ -966,7 +966,6 @@ nsresult nsFormFillController::HandleFocus(HTMLInputElement* aInput) {
return NS_OK;
}
#ifndef ANDROID
// If this focus doesn't follow a right click within our specified
// threshold then show the autocomplete popup for all password fields.
// This is done to avoid showing both the context menu and the popup
@ -992,7 +991,6 @@ nsresult nsFormFillController::HandleFocus(HTMLInputElement* aInput) {
mPasswordPopupAutomaticallyOpened = true;
ShowPopup();
}
#endif
return NS_OK;
}