/* 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(); } } };