зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
1e6ebcef70
Коммит
077dbf3832
|
@ -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;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче