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:
Sergey Galich 2023-08-16 14:15:30 +00:00
Родитель bc5116b463
Коммит 238b86456d
7 изменённых файлов: 206 добавлений и 41 удалений

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

@ -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;
}