fune/browser/extensions/formautofill/api.js
jneuberger 04bddea373 Bug 1884632 - P1. Handle heuristic for page navigations centrally in FormHandlerChild r=dimi,credential-management-reviewers,sgalich,geckoview-reviewers,owlish
FormHandler is a central place for heuristics that other components like the LoginManager and FormAutofill
rely on. This patch moves the heuristics that detect page navigations to the FormHandler.
It also applies two changes:
- Heuristic to capture on page navigation no longer relies on the process' active element in FormAutofill
- Capturing in cross-origin frames is introduced

Introduced page navigation heuristic:
When LoginManager/FormAutofill detect a form that they expect a submission for, a FormHandler actor pair is
created in the current window context, which registers the web progress listener that listens for "page navigations",
e.g. location changes of the observed window/document or history session changes.
- If the form is in a same-orign frame, we register the listener only at the top level.
- If the form is in a cross-origin frame, we additionally set up a listener with the root
  of the cross-origin process, so that we are aware of page navigations in both processes.
When a page navigation is observed, all existing (same-origin and cross-origin) FormHandler parents in the
browsing context subtree notify their children.
(Note: We don't create any new actors in this step, because they won't have any form to submit anyway).
When the corresponding FormHandlerChild(ren) are notified of the page navigation, they fire the "form-submission-detected" event.

On "form-submission-detected" event:
- The LoginManagerChild instance(s) kept track of all modified login forms and infers capturing them.
- The FormAutofillChild instance(s) kept track of all identified formautofill forms and infers capturing them.

Differential Revision: https://phabricator.services.mozilla.com/D204927
2024-05-13 21:31:38 +00:00

214 lines
6.9 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* globals ExtensionAPI, Services, XPCOMUtils */
const CACHED_STYLESHEETS = new WeakMap();
ChromeUtils.defineESModuleGetters(this, {
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
FormAutofillParent: "resource://autofill/FormAutofillParent.sys.mjs",
FormAutofillStatus: "resource://autofill/FormAutofillParent.sys.mjs",
AutoCompleteParent: "resource://gre/actors/AutoCompleteParent.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
this,
"resProto",
"@mozilla.org/network/protocol;1?name=resource",
"nsISubstitutingProtocolHandler"
);
const RESOURCE_HOST = "formautofill";
function insertStyleSheet(domWindow, url) {
let doc = domWindow.document;
let styleSheetAttr = `href="${url}" type="text/css"`;
let styleSheet = doc.createProcessingInstruction(
"xml-stylesheet",
styleSheetAttr
);
doc.insertBefore(styleSheet, doc.documentElement);
if (CACHED_STYLESHEETS.has(domWindow)) {
CACHED_STYLESHEETS.get(domWindow).push(styleSheet);
} else {
CACHED_STYLESHEETS.set(domWindow, [styleSheet]);
}
}
function ensureCssLoaded(domWindow) {
if (CACHED_STYLESHEETS.has(domWindow)) {
// This window already has autofill stylesheets.
return;
}
insertStyleSheet(domWindow, "chrome://formautofill/content/formautofill.css");
}
this.formautofill = class extends ExtensionAPI {
/**
* Adjusts and checks form autofill preferences during startup.
*
* @param {boolean} addressAutofillAvailable
* @param {boolean} creditCardAutofillAvailable
*/
adjustAndCheckFormAutofillPrefs(
addressAutofillAvailable,
creditCardAutofillAvailable
) {
// Reset the sync prefs in case the features were previously available
// but aren't now.
if (!creditCardAutofillAvailable) {
Services.prefs.clearUserPref(
"services.sync.engine.creditcards.available"
);
}
if (!addressAutofillAvailable) {
Services.prefs.clearUserPref("services.sync.engine.addresses.available");
}
if (!addressAutofillAvailable && !creditCardAutofillAvailable) {
Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
Services.telemetry.scalarSet("formautofill.availability", false);
return;
}
// This pref is used for web contents to detect the autocomplete feature.
// When it's true, "element.autocomplete" will return tokens we currently
// support -- otherwise it'll return an empty string.
Services.prefs.setBoolPref("dom.forms.autocomplete.formautofill", true);
Services.telemetry.scalarSet("formautofill.availability", true);
// These "*.available" prefs determines whether the "addresses"/"creditcards" sync engine is
// available (ie, whether it is shown in any UI etc) - it *does not* determine
// whether the engine is actually enabled or not.
if (FormAutofill.isAutofillAddressesAvailable) {
Services.prefs.setBoolPref(
"services.sync.engine.addresses.available",
true
);
} else {
Services.prefs.clearUserPref("services.sync.engine.addresses.available");
}
if (FormAutofill.isAutofillCreditCardsAvailable) {
Services.prefs.setBoolPref(
"services.sync.engine.creditcards.available",
true
);
} else {
Services.prefs.clearUserPref(
"services.sync.engine.creditcards.available"
);
}
}
onStartup() {
// We have to do this before actually determining if we're enabled, since
// there are scripts inside of the core browser code that depend on the
// FormAutofill JSMs being registered.
let uri = Services.io.newURI("chrome/res/", null, this.extension.rootURI);
resProto.setSubstitution(RESOURCE_HOST, uri);
let aomStartup = Cc[
"@mozilla.org/addons/addon-manager-startup;1"
].getService(Ci.amIAddonManagerStartup);
const manifestURI = Services.io.newURI(
"manifest.json",
null,
this.extension.rootURI
);
this.chromeHandle = aomStartup.registerChrome(manifestURI, [
["content", "formautofill", "chrome/content/"],
]);
// Until we move to fluent (bug 1446164), we're stuck with
// chrome.manifest for handling localization since its what the
// build system can handle for localized repacks.
if (this.extension.rootURI instanceof Ci.nsIJARURI) {
this.autofillManifest = this.extension.rootURI.JARFile.QueryInterface(
Ci.nsIFileURL
).file;
} else if (this.extension.rootURI instanceof Ci.nsIFileURL) {
this.autofillManifest = this.extension.rootURI.file;
}
if (this.autofillManifest) {
Components.manager.addBootstrappedManifestLocation(this.autofillManifest);
} else {
console.error(
"Cannot find formautofill chrome.manifest for registring translated strings"
);
}
let addressAutofillAvailable = FormAutofill.isAutofillAddressesAvailable;
let creditCardAutofillAvailable =
FormAutofill.isAutofillCreditCardsAvailable;
this.adjustAndCheckFormAutofillPrefs(
addressAutofillAvailable,
creditCardAutofillAvailable
);
if (!creditCardAutofillAvailable && !addressAutofillAvailable) {
return;
}
// Listen for the autocomplete popup message
// or the form submitted message (which may trigger a
// doorhanger) to lazily append our stylesheets related
// to the autocomplete feature.
AutoCompleteParent.addPopupStateListener(ensureCssLoaded);
FormAutofillParent.addMessageObserver(this);
this.onFormSubmitted = (data, window) => ensureCssLoaded(window);
FormAutofillStatus.init();
ChromeUtils.registerWindowActor("FormAutofill", {
parent: {
esModuleURI: "resource://autofill/FormAutofillParent.sys.mjs",
},
child: {
esModuleURI: "resource://autofill/FormAutofillChild.sys.mjs",
events: {
focusin: {},
"form-submission-detected": { createActor: false },
},
},
allFrames: true,
});
}
onShutdown(isAppShutdown) {
if (isAppShutdown) {
return;
}
resProto.setSubstitution(RESOURCE_HOST, null);
this.chromeHandle.destruct();
this.chromeHandle = null;
if (this.autofillManifest) {
Components.manager.removeBootstrappedManifestLocation(
this.autofillManifest
);
}
ChromeUtils.unregisterWindowActor("FormAutofill");
AutoCompleteParent.removePopupStateListener(ensureCssLoaded);
FormAutofillParent.removeMessageObserver(this);
for (let win of Services.wm.getEnumerator("navigator:browser")) {
let cachedStyleSheets = CACHED_STYLESHEETS.get(win);
if (!cachedStyleSheets) {
continue;
}
while (cachedStyleSheets.length !== 0) {
cachedStyleSheets.pop().remove();
}
}
}
};