1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs";
6 import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
10 ChromeUtils.defineESModuleGetters(lazy, {
11 FormLikeFactory: "resource://gre/modules/FormLikeFactory.sys.mjs",
12 LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
13 LoginManagerChild: "resource://gre/modules/LoginManagerChild.sys.mjs",
16 export class GeckoViewAutoFillChild extends GeckoViewActorChild {
20 this._autofillElements = undefined;
21 this._autofillInfos = undefined;
24 // eslint-disable-next-line complexity
26 debug`handleEvent: ${aEvent.type}`;
27 switch (aEvent.type) {
28 case "DOMFormHasPassword": {
30 lazy.FormLikeFactory.createFromForm(aEvent.composedTarget)
34 case "DOMInputPasswordAdded": {
35 const input = aEvent.composedTarget;
37 this.addElement(lazy.FormLikeFactory.createFromField(input));
42 const element = aEvent.composedTarget;
43 if (!this.contentWindow.HTMLInputElement.isInstance(element)) {
46 GeckoViewUtils.waitForPanZoomState(this.contentWindow).finally(() => {
47 if (Cu.isDeadWrapper(element)) {
48 // Focus element is removed or document is navigated to new page.
51 const focusedElement =
52 Services.focus.focusedElement ||
53 element.ownerDocument?.activeElement;
54 if (element == focusedElement) {
55 this.onFocus(focusedElement);
62 this.contentWindow.HTMLInputElement.isInstance(aEvent.composedTarget)
69 if (aEvent.target === this.document) {
70 this.clearElements(this.browsingContext);
75 if (aEvent.target === this.document) {
76 this.scanDocument(this.document);
80 case "PasswordManager:ShowDoorhanger": {
81 const { form: formLike } = aEvent.detail;
82 this.commitAutofill(formLike);
89 * Process an auto-fillable form and send the relevant details of the form
90 * to Java. Multiple calls within a short time period for the same form are
91 * coalesced, so that, e.g., if multiple inputs are added to a form in
92 * succession, we will only perform one processing pass. Note that for inputs
93 * without forms, FormLikeFactory treats the document as the "form", but
94 * there is no difference in how we process them.
96 * @param aFormLike A FormLike object produced by FormLikeFactory.
98 async addElement(aFormLike) {
99 debug`Adding auto-fill ${aFormLike.rootElement.tagName}`;
101 const window = aFormLike.rootElement.ownerGlobal;
102 // Get password field to get better form data via LoginManagerChild.
104 for (const field of aFormLike.elements) {
106 ChromeUtils.getClassName(field) === "HTMLInputElement" &&
107 field.type == "password"
109 passwordField = field;
114 const loginManagerChild = lazy.LoginManagerChild.forWindow(window);
115 const docState = loginManagerChild.stateForDocument(
116 passwordField.ownerDocument
118 const [usernameField] = docState.getUserNameAndPasswordFields(
119 passwordField || aFormLike.elements[0]
122 const focusedElement = aFormLike.rootElement.ownerDocument.activeElement;
123 let sendFocusEvent = aFormLike.rootElement === focusedElement;
125 const rootInfo = this._getInfo(
126 aFormLike.rootElement,
132 rootInfo.rootUuid = rootInfo.uuid;
133 rootInfo.children = aFormLike.elements
136 element.type != "hidden" &&
138 element.type != "text" ||
139 element == usernameField ||
140 (element.getAutocompleteInfo() &&
141 element.getAutocompleteInfo().fieldName == "email"))
144 sendFocusEvent |= element === focusedElement;
145 return this._getInfo(
154 // We don't await here so that we can send a focus event immediately
155 // after this as the app might not know which element is focused.
156 const responsePromise = this.sendQuery("Add", {
160 if (sendFocusEvent) {
161 // We might have missed sending a focus event for the active element.
162 this.onFocus(aFormLike.ownerDocument.activeElement);
165 const responses = await responsePromise;
166 // `responses` is an object with global IDs as keys.
167 debug`Performing auto-fill ${Object.keys(responses)}`;
169 const AUTOFILL_STATE = "autofill";
170 const winUtils = window.windowUtils;
172 for (const uuid in responses) {
174 this._autofillElements && this._autofillElements.get(uuid);
175 const element = entry && entry.get();
176 const value = responses[uuid] || "";
179 window.HTMLInputElement.isInstance(element) &&
181 element.parentElement
183 element.setUserInput(value);
184 if (winUtils && element.value === value) {
185 // Add highlighting for autofilled fields.
186 winUtils.addManuallyManagedState(element, AUTOFILL_STATE);
188 // Remove highlighting when the field is changed.
189 element.addEventListener(
191 _ => winUtils.removeManuallyManagedState(element, AUTOFILL_STATE),
192 { mozSystemGroup: true, once: true }
195 } else if (element) {
196 warn`Don't know how to auto-fill ${element.tagName}`;
200 warn`Cannot perform autofill ${error}`;
204 _getInfo(aElement, aParent, aRoot, aUsernameField) {
205 if (!this._autofillInfos) {
206 this._autofillInfos = new WeakMap();
207 this._autofillElements = new Map();
210 let info = this._autofillInfos.get(aElement);
215 const window = aElement.ownerGlobal;
216 const bounds = aElement.getBoundingClientRect();
217 const isInputElement = window.HTMLInputElement.isInstance(aElement);
221 uuid: Services.uuid.generateUUID().toString().slice(1, -1), // discard the surrounding curly braces
224 tag: aElement.tagName,
225 type: isInputElement ? aElement.type : null,
226 value: isInputElement ? aElement.value : null,
244 ].includes(aElement.type),
245 disabled: isInputElement ? aElement.disabled : null,
246 attributes: Object.assign(
248 ...Array.from(aElement.attributes)
249 .filter(attr => attr.localName !== "value")
250 .map(attr => ({ [attr.localName]: attr.value }))
252 origin: aElement.ownerDocument.location.origin,
258 bottom: bounds.bottom,
262 if (aElement === aUsernameField) {
263 info.autofillhint = "username"; // AUTOFILL.HINT.USERNAME
264 } else if (isInputElement) {
265 // Using autocomplete attribute if it is email.
266 const autocompleteInfo = aElement.getAutocompleteInfo();
267 if (autocompleteInfo) {
268 const autocompleteAttr = autocompleteInfo.fieldName;
269 if (autocompleteAttr == "email") {
275 this._autofillInfos.set(aElement, info);
276 this._autofillElements.set(info.uuid, Cu.getWeakReference(aElement));
280 _updateInfoValues(aElements) {
281 if (!this._autofillInfos) {
286 for (const element of aElements) {
287 const info = this._autofillInfos.get(element);
289 if (!info?.isInputElement || info.value === element.value) {
292 debug`Updating value ${info.value} to ${element.value}`;
294 info.value = element.value;
295 this._autofillInfos.set(element, info);
302 * Called when an auto-fillable field is focused or blurred.
304 * @param aTarget Focused element, or null if an element has lost focus.
307 debug`Auto-fill focus on ${aTarget && aTarget.tagName}`;
309 const info = aTarget && this._autofillInfos?.get(aTarget);
311 const bounds = aTarget.getBoundingClientRect();
312 const screenRect = lazy.LayoutUtils.rectToScreenRect(
317 left: screenRect.left,
319 right: screenRect.right,
320 bottom: screenRect.bottom,
324 if (!aTarget || info) {
325 this.sendAsyncMessage("Focus", {
331 commitAutofill(aFormLike) {
333 throw new Error("null-form on autofill commit");
336 debug`Committing auto-fill for ${aFormLike.rootElement.tagName}`;
338 const updatedNodeInfos = this._updateInfoValues([
339 aFormLike.rootElement,
340 ...aFormLike.elements,
343 for (const updatedInfo of updatedNodeInfos) {
344 debug`Updating node ${updatedInfo}`;
345 this.sendAsyncMessage("Update", {
350 const info = this._getInfo(aFormLike.rootElement);
352 debug`Committing node ${info}`;
353 this.sendAsyncMessage("Commit", {
360 * Clear all tracked auto-fill forms and notify Java.
362 clearElements(browsingContext) {
363 this._autofillInfos = undefined;
364 this._autofillElements = undefined;
366 if (browsingContext === browsingContext.top) {
367 this.sendAsyncMessage("Clear");
372 * Scan for auto-fillable forms and add them if necessary. Called when a page
373 * is navigated to through history, in which case we don't get our typical
374 * "input added" notifications.
376 * @param aDoc Document to scan.
379 // Add forms first; only check forms with password inputs.
380 const inputs = aDoc.querySelectorAll("input[type=password]");
381 let inputAdded = false;
382 for (let i = 0; i < inputs.length; i++) {
383 if (inputs[i].form) {
384 // Let addElement coalesce multiple calls for the same form.
385 this.addElement(lazy.FormLikeFactory.createFromForm(inputs[i].form));
386 } else if (!inputAdded) {
387 // Treat inputs without forms as one unit, and process them only once.
389 this.addElement(lazy.FormLikeFactory.createFromField(inputs[i]));
395 const { debug, warn } = GeckoViewAutoFillChild.initLogging("GeckoViewAutoFill");