tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 73fb1081cd3c3cc70ea382ed36930e58260fba1a
parent a99dfbcddf35ceb514d059180e6ce3b180659c90
Author: Julian Descottes <jdescottes@mozilla.com>
Date:   Thu, 13 Nov 2025 14:51:59 +0000

Bug 1855045 - [bidi] Add support for input.fileDialogOpened event r=Sasha

Differential Revision: https://phabricator.services.mozilla.com/D271791

Diffstat:
Mremote/jar.mn | 1+
Aremote/shared/listeners/FilePickerListener.sys.mjs | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mremote/webdriver-bidi/jar.mn | 1+
Mremote/webdriver-bidi/modules/ModuleRegistry.sys.mjs | 2++
Mremote/webdriver-bidi/modules/root/input.sys.mjs | 2+-
Aremote/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs | 34++++++++++++++++++++++++++++++++++
Mremote/webdriver-bidi/modules/windowglobal/input.sys.mjs | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
7 files changed, 280 insertions(+), 62 deletions(-)

diff --git a/remote/jar.mn b/remote/jar.mn @@ -52,6 +52,7 @@ remote.jar: content/shared/listeners/ContextualIdentityListener.sys.mjs (shared/listeners/ContextualIdentityListener.sys.mjs) content/shared/listeners/DataChannelListener.sys.mjs (shared/listeners/DataChannelListener.sys.mjs) content/shared/listeners/DownloadListener.sys.mjs (shared/listeners/DownloadListener.sys.mjs) + content/shared/listeners/FilePickerListener.sys.mjs (shared/listeners/FilePickerListener.sys.mjs) content/shared/listeners/LoadListener.sys.mjs (shared/listeners/LoadListener.sys.mjs) content/shared/listeners/NavigationListener.sys.mjs (shared/listeners/NavigationListener.sys.mjs) content/shared/listeners/NetworkEventRecord.sys.mjs (shared/listeners/NetworkEventRecord.sys.mjs) diff --git a/remote/shared/listeners/FilePickerListener.sys.mjs b/remote/shared/listeners/FilePickerListener.sys.mjs @@ -0,0 +1,78 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", +}); + +const OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING = "file-input-picker-opening"; + +/** + * The FilePickerListener can be used to listen for file picker dialog openings + * triggered by input type=file elements. + * + * Note that the actual file picker might not open if it is automatically + * dismissed as part of the defined user prompt behavior. + * + * Example: + * ``` + * const listener = new FilePickerListener(); + * listener.on("file-picker-opening", onFilePickerOpened); + * listener.startListening(); + * + * const onFilePickerOpened = (eventName, data) => { + * const { element } = data; + * console.log("File picker opened:", element.multiple); + * }; + * ``` + * + * @fires FilePickerListener#"file-picker-opening" + * The FilePickerListener emits the following event: + * - "file-picker-opening" when a file picker is requested to be opened, + * with the following object as payload: + * - {Element} element + * The DOM element which triggered the file picker to open. + */ +export class FilePickerListener { + #listening; + + constructor() { + lazy.EventEmitter.decorate(this); + + this.#listening = false; + } + + destroy() { + this.stopListening(); + } + + observe(subject, topic) { + switch (topic) { + case OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING: { + this.emit("file-picker-opening", { + element: subject, + }); + break; + } + } + } + + startListening() { + if (this.#listening) { + return; + } + Services.obs.addObserver(this, OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING); + this.#listening = true; + } + + stopListening() { + if (!this.#listening) { + return; + } + Services.obs.removeObserver(this, OBSERVER_TOPIC_FILE_INPUT_PICKER_OPENING); + this.#listening = false; + } +} diff --git a/remote/webdriver-bidi/jar.mn b/remote/webdriver-bidi/jar.mn @@ -41,6 +41,7 @@ remote.jar: # WebDriver BiDi windowglobal-in-root modules content/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs (modules/windowglobal-in-root/browsingContext.sys.mjs) + content/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs (modules/windowglobal-in-root/input.sys.mjs) content/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs (modules/windowglobal-in-root/log.sys.mjs) content/webdriver-bidi/modules/windowglobal-in-root/network.sys.mjs (modules/windowglobal-in-root/network.sys.mjs) content/webdriver-bidi/modules/windowglobal-in-root/script.sys.mjs (modules/windowglobal-in-root/script.sys.mjs) diff --git a/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs b/remote/webdriver-bidi/modules/ModuleRegistry.sys.mjs @@ -41,6 +41,8 @@ ChromeUtils.defineESModuleGetters(modules.root, { ChromeUtils.defineESModuleGetters(modules["windowglobal-in-root"], { browsingContext: "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/browsingContext.sys.mjs", + input: + "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs", log: "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/log.sys.mjs", network: "chrome://remote/content/webdriver-bidi/modules/windowglobal-in-root/network.sys.mjs", diff --git a/remote/webdriver-bidi/modules/root/input.sys.mjs b/remote/webdriver-bidi/modules/root/input.sys.mjs @@ -395,7 +395,7 @@ class InputModule extends RootBiDiModule { } static get supportedEvents() { - return []; + return ["input.fileDialogOpened"]; } } diff --git a/remote/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs b/remote/webdriver-bidi/modules/windowglobal-in-root/input.sys.mjs @@ -0,0 +1,34 @@ +/* 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/. */ + +import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", + TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", +}); + +class InputModule extends Module { + destroy() {} + + interceptEvent(name, payload) { + if (name == "input.fileDialogOpened") { + const browsingContext = payload.context; + if (!lazy.TabManager.isValidCanonicalBrowsingContext(browsingContext)) { + // Discard events for invalid browsing contexts. + return null; + } + + // Resolve browsing context to a Navigable id. + payload.context = + lazy.NavigableManager.getIdForBrowsingContext(browsingContext); + } + + return payload; + } +} + +export const input = InputModule; diff --git a/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs @@ -15,14 +15,97 @@ ChromeUtils.defineESModuleGetters(lazy, { dom: "chrome://remote/content/shared/DOM.sys.mjs", error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", event: "chrome://remote/content/shared/webdriver/Event.sys.mjs", + FilePickerListener: + "chrome://remote/content/shared/listeners/FilePickerListener.sys.mjs", + OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", + setDefaultSerializationOptions: + "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", }); class InputModule extends WindowGlobalBiDiModule { + #filePickerListener; + #subscribedEvents; + constructor(messageHandler) { super(messageHandler); + + this.#filePickerListener = new lazy.FilePickerListener(); + this.#filePickerListener.on( + "file-picker-opening", + this.#onFilePickerOpening + ); + + // Set of event names which have active subscriptions. + this.#subscribedEvents = new Set(); + } + + destroy() { + this.#filePickerListener.off( + "file-picker-opening", + this.#onFilePickerOpening + ); + this.#subscribedEvents = null; } - destroy() {} + async setFiles(options) { + const { element: sharedReference, files } = options; + + const element = + await this.#deserializeElementSharedReference(sharedReference); + + if ( + !HTMLInputElement.isInstance(element) || + element.type !== "file" || + element.disabled + ) { + throw new lazy.error.UnableToSetFileInputError( + `Element needs to be an <input> element with type "file" and not disabled` + ); + } + + if (files.length > 1 && !element.hasAttribute("multiple")) { + throw new lazy.error.UnableToSetFileInputError( + `Element should have an attribute "multiple" set when trying to set more than 1 file` + ); + } + + const fileObjects = []; + for (const file of files) { + try { + fileObjects.push(await File.createFromFileName(file)); + } catch (e) { + throw new lazy.error.UnsupportedOperationError( + `Failed to add file ${file} (${e})` + ); + } + } + + const selectedFiles = Array.from(element.files); + + const intersection = fileObjects.filter(fileObject => + selectedFiles.some( + selectedFile => + // Compare file fields to identify if the files are equal. + // TODO: Bug 1883856. Add check for full path or use a different way + // to compare files when it's available. + selectedFile.name === fileObject.name && + selectedFile.size === fileObject.size && + selectedFile.type === fileObject.type + ) + ); + + if ( + intersection.length === selectedFiles.length && + selectedFiles.length === fileObjects.length + ) { + lazy.event.cancel(element); + } else { + element.mozSetFileArray(fileObjects); + + lazy.event.input(element); + lazy.event.change(element); + } + } async #deserializeElementSharedReference(sharedReference) { if (typeof sharedReference?.sharedId !== "string") { @@ -43,6 +126,85 @@ class InputModule extends WindowGlobalBiDiModule { return element; } + #onFilePickerOpening = (eventName, data) => { + const { element } = data; + if (element.ownerGlobal.browsingContext != this.messageHandler.context) { + return; + } + + const realm = this.messageHandler.getRealm(); + + const serializedNode = this.serialize( + element, + lazy.setDefaultSerializationOptions(), + lazy.OwnershipModel.None, + realm + ); + + this.emitEvent("input.fileDialogOpened", { + context: this.messageHandler.context, + element: serializedNode, + multiple: element.multiple, + }); + }; + + #startListingOnFilePickerOpened() { + if (!this.#subscribedEvents.has("script.FilePickerOpened")) { + this.#filePickerListener.startListening(); + } + } + + #stopListingOnFilePickerOpened() { + if (this.#subscribedEvents.has("script.FilePickerOpened")) { + this.#filePickerListener.stopListening(); + } + } + + #subscribeEvent(event) { + switch (event) { + case "input.fileDialogOpened": { + this.#startListingOnFilePickerOpened(); + this.#subscribedEvents.add(event); + break; + } + } + } + + #unsubscribeEvent(event) { + switch (event) { + case "input.fileDialogOpened": { + this.#stopListingOnFilePickerOpened(); + this.#subscribedEvents.delete(event); + break; + } + } + } + + _applySessionData(params) { + // TODO: Bug 1775231. Move this logic to a shared module or an abstract + // class. + const { category } = params; + if (category === "event") { + const filteredSessionData = params.sessionData.filter(item => + this.messageHandler.matchesContext(item.contextDescriptor) + ); + for (const event of this.#subscribedEvents.values()) { + const hasSessionItem = filteredSessionData.some( + item => item.value === event + ); + // If there are no session items for this context, we should unsubscribe from the event. + if (!hasSessionItem) { + this.#unsubscribeEvent(event); + } + } + + // Subscribe to all events, which have an item in SessionData. + for (const { value } of filteredSessionData) { + this.#subscribeEvent(value); + } + } + } + _assertInViewPort(options) { const { target } = options; @@ -169,66 +331,6 @@ class InputModule extends WindowGlobalBiDiModule { return [val.x / dpr, val.y / dpr]; } - - async setFiles(options) { - const { element: sharedReference, files } = options; - - const element = - await this.#deserializeElementSharedReference(sharedReference); - - if ( - !HTMLInputElement.isInstance(element) || - element.type !== "file" || - element.disabled - ) { - throw new lazy.error.UnableToSetFileInputError( - `Element needs to be an <input> element with type "file" and not disabled` - ); - } - - if (files.length > 1 && !element.hasAttribute("multiple")) { - throw new lazy.error.UnableToSetFileInputError( - `Element should have an attribute "multiple" set when trying to set more than 1 file` - ); - } - - const fileObjects = []; - for (const file of files) { - try { - fileObjects.push(await File.createFromFileName(file)); - } catch (e) { - throw new lazy.error.UnsupportedOperationError( - `Failed to add file ${file} (${e})` - ); - } - } - - const selectedFiles = Array.from(element.files); - - const intersection = fileObjects.filter(fileObject => - selectedFiles.some( - selectedFile => - // Compare file fields to identify if the files are equal. - // TODO: Bug 1883856. Add check for full path or use a different way - // to compare files when it's available. - selectedFile.name === fileObject.name && - selectedFile.size === fileObject.size && - selectedFile.type === fileObject.type - ) - ); - - if ( - intersection.length === selectedFiles.length && - selectedFiles.length === fileObjects.length - ) { - lazy.event.cancel(element); - } else { - element.mozSetFileArray(fileObjects); - - lazy.event.input(element); - lazy.event.change(element); - } - } } export const input = InputModule;