diff options
Diffstat (limited to 'remote/marionette/modal.sys.mjs')
-rw-r--r-- | remote/marionette/modal.sys.mjs | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/remote/marionette/modal.sys.mjs b/remote/marionette/modal.sys.mjs new file mode 100644 index 0000000000..eb3a96170b --- /dev/null +++ b/remote/marionette/modal.sys.mjs @@ -0,0 +1,377 @@ +/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs", + Log: "chrome://remote/content/shared/Log.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "logger", () => + lazy.Log.get(lazy.Log.TYPES.MARIONETTE) +); + +const COMMON_DIALOG = "chrome://global/content/commonDialog.xhtml"; + +/** @namespace */ +export const modal = { + ACTION_CLOSED: "closed", + ACTION_OPENED: "opened", +}; + +/** + * Check for already existing modal or tab modal dialogs + * + * @param {browser.Context} context + * Reference to the browser context to check for existent dialogs. + * + * @returns {modal.Dialog} + * Returns instance of the Dialog class, or `null` if no modal dialog + * is present. + */ +modal.findModalDialogs = function (context) { + // First check if there is a modal dialog already present for the + // current browser window. + for (let win of Services.wm.getEnumerator(null)) { + // TODO: Use BrowserWindowTracker.getTopWindow for modal dialogs without + // an opener. + if ( + win.document.documentURI === COMMON_DIALOG && + win.opener && + win.opener === context.window + ) { + lazy.logger.trace("Found open window modal prompt"); + return new modal.Dialog(() => context, win); + } + } + + if (lazy.AppInfo.isAndroid) { + const geckoViewPrompts = context.window.prompts(); + if (geckoViewPrompts.length) { + lazy.logger.trace("Found open GeckoView prompt"); + const prompt = geckoViewPrompts[0]; + return new modal.Dialog(() => context, prompt); + } + } + + const contentBrowser = context.contentBrowser; + + // If no modal dialog has been found yet, also check for tab and content modal + // dialogs for the current tab. + // + // TODO: Find an adequate implementation for Firefox on Android (bug 1708105) + if (contentBrowser?.tabDialogBox) { + let dialogs = contentBrowser.tabDialogBox.getTabDialogManager().dialogs; + if (dialogs.length) { + lazy.logger.trace("Found open tab modal prompt"); + return new modal.Dialog(() => context, dialogs[0].frameContentWindow); + } + + dialogs = contentBrowser.tabDialogBox.getContentDialogManager().dialogs; + + // Even with the dialog manager handing back a dialog, the `Dialog` property + // gets lazily added. If it's not set yet, ignore the dialog for now. + if (dialogs.length && dialogs[0].frameContentWindow.Dialog) { + lazy.logger.trace("Found open content prompt"); + return new modal.Dialog(() => context, dialogs[0].frameContentWindow); + } + } + + // If no modal dialog has been found yet, check for old non SubDialog based + // content modal dialogs. Even with those deprecated in Firefox 89 we should + // keep supporting applications that don't have them implemented yet. + if (contentBrowser?.tabModalPromptBox) { + const prompts = contentBrowser.tabModalPromptBox.listPrompts(); + if (prompts.length) { + lazy.logger.trace("Found open old-style content prompt"); + return new modal.Dialog(() => context, null); + } + } + + return null; +}; + +/** + * Observer for modal and tab modal dialogs. + * + * @param {function(): browser.Context} curBrowserFn + * Function that returns the current |browser.Context|. + * + * @returns {modal.DialogObserver} + * Returns instance of the DialogObserver class. + */ +modal.DialogObserver = class { + constructor(curBrowserFn) { + this._curBrowserFn = curBrowserFn; + + this.callbacks = new Set(); + this.register(); + } + + register() { + Services.obs.addObserver(this, "common-dialog-loaded"); + Services.obs.addObserver(this, "domwindowopened"); + Services.obs.addObserver(this, "geckoview-prompt-show"); + Services.obs.addObserver(this, "tabmodal-dialog-loaded"); + + // Register event listener for all already open windows + for (let win of Services.wm.getEnumerator(null)) { + win.addEventListener("DOMModalDialogClosed", this); + } + } + + unregister() { + Services.obs.removeObserver(this, "common-dialog-loaded"); + Services.obs.removeObserver(this, "domwindowopened"); + Services.obs.removeObserver(this, "geckoview-prompt-show"); + Services.obs.removeObserver(this, "tabmodal-dialog-loaded"); + + // Unregister event listener for all open windows + for (let win of Services.wm.getEnumerator(null)) { + win.removeEventListener("DOMModalDialogClosed", this); + } + } + + cleanup() { + this.callbacks.clear(); + this.unregister(); + } + + handleEvent(event) { + lazy.logger.trace(`Received event ${event.type}`); + + const chromeWin = event.target.opener + ? event.target.opener.ownerGlobal + : event.target.ownerGlobal; + + if (chromeWin != this._curBrowserFn().window) { + return; + } + + this.callbacks.forEach(callback => { + callback(modal.ACTION_CLOSED, event.target); + }); + } + + observe(subject, topic) { + lazy.logger.trace(`Received observer notification ${topic}`); + + const curBrowser = this._curBrowserFn(); + + switch (topic) { + // This topic is only used by the old-style content modal dialogs like + // alert, confirm, and prompt. It can be removed when only the new + // subdialog based content modals remain. Those will be made default in + // Firefox 89, and this case is deprecated. + case "tabmodal-dialog-loaded": + const container = curBrowser.contentBrowser.closest( + ".browserSidebarContainer" + ); + if (!container.contains(subject)) { + return; + } + this.callbacks.forEach(callback => + callback(modal.ACTION_OPENED, subject) + ); + break; + + case "common-dialog-loaded": + const modalType = subject.Dialog.args.modalType; + + if ( + modalType === Services.prompt.MODAL_TYPE_TAB || + modalType === Services.prompt.MODAL_TYPE_CONTENT + ) { + // Find the container of the dialog in the parent document, and ensure + // it is a descendant of the same container as the current browser. + const container = curBrowser.contentBrowser.closest( + ".browserSidebarContainer" + ); + if (!container.contains(subject.docShell.chromeEventHandler)) { + return; + } + } else if ( + subject.ownerGlobal != curBrowser.window && + subject.opener?.ownerGlobal != curBrowser.window + ) { + return; + } + + this.callbacks.forEach(callback => + callback(modal.ACTION_OPENED, subject) + ); + break; + + case "domwindowopened": + subject.addEventListener("DOMModalDialogClosed", this); + break; + + case "geckoview-prompt-show": + for (let win of Services.wm.getEnumerator(null)) { + const prompt = win.prompts().find(item => item.id == subject.id); + if (prompt) { + this.callbacks.forEach(callback => + callback(modal.ACTION_OPENED, prompt) + ); + return; + } + } + break; + } + } + + /** + * Add dialog handler by function reference. + * + * @param {Function} callback + * The handler to be added. + */ + add(callback) { + if (this.callbacks.has(callback)) { + return; + } + this.callbacks.add(callback); + } + + /** + * Remove dialog handler by function reference. + * + * @param {Function} callback + * The handler to be removed. + */ + remove(callback) { + if (!this.callbacks.has(callback)) { + return; + } + this.callbacks.delete(callback); + } + + /** + * Returns a promise that waits for the dialog to be closed. + */ + async dialogClosed() { + return new Promise(resolve => { + const dialogClosed = (action, dialog) => { + if (action == modal.ACTION_CLOSED) { + this.remove(dialogClosed); + resolve(); + } + }; + + this.add(dialogClosed); + }); + } +}; + +/** + * Represents a modal dialog. + * + * @param {function(): browser.Context} curBrowserFn + * Function that returns the current |browser.Context|. + * @param {DOMWindow} dialog + * DOMWindow of the dialog. + */ +modal.Dialog = class { + constructor(curBrowserFn, dialog) { + this.curBrowserFn_ = curBrowserFn; + this.win_ = Cu.getWeakReference(dialog); + } + + get args() { + if (lazy.AppInfo.isAndroid) { + return this.window.args; + } + let tm = this.tabModal; + return tm ? tm.args : null; + } + + get curBrowser_() { + return this.curBrowserFn_(); + } + + get isOpen() { + if (lazy.AppInfo.isAndroid) { + return this.window !== null; + } + if (!this.ui) { + return false; + } + return true; + } + + get isWindowModal() { + return [ + Services.prompt.MODAL_TYPE_WINDOW, + Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, + ].includes(this.args.modalType); + } + + get tabModal() { + let win = this.window; + if (win) { + return win.Dialog; + } + return this.curBrowser_.getTabModal(); + } + + get text() { + if (lazy.AppInfo.isAndroid) { + return this.window.getPromptText(); + } + return this.ui.infoBody.textContent; + } + + get ui() { + let tm = this.tabModal; + return tm ? tm.ui : null; + } + + /** + * For Android, this returns a GeckoViewPrompter, which can be used to control prompts. + * Otherwise, this returns the ChromeWindow associated with an open dialog window if + * it is currently attached to the DOM. + */ + get window() { + if (this.win_) { + let win = this.win_.get(); + if (win && (lazy.AppInfo.isAndroid || win.parent)) { + return win; + } + } + return null; + } + + set text(inputText) { + if (lazy.AppInfo.isAndroid) { + this.window.setInputText(inputText); + } else { + // see toolkit/components/prompts/content/commonDialog.js + let { loginTextbox } = this.ui; + loginTextbox.value = inputText; + } + } + + accept() { + if (lazy.AppInfo.isAndroid) { + // GeckoView does not have a UI, so the methods are called directly + this.window.acceptPrompt(); + } else { + const { button0 } = this.ui; + button0.click(); + } + } + + dismiss() { + if (lazy.AppInfo.isAndroid) { + // GeckoView does not have a UI, so the methods are called directly + this.window.dismissPrompt(); + } else { + const { button0, button1 } = this.ui; + (button1 ? button1 : button0).click(); + } + } +}; |