зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1848472 - Injecting Firefox Relay integration into Form Autofill r=credential-management-reviewers,dimi
Differential Revision: https://phabricator.services.mozilla.com/D186078
This commit is contained in:
Родитель
bc5116b463
Коммит
238b86456d
|
@ -496,7 +496,7 @@ async function getAutofillRecords(data) {
|
|||
name: "FormAutofill:GetRecords",
|
||||
data,
|
||||
});
|
||||
return records?.length ?? 0;
|
||||
return records?.records?.length ?? 0;
|
||||
}
|
||||
|
||||
// Attribution data can be encoded multiple times so we need this function to
|
||||
|
|
|
@ -650,7 +650,9 @@ function emulateMessageToBrowser(name, data) {
|
|||
|
||||
function getRecords(data) {
|
||||
info(`expecting record retrievals: ${data.collectionName}`);
|
||||
return emulateMessageToBrowser("FormAutofill:GetRecords", data);
|
||||
return emulateMessageToBrowser("FormAutofill:GetRecords", data).then(
|
||||
result => result.records
|
||||
);
|
||||
}
|
||||
|
||||
function getAddresses() {
|
||||
|
|
|
@ -31,13 +31,15 @@ var ParentUtils = {
|
|||
},
|
||||
|
||||
_getRecords(collectionName) {
|
||||
return this.getFormAutofillActor().receiveMessage({
|
||||
name: "FormAutofill:GetRecords",
|
||||
data: {
|
||||
searchString: "",
|
||||
collectionName,
|
||||
},
|
||||
});
|
||||
return this.getFormAutofillActor()
|
||||
.receiveMessage({
|
||||
name: "FormAutofill:GetRecords",
|
||||
data: {
|
||||
searchString: "",
|
||||
collectionName,
|
||||
},
|
||||
})
|
||||
.then(result => result.records);
|
||||
},
|
||||
|
||||
async _storageChangeObserved({
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
* Form Autofill content process module.
|
||||
*/
|
||||
|
||||
import { GenericAutocompleteItem } from "resource://gre/modules/FillHelpers.sys.mjs";
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
|
||||
const Cm = Components.manager;
|
||||
|
@ -19,7 +21,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
|
||||
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
|
||||
FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
|
||||
FormLikeFactory: "resource://gre/modules/FormLikeFactory.sys.mjs",
|
||||
InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
|
||||
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
|
||||
SignUpFormRuleset: "resource://gre/modules/SignUpFormRuleset.sys.mjs",
|
||||
});
|
||||
|
||||
const autocompleteController = Cc[
|
||||
|
@ -200,7 +205,7 @@ AutofillProfileAutoCompleteSearch.prototype = {
|
|||
};
|
||||
|
||||
pendingSearchResult = this._getRecords(activeInput, data).then(
|
||||
records => {
|
||||
({ records, externalEntries }) => {
|
||||
if (this.forceStop) {
|
||||
return null;
|
||||
}
|
||||
|
@ -211,13 +216,28 @@ AutofillProfileAutoCompleteSearch.prototype = {
|
|||
let handler = lazy.FormAutofillContent.activeHandler;
|
||||
let isSecure = lazy.InsecurePasswordUtils.isFormSecure(handler.form);
|
||||
|
||||
return new AutocompleteResult(
|
||||
const result = new AutocompleteResult(
|
||||
searchString,
|
||||
activeFieldDetail.fieldName,
|
||||
allFieldNames,
|
||||
adaptedRecords,
|
||||
{ isSecure, isInputAutofilled }
|
||||
);
|
||||
|
||||
result.externalEntries.push(
|
||||
...externalEntries.map(
|
||||
entry =>
|
||||
new GenericAutocompleteItem(
|
||||
entry.icon,
|
||||
entry.title,
|
||||
entry.subtitle,
|
||||
entry.fillMessageName,
|
||||
entry.fillMessageData
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -250,6 +270,34 @@ AutofillProfileAutoCompleteSearch.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
getScenarioName(input) {
|
||||
if (!input) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Running simple heuristics first, because running the SignUpFormRuleset is expensive
|
||||
if (
|
||||
!(
|
||||
lazy.LoginHelper.isInferredEmailField(input) ||
|
||||
lazy.LoginHelper.isInferredUsernameField(input)
|
||||
)
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const formRoot = lazy.FormLikeFactory.findRootForField(input);
|
||||
|
||||
if (!HTMLFormElement.isInstance(formRoot)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const threshold = lazy.LoginHelper.signupDetectionConfidenceThreshold;
|
||||
const { rules, type } = lazy.SignUpFormRuleset;
|
||||
const results = rules.against(formRoot);
|
||||
const score = results.get(formRoot).scoreFor(type);
|
||||
return score > threshold ? "SignUpFormScenario" : "";
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops an asynchronous search that is in progress
|
||||
*/
|
||||
|
@ -281,7 +329,10 @@ AutofillProfileAutoCompleteSearch.prototype = {
|
|||
}
|
||||
|
||||
let actor = getActorFromWindow(input.ownerGlobal);
|
||||
return actor.sendQuery("FormAutofill:GetRecords", data);
|
||||
return actor.sendQuery("FormAutofill:GetRecords", {
|
||||
scenarioName: this.getScenarioName(input),
|
||||
...data,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -345,6 +396,44 @@ export const ProfileAutocomplete = {
|
|||
}
|
||||
},
|
||||
|
||||
fillRequestId: 0,
|
||||
|
||||
async sendFillRequestToFormAutofillParent(input, comment) {
|
||||
if (!comment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!input || input != autocompleteController?.input.focusedInput) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { fillMessageName, fillMessageData } = JSON.parse(comment ?? "{}");
|
||||
if (!fillMessageName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.fillRequestId++;
|
||||
const fillRequestId = this.fillRequestId;
|
||||
const actor = getActorFromWindow(input.ownerGlobal, "FormAutofill");
|
||||
const value = await actor.sendQuery(fillMessageName, fillMessageData ?? {});
|
||||
|
||||
// skip fill if another fill operation started during await
|
||||
if (fillRequestId != this.fillRequestId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If AutoFillParent returned a string to fill, we must do it here because
|
||||
// nsAutoCompleteController.cpp already finished it's work before we finished await.
|
||||
input.setUserInput(value);
|
||||
input.select(value.length, value.length);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
_getSelectedIndex(contentWindow) {
|
||||
let actor = getActorFromWindow(contentWindow, "AutoComplete");
|
||||
if (!actor) {
|
||||
|
@ -363,18 +452,24 @@ export const ProfileAutocomplete = {
|
|||
}
|
||||
|
||||
let selectedIndex = this._getSelectedIndex(focusedInput.ownerGlobal);
|
||||
const validIndex =
|
||||
selectedIndex >= 0 &&
|
||||
selectedIndex < this.lastProfileAutoCompleteResult.matchCount;
|
||||
const comment = validIndex
|
||||
? this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex)
|
||||
: null;
|
||||
|
||||
if (
|
||||
selectedIndex == -1 ||
|
||||
!this.lastProfileAutoCompleteResult ||
|
||||
this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) !=
|
||||
"autofill-profile"
|
||||
) {
|
||||
await this.sendFillRequestToFormAutofillParent(focusedInput, comment);
|
||||
return;
|
||||
}
|
||||
|
||||
let profile = JSON.parse(
|
||||
this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex)
|
||||
);
|
||||
let profile = JSON.parse(comment);
|
||||
|
||||
await lazy.FormAutofillContent.activeHandler.autofillFormFields(profile);
|
||||
},
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
// We expose a singleton from this module. Some tests may import the
|
||||
// constructor via a backstage pass.
|
||||
import { FirefoxRelayTelemetry } from "resource://gre/modules/FirefoxRelayTelemetry.mjs";
|
||||
import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs";
|
||||
import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
|
||||
|
||||
|
@ -38,6 +39,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
FormAutofillPreferences:
|
||||
"resource://autofill/FormAutofillPreferences.sys.mjs",
|
||||
FormAutofillPrompter: "resource://autofill/FormAutofillPrompter.sys.mjs",
|
||||
FirefoxRelay: "resource://gre/modules/FirefoxRelay.sys.mjs",
|
||||
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
|
||||
OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
|
||||
});
|
||||
|
||||
|
@ -304,7 +307,17 @@ export class FormAutofillParent extends JSWindowActorParent {
|
|||
break;
|
||||
}
|
||||
case "FormAutofill:GetRecords": {
|
||||
return FormAutofillParent._getRecords(data);
|
||||
const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({
|
||||
formOrigin: this.formOrigin,
|
||||
scenarioName: data.scenarioName,
|
||||
hasInput: !!data.searchString?.length,
|
||||
});
|
||||
const recordsPromise = FormAutofillParent._getRecords(data);
|
||||
const [records, externalEntries] = await Promise.all([
|
||||
recordsPromise,
|
||||
relayPromise,
|
||||
]);
|
||||
return { records, externalEntries };
|
||||
}
|
||||
case "FormAutofill:OnFormSubmit": {
|
||||
this.notifyMessageObservers("onFormSubmitted", data);
|
||||
|
@ -373,11 +386,46 @@ export class FormAutofillParent extends JSWindowActorParent {
|
|||
);
|
||||
break;
|
||||
}
|
||||
case "PasswordManager:offerRelayIntegration": {
|
||||
FirefoxRelayTelemetry.recordRelayOfferedEvent(
|
||||
"clicked",
|
||||
data.telemetry.flowId,
|
||||
data.telemetry.scenarioName
|
||||
);
|
||||
return this.#offerRelayIntegration();
|
||||
}
|
||||
case "PasswordManager:generateRelayUsername": {
|
||||
FirefoxRelayTelemetry.recordRelayUsernameFilledEvent(
|
||||
"clicked",
|
||||
data.telemetry.flowId
|
||||
);
|
||||
return this.#generateRelayUsername();
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get formOrigin() {
|
||||
return lazy.LoginHelper.getLoginOrigin(
|
||||
this.manager.documentPrincipal?.originNoSuffix
|
||||
);
|
||||
}
|
||||
|
||||
getRootBrowser() {
|
||||
return this.browsingContext.topFrameElement;
|
||||
}
|
||||
|
||||
async #offerRelayIntegration() {
|
||||
const browser = this.getRootBrowser();
|
||||
return lazy.FirefoxRelay.offerRelayIntegration(browser, this.formOrigin);
|
||||
}
|
||||
|
||||
async #generateRelayUsername() {
|
||||
const browser = this.getRootBrowser();
|
||||
return lazy.FirefoxRelay.generateUsername(browser, this.formOrigin);
|
||||
}
|
||||
|
||||
notifyMessageObservers(callbackName, data) {
|
||||
for (let observer of gMessageObservers) {
|
||||
try {
|
||||
|
|
|
@ -16,6 +16,8 @@ ChromeUtils.defineLazyGetter(
|
|||
);
|
||||
|
||||
class ProfileAutoCompleteResult {
|
||||
externalEntries = [];
|
||||
|
||||
constructor(
|
||||
searchString,
|
||||
focusedFieldName,
|
||||
|
@ -73,20 +75,25 @@ class ProfileAutoCompleteResult {
|
|||
);
|
||||
}
|
||||
|
||||
getAt(index) {
|
||||
for (const group of [this._popupLabels, this.externalEntries]) {
|
||||
if (index < group.length) {
|
||||
return group[index];
|
||||
}
|
||||
index -= group.length;
|
||||
}
|
||||
|
||||
throw Components.Exception(
|
||||
"Index out of range.",
|
||||
Cr.NS_ERROR_ILLEGAL_VALUE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} The number of results
|
||||
*/
|
||||
get matchCount() {
|
||||
return this._popupLabels.length;
|
||||
}
|
||||
|
||||
_checkIndexBounds(index) {
|
||||
if (index < 0 || index >= this._popupLabels.length) {
|
||||
throw Components.Exception(
|
||||
"Index out of range.",
|
||||
Cr.NS_ERROR_ILLEGAL_VALUE
|
||||
);
|
||||
}
|
||||
return this._popupLabels.length + this.externalEntries.length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,14 +122,12 @@ class ProfileAutoCompleteResult {
|
|||
* @returns {string} The result at the specified index
|
||||
*/
|
||||
getValueAt(index) {
|
||||
this._checkIndexBounds(index);
|
||||
this.getAt(index);
|
||||
return "";
|
||||
}
|
||||
|
||||
getLabelAt(index) {
|
||||
this._checkIndexBounds(index);
|
||||
|
||||
let label = this._popupLabels[index];
|
||||
const label = this.getAt(index);
|
||||
if (typeof label == "string") {
|
||||
return label;
|
||||
}
|
||||
|
@ -136,8 +141,8 @@ class ProfileAutoCompleteResult {
|
|||
* @returns {string} The comment at the specified index
|
||||
*/
|
||||
getCommentAt(index) {
|
||||
this._checkIndexBounds(index);
|
||||
return JSON.stringify(this._matchingProfiles[index]);
|
||||
const item = this.getAt(index);
|
||||
return item.comment ?? JSON.stringify(this._matchingProfiles[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,8 +152,12 @@ class ProfileAutoCompleteResult {
|
|||
* @returns {string} The style hint at the specified index
|
||||
*/
|
||||
getStyleAt(index) {
|
||||
this._checkIndexBounds(index);
|
||||
if (index == this.matchCount - 1) {
|
||||
const itemStyle = this.getAt(index).style;
|
||||
if (itemStyle) {
|
||||
return itemStyle;
|
||||
}
|
||||
|
||||
if (index == this._popupLabels.length - 1) {
|
||||
return "autofill-footer";
|
||||
}
|
||||
if (this._isInputAutofilled) {
|
||||
|
@ -165,7 +174,7 @@ class ProfileAutoCompleteResult {
|
|||
* @returns {string} The image url at the specified index
|
||||
*/
|
||||
getImageAt(index) {
|
||||
this._checkIndexBounds(index);
|
||||
this.getAt(index);
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -186,7 +195,7 @@ class ProfileAutoCompleteResult {
|
|||
* @returns {boolean} True if the value is removable
|
||||
*/
|
||||
isRemovableAt(index) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -467,7 +476,11 @@ export class CreditCardResult extends ProfileAutoCompleteResult {
|
|||
}
|
||||
|
||||
getStyleAt(index) {
|
||||
this._checkIndexBounds(index);
|
||||
const itemStyle = this.getAt(index).style;
|
||||
if (itemStyle) {
|
||||
return itemStyle;
|
||||
}
|
||||
|
||||
if (!this._isSecure) {
|
||||
return "autofill-insecureWarning";
|
||||
}
|
||||
|
@ -476,8 +489,13 @@ export class CreditCardResult extends ProfileAutoCompleteResult {
|
|||
}
|
||||
|
||||
getImageAt(index) {
|
||||
this._checkIndexBounds(index);
|
||||
let network = this._cardTypes[index];
|
||||
return lazy.CreditCard.getCreditCardLogo(network);
|
||||
this.getAt(index);
|
||||
|
||||
if (index < this._cardTypes.length) {
|
||||
let network = this._cardTypes[index];
|
||||
return lazy.CreditCard.getCreditCardLogo(network);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -166,7 +166,7 @@ const observer = {
|
|||
case "autocomplete-did-enter-text": {
|
||||
let input = subject.QueryInterface(Ci.nsIAutoCompleteInput);
|
||||
let { selectedIndex } = input.popup;
|
||||
if (selectedIndex < 0) {
|
||||
if (selectedIndex < 0 || selectedIndex >= input.controller.matchCount) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче