summaryrefslogtreecommitdiffstats
path: root/toolkit/components/prompts
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/prompts
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/prompts')
-rw-r--r--toolkit/components/prompts/content/adjustableTitle.js193
-rw-r--r--toolkit/components/prompts/content/commonDialog.css124
-rw-r--r--toolkit/components/prompts/content/commonDialog.js148
-rw-r--r--toolkit/components/prompts/content/commonDialog.xhtml103
-rw-r--r--toolkit/components/prompts/content/selectDialog.js83
-rw-r--r--toolkit/components/prompts/content/selectDialog.xhtml21
-rw-r--r--toolkit/components/prompts/content/tabprompts.css119
-rw-r--r--toolkit/components/prompts/content/tabprompts.sys.mjs298
-rw-r--r--toolkit/components/prompts/docs/index.rst10
-rw-r--r--toolkit/components/prompts/docs/modal-type-content.pngbin0 -> 35447 bytes
-rw-r--r--toolkit/components/prompts/docs/modal-type-tab.pngbin0 -> 40928 bytes
-rw-r--r--toolkit/components/prompts/docs/modal-type-window.pngbin0 -> 49368 bytes
-rw-r--r--toolkit/components/prompts/docs/modalTypes.rst95
-rw-r--r--toolkit/components/prompts/docs/nsIPromptService-reference.rst10
-rw-r--r--toolkit/components/prompts/docs/nsIPromptService.rst195
-rw-r--r--toolkit/components/prompts/jar.mn13
-rw-r--r--toolkit/components/prompts/moz.build24
-rw-r--r--toolkit/components/prompts/src/CommonDialog.sys.mjs381
-rw-r--r--toolkit/components/prompts/src/PromptUtils.sys.mjs186
-rw-r--r--toolkit/components/prompts/src/Prompter.sys.mjs1824
-rw-r--r--toolkit/components/prompts/src/components.conf26
-rw-r--r--toolkit/components/prompts/src/moz.build15
-rw-r--r--toolkit/components/prompts/test/.eslintrc.js8
-rw-r--r--toolkit/components/prompts/test/PromptTestUtils.sys.mjs237
-rw-r--r--toolkit/components/prompts/test/bug619644_inner.html7
-rw-r--r--toolkit/components/prompts/test/bug625187_iframe.html16
-rw-r--r--toolkit/components/prompts/test/chrome.ini11
-rw-r--r--toolkit/components/prompts/test/chromeScript.js353
-rw-r--r--toolkit/components/prompts/test/mochitest.ini19
-rw-r--r--toolkit/components/prompts/test/prompt_common.js445
-rw-r--r--toolkit/components/prompts/test/test_bug619644.html74
-rw-r--r--toolkit/components/prompts/test/test_bug620145.html96
-rw-r--r--toolkit/components/prompts/test/test_dom_prompts.html207
-rw-r--r--toolkit/components/prompts/test/test_modal_prompts.html1311
-rw-r--r--toolkit/components/prompts/test/test_modal_select.html138
-rw-r--r--toolkit/components/prompts/test/test_subresources_prompts.html200
36 files changed, 6990 insertions, 0 deletions
diff --git a/toolkit/components/prompts/content/adjustableTitle.js b/toolkit/components/prompts/content/adjustableTitle.js
new file mode 100644
index 0000000000..bd9afd909a
--- /dev/null
+++ b/toolkit/components/prompts/content/adjustableTitle.js
@@ -0,0 +1,193 @@
+/* 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";
+
+let { PromptUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromptUtils.sys.mjs"
+);
+
+const AdjustableTitle = {
+ _cssSnippet: `
+ #titleContainer {
+ /* This gets display: flex by virtue of being a row in a subdialog, from
+ * commonDialog.css . */
+ flex-shrink: 0;
+
+ flex-direction: row;
+ align-items: baseline;
+
+ margin-inline: 4px;
+ /* Ensure we don't exceed the bounds of the dialog: */
+ max-width: calc(100vw - 32px);
+
+ --icon-size: 16px;
+ }
+
+ #titleContainer[noicon] > .titleIcon {
+ display: none;
+ }
+
+ .titleIcon {
+ width: var(--icon-size);
+ height: var(--icon-size);
+ padding-inline-end: 4px;
+ flex-shrink: 0;
+
+ background-image: var(--icon-url, url("chrome://global/skin/icons/defaultFavicon.svg"));
+ background-size: 16px 16px;
+ background-origin: content-box;
+ background-repeat: no-repeat;
+ background-color: var(--in-content-page-background);
+ -moz-context-properties: fill;
+ fill: currentColor;
+ }
+
+ #titleCropper:not([nomaskfade]) {
+ display: inline-flex;
+ }
+
+ #titleCropper {
+ overflow: hidden;
+
+ justify-content: right;
+ mask-repeat: no-repeat;
+ /* go from left to right with the mask: */
+ --mask-dir: right;
+ }
+
+ #titleContainer:not([noicon]) > #titleCropper {
+ /* Align the icon and text: */
+ translate: 0 calc(-1px - max(.6 * var(--icon-size) - .6em, 0px));
+ }
+
+ #titleCropper[rtlorigin] {
+ justify-content: left;
+ /* go from right to left with the mask: */
+ --mask-dir: left;
+ }
+
+
+ #titleCropper:not([nomaskfade]) #titleText {
+ display: inline-flex;
+ white-space: nowrap;
+ }
+
+ #titleText {
+ font-weight: 600;
+ flex: 1 0 auto; /* Grow but do not shrink. */
+ unicode-bidi: plaintext; /* Ensure we align RTL text correctly. */
+ text-align: match-parent;
+ }
+
+ #titleCropper[overflown] {
+ mask-image: linear-gradient(to var(--mask-dir), transparent, black 100px);
+ }
+
+ /* hide the old title */
+ #infoTitle {
+ display: none;
+ }
+ `,
+
+ _insertMarkup() {
+ let iconEl = document.createElement("span");
+ iconEl.className = "titleIcon";
+ this._titleCropEl = document.createElement("span");
+ this._titleCropEl.id = "titleCropper";
+ this._titleEl = document.createElement("span");
+ this._titleEl.id = "titleText";
+ this._containerEl = document.createElement("div");
+ this._containerEl.id = "titleContainer";
+ this._containerEl.className = "dialogRow titleContainer";
+ this._titleCropEl.append(this._titleEl);
+ this._containerEl.append(iconEl, this._titleCropEl);
+ let targetID = document.documentElement.getAttribute("headerparent");
+ document.getElementById(targetID).prepend(this._containerEl);
+ let styleEl = document.createElement("style");
+ styleEl.textContent = this._cssSnippet;
+ document.documentElement.prepend(styleEl);
+ },
+
+ _overflowHandler() {
+ requestAnimationFrame(async () => {
+ let isOverflown;
+ try {
+ isOverflown = await window.promiseDocumentFlushed(() => {
+ return (
+ this._titleCropEl.getBoundingClientRect().width <
+ this._titleEl.getBoundingClientRect().width
+ );
+ });
+ } catch (ex) {
+ // In automated tests, this can fail with a DOM exception if
+ // the window has closed by the time layout tries to call us.
+ // In this case, just bail, and only log any other errors:
+ if (
+ !DOMException.isInstance(ex) ||
+ ex.name != "NoModificationAllowedError"
+ ) {
+ console.error(ex);
+ }
+ return;
+ }
+ this._titleCropEl.toggleAttribute("overflown", isOverflown);
+ if (isOverflown) {
+ this._titleEl.setAttribute("title", this._titleEl.textContent);
+ } else {
+ this._titleEl.removeAttribute("title");
+ }
+ });
+ },
+
+ _updateTitle(title) {
+ title = JSON.parse(title);
+ if (title.raw) {
+ this._titleEl.textContent = title.raw;
+ let { DIRECTION_RTL } = window.windowUtils;
+ this._titleCropEl.toggleAttribute(
+ "rtlorigin",
+ window.windowUtils.getDirectionFromText(title.raw) == DIRECTION_RTL
+ );
+ } else {
+ document.l10n.setAttributes(this._titleEl, title.l10nId);
+ }
+
+ if (!document.documentElement.hasAttribute("neediconheader")) {
+ this._containerEl.setAttribute("noicon", "true");
+ } else if (title.shouldUseMaskFade) {
+ this._overflowHandler();
+ } else {
+ this._titleCropEl.toggleAttribute("nomaskfade", true);
+ }
+ },
+
+ init() {
+ // Only run this if we're embedded and proton modals are enabled.
+ if (!window.docShell.chromeEventHandler) {
+ return;
+ }
+
+ this._insertMarkup();
+ let title = document.documentElement.getAttribute("headertitle");
+ if (title) {
+ this._updateTitle(title);
+ }
+ this._mutObs = new MutationObserver(() => {
+ this._updateTitle(document.documentElement.getAttribute("headertitle"));
+ });
+ this._mutObs.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["headertitle"],
+ });
+ },
+};
+
+document.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ AdjustableTitle.init();
+ },
+ { once: true }
+);
diff --git a/toolkit/components/prompts/content/commonDialog.css b/toolkit/components/prompts/content/commonDialog.css
new file mode 100644
index 0000000000..3521af13c6
--- /dev/null
+++ b/toolkit/components/prompts/content/commonDialog.css
@@ -0,0 +1,124 @@
+/* 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/. */
+
+:root {
+ min-width: 29em;
+}
+
+dialog[insecureauth] {
+ --icon-url: url("chrome://global/skin/icons/security-broken.svg");
+}
+
+#dialogGrid {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ align-items: center;
+ max-height: 100%;
+}
+
+.dialogRow:not([hidden]) {
+ display: contents;
+}
+
+#iconContainer {
+ align-self: start;
+}
+
+#infoContainer {
+ max-width: 45em;
+}
+
+#infoTitle {
+ margin-bottom: 1em;
+}
+
+#infoBody {
+ -moz-user-focus: normal;
+ user-select: text;
+ cursor: text !important;
+ white-space: pre-wrap;
+ word-break: break-word;
+ unicode-bidi: plaintext;
+ overflow-y: auto;
+}
+
+.sizeDetermined,
+.sizeDetermined::part(content-box),
+.sizeDetermined #infoBody,
+.sizeDetermined #infoRow,
+.sizeDetermined #infoContainer {
+ /* Allow stuff to shrink */
+ min-height: 0;
+}
+
+.sizeDetermined #infoRow {
+ /* Make the info row take all the available space, potentially shrinking,
+ * since it's what contains the infoBody, which is scrollable */
+ flex: 1;
+}
+
+#loginLabel, #password1Label {
+ text-align: start;
+}
+
+#loginTextbox,
+#password1Textbox {
+ text-align: match-parent;
+}
+
+/* use flexbox instead of grid: */
+dialog[subdialog],
+dialog[subdialog] #dialogGrid,
+dialog[subdialog] #infoContainer,
+dialog[subdialog] .dialogRow:not([hidden]) {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
+
+dialog[subdialog] #iconContainer {
+ display: none;
+}
+
+/* Fix padding/spacing */
+dialog[subdialog] {
+ --grid-padding: 16px;
+ /* All the inner items should have 4px inline margin, leading to 1.16em spacing
+ * between the dialog and its contents, and 8px horizontal spacing between items. */
+ padding: var(--grid-padding) calc(var(--grid-padding) - 4px);
+}
+
+/* Use an ID selector for the dialog to win on specificity... */
+#commonDialog[subdialog] description,
+#commonDialog[subdialog] checkbox {
+ margin: 0 4px;
+}
+
+#commonDialog[subdialog] label {
+ margin: 4px; /* Labels for input boxes should get block as well as the regular inline margin. */
+}
+
+#commonDialog[subdialog] .checkbox-label {
+ /* The checkbox already has the horizontal margin, so override the rule
+ * above. */
+ margin: 0;
+}
+
+#commonDialog[subdialog] input {
+ margin: 0 4px 4px;
+}
+
+/* Add vertical spaces between rows: */
+dialog[subdialog] .dialogRow {
+ margin-block: 0 var(--grid-padding);
+}
+
+/* Fix spacing in the `prompt()` case: */
+dialog[subdialog] #loginLabel[value=""] {
+ display: none;
+}
+
+dialog[subdialog][windowtype="prompt:prompt"] #infoRow {
+ margin-bottom: 4px;
+}
diff --git a/toolkit/components/prompts/content/commonDialog.js b/toolkit/components/prompts/content/commonDialog.js
new file mode 100644
index 0000000000..a18b7cbd6c
--- /dev/null
+++ b/toolkit/components/prompts/content/commonDialog.js
@@ -0,0 +1,148 @@
+/* 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 { CommonDialog } = ChromeUtils.importESModule(
+ "resource://gre/modules/CommonDialog.sys.mjs"
+);
+
+// imported by adjustableTitle.js loaded in the same context:
+/* globals PromptUtils */
+
+var propBag, args, Dialog;
+
+// Inherit color scheme overrides from parent window. This is to inherit the
+// color scheme of dark themed PBM windows.
+{
+ let openerColorSchemeOverride =
+ window.opener?.browsingContext?.top.prefersColorSchemeOverride;
+ if (
+ openerColorSchemeOverride &&
+ window.browsingContext == window.browsingContext.top
+ ) {
+ window.browsingContext.prefersColorSchemeOverride =
+ openerColorSchemeOverride;
+ }
+}
+
+function commonDialogOnLoad() {
+ propBag = window.arguments[0]
+ .QueryInterface(Ci.nsIWritablePropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag);
+ // Convert to a JS object
+ args = {};
+ for (let prop of propBag.enumerator) {
+ args[prop.name] = prop.value;
+ }
+
+ let dialog = document.getElementById("commonDialog");
+
+ let needIconifiedHeader =
+ args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT ||
+ ["promptUserAndPass", "promptPassword"].includes(args.promptType) ||
+ args.headerIconURL;
+ let root = document.documentElement;
+ if (needIconifiedHeader) {
+ root.setAttribute("neediconheader", "true");
+ }
+ let title = { raw: args.title };
+ let { promptPrincipal } = args;
+ if (promptPrincipal) {
+ if (promptPrincipal.isNullPrincipal) {
+ title = { l10nId: "common-dialog-title-null" };
+ } else if (promptPrincipal.isSystemPrincipal) {
+ title = { l10nId: "common-dialog-title-system" };
+ root.style.setProperty(
+ "--icon-url",
+ "url('chrome://branding/content/icon32.png')"
+ );
+ } else if (promptPrincipal.addonPolicy) {
+ title.raw = promptPrincipal.addonPolicy.name;
+ } else if (promptPrincipal.isContentPrincipal) {
+ try {
+ title.raw = promptPrincipal.URI.displayHostPort;
+ } catch (ex) {
+ // hostPort getter can throw, e.g. for about URIs.
+ title.raw = promptPrincipal.originNoSuffix;
+ }
+ // hostPort can be empty for file URIs.
+ if (!title.raw) {
+ title.raw = promptPrincipal.prePath;
+ }
+ } else {
+ title = { l10nId: "common-dialog-title-unknown" };
+ }
+ } else if (args.authOrigin) {
+ title = { raw: args.authOrigin };
+ }
+ if (args.headerIconURL) {
+ root.style.setProperty("--icon-url", `url('${args.headerIconURL}')`);
+ }
+ // Fade and crop potentially long raw titles, e.g., origins and hostnames.
+ title.shouldUseMaskFade = title.raw && (args.authOrigin || promptPrincipal);
+ root.setAttribute("headertitle", JSON.stringify(title));
+ if (args.isInsecureAuth) {
+ dialog.setAttribute("insecureauth", "true");
+ }
+
+ let ui = {
+ prompt: window,
+ loginContainer: document.getElementById("loginContainer"),
+ loginTextbox: document.getElementById("loginTextbox"),
+ loginLabel: document.getElementById("loginLabel"),
+ password1Container: document.getElementById("password1Container"),
+ password1Textbox: document.getElementById("password1Textbox"),
+ password1Label: document.getElementById("password1Label"),
+ infoRow: document.getElementById("infoRow"),
+ infoBody: document.getElementById("infoBody"),
+ infoTitle: document.getElementById("infoTitle"),
+ infoIcon: document.getElementById("infoIcon"),
+ checkbox: document.getElementById("checkbox"),
+ checkboxContainer: document.getElementById("checkboxContainer"),
+ button3: dialog.getButton("extra2"),
+ button2: dialog.getButton("extra1"),
+ button1: dialog.getButton("cancel"),
+ button0: dialog.getButton("accept"),
+ focusTarget: window,
+ };
+
+ Dialog = new CommonDialog(args, ui);
+ window.addEventListener("dialogclosing", function (aEvent) {
+ if (aEvent.detail?.abort) {
+ Dialog.abortPrompt();
+ }
+ });
+ document.addEventListener("dialogaccept", function () {
+ Dialog.onButton0();
+ });
+ document.addEventListener("dialogcancel", function () {
+ Dialog.onButton1();
+ });
+ document.addEventListener("dialogextra1", function () {
+ Dialog.onButton2();
+ window.close();
+ });
+ document.addEventListener("dialogextra2", function () {
+ Dialog.onButton3();
+ window.close();
+ });
+ document.subDialogSetDefaultFocus = isInitialFocus =>
+ Dialog.setDefaultFocus(isInitialFocus);
+ Dialog.onLoad(dialog);
+
+ // resize the window to the content
+ window.sizeToContent();
+
+ // If the icon hasn't loaded yet, size the window to the content again when
+ // it does, as its layout can change.
+ ui.infoIcon.addEventListener("load", () => window.sizeToContent());
+
+ window.getAttention();
+}
+
+function commonDialogOnUnload() {
+ // Convert args back into property bag
+ for (let propName in args) {
+ propBag.setProperty(propName, args[propName]);
+ }
+}
diff --git a/toolkit/components/prompts/content/commonDialog.xhtml b/toolkit/components/prompts/content/commonDialog.xhtml
new file mode 100644
index 0000000000..02e0749a9e
--- /dev/null
+++ b/toolkit/components/prompts/content/commonDialog.xhtml
@@ -0,0 +1,103 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/content/commonDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/commonDialog.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window
+ id="commonDialogWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ aria-describedby="infoBody"
+ headerparent="dialogGrid"
+ onunload="commonDialogOnUnload();"
+>
+ <dialog id="commonDialog" buttonpack="end">
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="toolkit/global/commonDialog.ftl" />
+ </linkset>
+ <script src="chrome://global/content/adjustableTitle.js" />
+ <script src="chrome://global/content/commonDialog.js" />
+ <script src="chrome://global/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://global/content/customElements.js" />
+ <script>
+ /* eslint-disable no-undef */
+ document.addEventListener("DOMContentLoaded", function () {
+ commonDialogOnLoad();
+ });
+ </script>
+
+ <commandset id="selectEditMenuItems">
+ <command
+ id="cmd_copy"
+ oncommand="goDoCommand('cmd_copy')"
+ disabled="true"
+ />
+ <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')" />
+ </commandset>
+
+ <popupset id="contentAreaContextSet">
+ <menupopup
+ id="contentAreaContextMenu"
+ onpopupshowing="goUpdateCommand('cmd_copy')"
+ >
+ <menuitem
+ id="context-copy"
+ data-l10n-id="common-dialog-copy-cmd"
+ command="cmd_copy"
+ disabled="true"
+ />
+ <menuitem
+ id="context-selectall"
+ data-l10n-id="common-dialog-select-all-cmd"
+ command="cmd_selectAll"
+ />
+ </menupopup>
+ </popupset>
+
+ <div xmlns="http://www.w3.org/1999/xhtml" id="dialogGrid">
+ <div class="dialogRow" id="infoRow" hidden="hidden">
+ <div id="iconContainer">
+ <xul:image id="infoIcon" />
+ </div>
+ <div id="infoContainer">
+ <xul:description id="infoTitle" />
+ <xul:description
+ id="infoBody"
+ context="contentAreaContextMenu"
+ noinitialfocus="true"
+ />
+ </div>
+ </div>
+ <div id="loginContainer" class="dialogRow" hidden="hidden">
+ <xul:label
+ id="loginLabel"
+ data-l10n-id="common-dialog-username"
+ control="loginTextbox"
+ />
+ <input type="text" id="loginTextbox" dir="ltr" />
+ </div>
+ <div id="password1Container" class="dialogRow" hidden="hidden">
+ <xul:label
+ id="password1Label"
+ data-l10n-id="common-dialog-password"
+ control="password1Textbox"
+ />
+ <input type="password" id="password1Textbox" dir="ltr" />
+ </div>
+ <div id="checkboxContainer" class="dialogRow" hidden="hidden">
+ <div />
+ <!-- spacer -->
+ <xul:checkbox id="checkbox" oncommand="Dialog.onCheckbox()" />
+ </div>
+ </div>
+ </dialog>
+</window>
diff --git a/toolkit/components/prompts/content/selectDialog.js b/toolkit/components/prompts/content/selectDialog.js
new file mode 100644
index 0000000000..86809bc879
--- /dev/null
+++ b/toolkit/components/prompts/content/selectDialog.js
@@ -0,0 +1,83 @@
+/* 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/. */
+
+// Defined in dialog.xml.
+/* globals centerWindowOnScreen:false, moveToAlertPosition:false */
+
+var propBag, listBox, args;
+
+function onDCL() {
+ propBag = window.arguments[0]
+ .QueryInterface(Ci.nsIWritablePropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag);
+
+ // Convert to a JS object
+ let args = {};
+ for (let prop of propBag.enumerator) {
+ args[prop.name] = prop.value;
+ }
+
+ let promptType = propBag.getProperty("promptType");
+ if (promptType != "select") {
+ console.error("selectDialog opened for unknown type: ", promptType);
+ window.close();
+ }
+
+ // Default to canceled.
+ propBag.setProperty("ok", false);
+
+ document.title = propBag.getProperty("title");
+
+ let text = propBag.getProperty("text");
+ document.getElementById("info.txt").setAttribute("value", text);
+
+ let items = propBag.getProperty("list");
+ listBox = document.getElementById("list");
+
+ for (let i = 0; i < items.length; i++) {
+ let str = items[i];
+ if (str == "") {
+ str = "<>";
+ }
+ listBox.appendItem(str);
+ listBox.getItemAtIndex(i).addEventListener("dblclick", dialogDoubleClick);
+ }
+ listBox.selectedIndex = 0;
+}
+
+function onLoad() {
+ listBox.focus();
+
+ document.addEventListener("dialogaccept", dialogOK);
+ // resize the window to the content
+ window.sizeToContent();
+
+ // Move to the right location
+ moveToAlertPosition();
+ centerWindowOnScreen();
+
+ // play sound
+ try {
+ if (!args.openedWithTabDialog) {
+ Cc["@mozilla.org/sound;1"]
+ .getService(Ci.nsISound)
+ .playEventSound(Ci.nsISound.EVENT_SELECT_DIALOG_OPEN);
+ }
+ } catch (e) {}
+
+ Services.obs.notifyObservers(window, "select-dialog-loaded");
+}
+
+function dialogOK() {
+ propBag.setProperty("selected", listBox.selectedIndex);
+ propBag.setProperty("ok", true);
+}
+
+function dialogDoubleClick() {
+ dialogOK();
+ window.close();
+}
+
+document.addEventListener("DOMContentLoaded", onDCL);
+window.addEventListener("load", onLoad, { once: true });
diff --git a/toolkit/components/prompts/content/selectDialog.xhtml b/toolkit/components/prompts/content/selectDialog.xhtml
new file mode 100644
index 0000000000..27963ebd57
--- /dev/null
+++ b/toolkit/components/prompts/content/selectDialog.xhtml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<!DOCTYPE window>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <dialog>
+ <script src="chrome://global/content/selectDialog.js" />
+ <keyset id="dialogKeys" />
+ <vbox style="width: 24em; margin: 5px">
+ <label id="info.txt" />
+ <vbox>
+ <richlistbox id="list" class="theme-listbox" style="height: 8em" />
+ </vbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/toolkit/components/prompts/content/tabprompts.css b/toolkit/components/prompts/content/tabprompts.css
new file mode 100644
index 0000000000..539e8792cc
--- /dev/null
+++ b/toolkit/components/prompts/content/tabprompts.css
@@ -0,0 +1,119 @@
+/* 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/. */
+
+/* Tab Modal Prompt boxes */
+
+.tabModalBackground {
+ justify-content: center;
+ flex-direction: column;
+}
+
+.tabModalBackground,
+tabmodalprompt {
+ width: 100%;
+ height: 100%;
+}
+
+tabmodalprompt {
+ --tabmodalprompt-padding: 20px;
+ overflow: hidden;
+ text-shadow: none; /* remove lightweight theme text-shadow */
+}
+
+tabmodalprompt:not([hidden]) {
+ display: grid;
+ grid-template-rows: 1fr [dialog-start] auto [dialog-end] 2fr;
+ justify-items: center;
+}
+
+/*
+ Adjustments for chrome level tab-prompts to make them
+ overlap with the upper chrome UI and move them in
+ front of content prompts.
+*/
+tabmodalprompt.tab-prompt {
+ overflow: visible;
+ z-index: 1;
+ grid-template-rows: [dialog-start] auto [dialog-end] 1fr;
+}
+
+tabmodalprompt.tab-prompt .tabmodalprompt-mainContainer {
+ margin-top: -5px;
+}
+
+.tabmodalprompt-mainContainer {
+ min-width: 20em;
+ min-height: 12em;
+ max-width: calc(60% + calc(var(--tabmodalprompt-padding) * 2));
+ -moz-user-focus: normal;
+ grid-row: dialog;
+
+ display: flex;
+ flex-direction: column;
+}
+
+.tabmodalprompt-topContainer {
+ flex-grow: 1;
+ padding: var(--tabmodalprompt-padding);
+ display: grid;
+ grid-template-columns: auto 1fr;
+ align-items: baseline;
+ align-content: center; /* center content vertically */
+ max-width: 100%;
+ min-height: 0;
+ max-height: 60vh;
+ box-sizing: border-box;
+}
+
+.tabmodalprompt-topContainer > div:not(.tabmodalprompt-infoContainer, [hidden]) {
+ display: contents;
+}
+
+.tabmodalprompt-infoContainer {
+ grid-column: span 2;
+
+ display: block;
+ margin-block: auto;
+ max-width: 100%;
+ height: 100%;
+ min-height: 0;
+ justify-self: center; /* center text, but only when it fits in one line */
+}
+
+/* When all elements in the first column are hidden, prevent the second column
+ from becoming the first one because it won't have the right fraction */
+.tabmodalprompt-topContainer > div > *:nth-child(2) {
+ grid-column: 2;
+}
+
+.infoTitle {
+ margin-bottom: 1em !important;
+ font-weight: bold;
+}
+
+.infoBody {
+ margin: 0 !important;
+ -moz-user-focus: normal;
+ user-select: text;
+ cursor: text !important;
+ white-space: pre-wrap;
+ unicode-bidi: plaintext;
+ outline: none; /* remove focus outline */
+ overflow: auto;
+ max-width: 100%;
+ max-height: 100%;
+}
+
+tabmodalprompt label[value=""] {
+ display: none;
+}
+
+.tabmodalprompt-buttonContainer {
+ display: flex;
+ padding: 12px var(--tabmodalprompt-padding) 15px;
+}
+
+.tabmodalprompt-buttonSpacer {
+ flex-grow: 1;
+}
diff --git a/toolkit/components/prompts/content/tabprompts.sys.mjs b/toolkit/components/prompts/content/tabprompts.sys.mjs
new file mode 100644
index 0000000000..7edb024fa8
--- /dev/null
+++ b/toolkit/components/prompts/content/tabprompts.sys.mjs
@@ -0,0 +1,298 @@
+/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+export var TabModalPrompt = class {
+ constructor(win) {
+ this.win = win;
+ let newPrompt = (this.element =
+ win.document.createElement("tabmodalprompt"));
+ win.MozXULElement.insertFTLIfNeeded("toolkit/global/tabprompts.ftl");
+ newPrompt.setAttribute("role", "dialog");
+ let randomIdSuffix = Math.random().toString(32).substring(2);
+ newPrompt.setAttribute("aria-describedby", `infoBody-${randomIdSuffix}`);
+ newPrompt.appendChild(
+ win.MozXULElement.parseXULToFragment(
+ `
+ <div class="tabmodalprompt-mainContainer" xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <div class="tabmodalprompt-topContainer">
+ <div class="tabmodalprompt-infoContainer">
+ <div class="tabmodalprompt-infoTitle infoTitle" hidden="hidden"/>
+ <div class="tabmodalprompt-infoBody infoBody" id="infoBody-${randomIdSuffix}" tabindex="-1"/>
+ </div>
+
+ <div class="tabmodalprompt-loginContainer" hidden="hidden">
+ <xul:label class="tabmodalprompt-loginLabel" data-l10n-id="tabmodalprompt-username" control="loginTextbox-${randomIdSuffix}"/>
+ <input class="tabmodalprompt-loginTextbox" id="loginTextbox-${randomIdSuffix}"/>
+ </div>
+
+ <div class="tabmodalprompt-password1Container" hidden="hidden">
+ <xul:label class="tabmodalprompt-password1Label" data-l10n-id="tabmodalprompt-password" control="password1Textbox-${randomIdSuffix}"/>
+ <input class="tabmodalprompt-password1Textbox" type="password" id="password1Textbox-${randomIdSuffix}"/>
+ </div>
+
+ <div class="tabmodalprompt-checkboxContainer" hidden="hidden">
+ <div/>
+ <xul:checkbox class="tabmodalprompt-checkbox"/>
+ </div>
+
+ <!-- content goes here -->
+ </div>
+ <div class="tabmodalprompt-buttonContainer">
+ <xul:button class="tabmodalprompt-button3" hidden="true"/>
+ <div class="tabmodalprompt-buttonSpacer"/>
+ <xul:button class="tabmodalprompt-button0" data-l10n-id="tabmodalprompt-ok-button"/>
+ <xul:button class="tabmodalprompt-button2" hidden="true"/>
+ <xul:button class="tabmodalprompt-button1" data-l10n-id="tabmodalprompt-cancel-button"/>
+ </div>
+ </div>`
+ )
+ );
+
+ this.ui = {
+ prompt: this,
+ promptContainer: this.element,
+ mainContainer: newPrompt.querySelector(".tabmodalprompt-mainContainer"),
+ loginContainer: newPrompt.querySelector(".tabmodalprompt-loginContainer"),
+ loginTextbox: newPrompt.querySelector(".tabmodalprompt-loginTextbox"),
+ loginLabel: newPrompt.querySelector(".tabmodalprompt-loginLabel"),
+ password1Container: newPrompt.querySelector(
+ ".tabmodalprompt-password1Container"
+ ),
+ password1Textbox: newPrompt.querySelector(
+ ".tabmodalprompt-password1Textbox"
+ ),
+ password1Label: newPrompt.querySelector(".tabmodalprompt-password1Label"),
+ infoContainer: newPrompt.querySelector(".tabmodalprompt-infoContainer"),
+ infoBody: newPrompt.querySelector(".tabmodalprompt-infoBody"),
+ infoTitle: newPrompt.querySelector(".tabmodalprompt-infoTitle"),
+ infoIcon: null,
+ rows: newPrompt.querySelector(".tabmodalprompt-topContainer"),
+ checkbox: newPrompt.querySelector(".tabmodalprompt-checkbox"),
+ checkboxContainer: newPrompt.querySelector(
+ ".tabmodalprompt-checkboxContainer"
+ ),
+ button3: newPrompt.querySelector(".tabmodalprompt-button3"),
+ button2: newPrompt.querySelector(".tabmodalprompt-button2"),
+ button1: newPrompt.querySelector(".tabmodalprompt-button1"),
+ button0: newPrompt.querySelector(".tabmodalprompt-button0"),
+ // focusTarget (for BUTTON_DELAY_ENABLE) not yet supported
+ };
+
+ if (AppConstants.XP_UNIX) {
+ // Reorder buttons on Linux
+ let buttonContainer = newPrompt.querySelector(
+ ".tabmodalprompt-buttonContainer"
+ );
+ buttonContainer.appendChild(this.ui.button3);
+ buttonContainer.appendChild(this.ui.button2);
+ buttonContainer.appendChild(
+ newPrompt.querySelector(".tabmodalprompt-buttonSpacer")
+ );
+ buttonContainer.appendChild(this.ui.button1);
+ buttonContainer.appendChild(this.ui.button0);
+ }
+
+ this.ui.button0.addEventListener(
+ "command",
+ this.onButtonClick.bind(this, 0)
+ );
+ this.ui.button1.addEventListener(
+ "command",
+ this.onButtonClick.bind(this, 1)
+ );
+ this.ui.button2.addEventListener(
+ "command",
+ this.onButtonClick.bind(this, 2)
+ );
+ this.ui.button3.addEventListener(
+ "command",
+ this.onButtonClick.bind(this, 3)
+ );
+ // Anonymous wrapper used here because |Dialog| doesn't exist until init() is called!
+ this.ui.checkbox.addEventListener("command", () => {
+ this.Dialog.onCheckbox();
+ });
+
+ /**
+ * Based on dialog.xml handlers
+ */
+ this.element.addEventListener(
+ "keypress",
+ event => {
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_RETURN:
+ this.onKeyAction("default", event);
+ break;
+
+ case KeyEvent.DOM_VK_ESCAPE:
+ this.onKeyAction("cancel", event);
+ break;
+
+ default:
+ if (
+ AppConstants.platform == "macosx" &&
+ event.key == "." &&
+ event.metaKey
+ ) {
+ this.onKeyAction("cancel", event);
+ }
+ break;
+ }
+ },
+ { mozSystemGroup: true }
+ );
+
+ this.element.addEventListener(
+ "focus",
+ event => {
+ let bnum = this.args.defaultButtonNum || 0;
+ let defaultButton = this.ui["button" + bnum];
+
+ if (AppConstants.platform == "macosx") {
+ // On OS X, the default button always stays marked as such (until
+ // the entire prompt blurs).
+ defaultButton.setAttribute("default", "true");
+ } else {
+ // On other platforms, the default button is only marked as such
+ // when no other button has focus. XUL buttons on not-OSX will
+ // react to pressing enter as a command, so you can't trigger the
+ // default without tabbing to it or something that isn't a button.
+ let focusedDefault = event.originalTarget == defaultButton;
+ let someButtonFocused =
+ event.originalTarget.localName == "button" ||
+ event.originalTarget.localName == "toolbarbutton";
+ if (focusedDefault || !someButtonFocused) {
+ defaultButton.setAttribute("default", "true");
+ }
+ }
+ },
+ true
+ );
+
+ this.element.addEventListener("blur", () => {
+ // If focus shifted to somewhere else in the browser, don't make
+ // the default button look active.
+ let bnum = this.args.defaultButtonNum || 0;
+ let button = this.ui["button" + bnum];
+ button.removeAttribute("default");
+ });
+ }
+
+ init(args, linkedTab, onCloseCallback) {
+ this.args = args;
+ this.linkedTab = linkedTab;
+ this.onCloseCallback = onCloseCallback;
+
+ if (args.enableDelay) {
+ throw new Error(
+ "BUTTON_DELAY_ENABLE not yet supported for tab-modal prompts"
+ );
+ }
+
+ // Apply styling depending on modalType (content or tab prompt)
+ if (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) {
+ this.element.classList.add("tab-prompt");
+ } else {
+ this.element.classList.add("content-prompt");
+ }
+
+ // We need to remove the prompt when the tab or browser window is closed or
+ // the page navigates, else we never unwind the event loop and that's sad times.
+ // Remember to cleanup in shutdownPrompt()!
+ this.win.addEventListener("resize", this);
+ this.win.addEventListener("unload", this);
+ if (linkedTab) {
+ linkedTab.addEventListener("TabClose", this);
+ }
+ // Note:
+ // nsPrompter.js or in e10s mode browser-parent.js call abortPrompt,
+ // when the domWindow, for which the prompt was created, generates
+ // a "pagehide" event.
+
+ let { CommonDialog } = ChromeUtils.importESModule(
+ "resource://gre/modules/CommonDialog.sys.mjs"
+ );
+ this.Dialog = new CommonDialog(args, this.ui);
+ this.Dialog.onLoad(null);
+
+ // For content prompts display the tabprompt title that shows the prompt origin when
+ // the prompt origin is not the same as that of the top window.
+ if (
+ args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT &&
+ args.showCallerOrigin
+ ) {
+ this.ui.infoTitle.removeAttribute("hidden");
+ }
+
+ // TODO: should unhide buttonSpacer on Windows when there are 4 buttons.
+ // Better yet, just drop support for 4-button dialogs. (bug 609510)
+ }
+
+ shutdownPrompt() {
+ // remove our event listeners
+ try {
+ this.win.removeEventListener("resize", this);
+ this.win.removeEventListener("unload", this);
+ if (this.linkedTab) {
+ this.linkedTab.removeEventListener("TabClose", this);
+ }
+ } catch (e) {}
+ // invoke callback
+ this.onCloseCallback();
+ this.win = null;
+ this.ui = null;
+ // Intentionally not cleaning up |this.element| here --
+ // TabModalPromptBox.removePrompt() would need it and it might not
+ // be called yet -- see browser_double_close_tabs.js.
+ }
+
+ abortPrompt() {
+ // Called from other code when the page changes.
+ this.Dialog.abortPrompt();
+ this.shutdownPrompt();
+ }
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ case "TabClose":
+ this.abortPrompt();
+ break;
+ }
+ }
+
+ onButtonClick(buttonNum) {
+ // We want to do all the work her asynchronously off a Gecko
+ // runnable, because of situations like the one described in
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1167575#c35 : we
+ // get here off processing of an OS event and will also process
+ // one more Gecko runnable before we break out of the event loop
+ // spin whoever posted the prompt is doing. If we do all our
+ // work sync, we will exit modal state _before_ processing that
+ // runnable, and if exiting moral state posts a runnable we will
+ // incorrectly process that runnable before leaving our event
+ // loop spin.
+ Services.tm.dispatchToMainThread(() => {
+ this.Dialog["onButton" + buttonNum]();
+ this.shutdownPrompt();
+ });
+ }
+
+ onKeyAction(action, event) {
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ event.stopPropagation();
+ if (action == "default") {
+ let bnum = this.args.defaultButtonNum || 0;
+ this.onButtonClick(bnum);
+ } else {
+ // action == "cancel"
+ this.onButtonClick(1); // Cancel button
+ }
+ }
+};
diff --git a/toolkit/components/prompts/docs/index.rst b/toolkit/components/prompts/docs/index.rst
new file mode 100644
index 0000000000..c518bef41b
--- /dev/null
+++ b/toolkit/components/prompts/docs/index.rst
@@ -0,0 +1,10 @@
+=======
+Prompts
+=======
+
+.. toctree::
+ :maxdepth: 1
+
+ nsIPromptService
+ nsIPromptService-reference
+ modalTypes
diff --git a/toolkit/components/prompts/docs/modal-type-content.png b/toolkit/components/prompts/docs/modal-type-content.png
new file mode 100644
index 0000000000..730fe7828c
--- /dev/null
+++ b/toolkit/components/prompts/docs/modal-type-content.png
Binary files differ
diff --git a/toolkit/components/prompts/docs/modal-type-tab.png b/toolkit/components/prompts/docs/modal-type-tab.png
new file mode 100644
index 0000000000..66da8ef782
--- /dev/null
+++ b/toolkit/components/prompts/docs/modal-type-tab.png
Binary files differ
diff --git a/toolkit/components/prompts/docs/modal-type-window.png b/toolkit/components/prompts/docs/modal-type-window.png
new file mode 100644
index 0000000000..505850e8de
--- /dev/null
+++ b/toolkit/components/prompts/docs/modal-type-window.png
Binary files differ
diff --git a/toolkit/components/prompts/docs/modalTypes.rst b/toolkit/components/prompts/docs/modalTypes.rst
new file mode 100644
index 0000000000..1425d6832c
--- /dev/null
+++ b/toolkit/components/prompts/docs/modalTypes.rst
@@ -0,0 +1,95 @@
+==================
+Prompt Modal Types
+==================
+
+Window Prompts
+--------------
+
+Window prompts are system prompts. They are clearly distinguishable from website
+content and can be opened with or without a parent window.
+While a window prompt is open, the parent window cannot be interacted with.
+That means the user can not close the window or switch tabs.
+Providing a parent window is optional, but highly encouraged. If you do not
+pass a parent the implementation will try to find one or fallback to aborted
+standalone window.
+
+**When to use**: This should be reserved for important browser-wide messages
+with the intent to block any further user interaction until the message has been
+read.
+
+**Flag**: `MODAL_TYPE_WINDOW`
+
+.. caution::
+
+ When using window prompts, make sure they can not be spawned by web content.
+ We've seen `cases <https://bugzilla.mozilla.org/show_bug.cgi?id=1571003>`_
+ of DoS attacks in the wild where websites spammed window prompts to lock up
+ the browser.
+ This prompt type should only be used when necessary and with proper rate
+ limiting. Most of the time, a tab prompt can be be used.
+
+.. figure:: modal-type-window.png
+ :width: 425px
+ :height: 230px
+ :alt: Screenshot of a window prompt.
+
+ Window alert prompt
+
+Tab Prompts
+-----------
+
+Tab prompts are system prompts like window prompts. As opposed to window
+prompts, they are tab modal and don't steal focus from the parent window.
+Multiple tab prompts cannot be shown at the same time. When opening additional
+prompts, they are FIFO queued.
+
+When the user closes the tab or navigates to a different URI, prompts associated
+with the given tab are closed.
+In this case an exception will be thrown:
+
+.. code-block::
+
+ /*
+ Exception: prompt aborted by user
+ undefined:425
+ */
+
+**When to use**: This prompt should be used for dialogs that were caused by web
+content and thus should be bound to the scope and lifetime of a specific tab,
+but should still look like secure browser UI. Examples are HTTP Auth prompt or
+the dialog to add a new search provider for the current website.
+
+**Flag**: `MODAL_TYPE_TAB`
+
+
+.. figure:: modal-type-tab.png
+ :width: 425px
+ :height: 230px
+ :alt: Screenshot of a tab prompt.
+
+ Tab alert prompt
+
+Content Prompts
+---------------
+
+Content prompts are like tab prompts, but they belong to the web content. Thus,
+they are positioned in the center of the selected browser.
+
+**When to use**: The prompt is triggered by or as a result of an action of web
+content and is **not** intended to look like secure browser UI.
+
+**Flag**: `MODAL_TYPE_CONTENT`
+
+.. figure:: modal-type-content.png
+ :width: 425px
+ :height: 230px
+ :alt: Screenshot of a content prompt.
+
+ Content alert prompt
+
+
+Disabling tab/content modal prompts
+-----------------------------------
+You can disable tab and content modal prompts and get back window-modal for
+individual prompts by setting the `prompts.modalType.<promptName>` preference to
+`3`.
diff --git a/toolkit/components/prompts/docs/nsIPromptService-reference.rst b/toolkit/components/prompts/docs/nsIPromptService-reference.rst
new file mode 100644
index 0000000000..9879cd753a
--- /dev/null
+++ b/toolkit/components/prompts/docs/nsIPromptService-reference.rst
@@ -0,0 +1,10 @@
+========================
+Prompt Service Reference
+========================
+
+This is the JSDoc from the Prompter.jsm implementation. You can find the full
+interface definition in
+`nsIPromptService.idl <https://searchfox.org/mozilla-central/source/toolkit/components/windowwatcher/nsIPromptService.idl>`_.
+
+.. js:autoclass:: Prompter
+ :members:
diff --git a/toolkit/components/prompts/docs/nsIPromptService.rst b/toolkit/components/prompts/docs/nsIPromptService.rst
new file mode 100644
index 0000000000..386572ef3f
--- /dev/null
+++ b/toolkit/components/prompts/docs/nsIPromptService.rst
@@ -0,0 +1,195 @@
+==============
+Prompt Service
+==============
+
+The `nsIPromptService` provides methods for opening various types of prompts.
+See the `interface documentation <nsIPromptService-reference.html>`_ for a list
+of prompt types.
+Every prompt method has 3 different versions:
+
+- **Prompt by window**:
+ This is considered the legacy way of prompting and only works if the window
+ you want to prompt for is in the same process.
+ Only supports window prompts.
+
+- **Prompt by browsing context (synchronous)**:
+ Use a browsing context as parent for the prompt. Works cross process from
+ parent and content process.
+
+- **Prompt by browsing context (asynchronous)**:
+ Returns a promise which resolves once the prompt closes.
+
+
+The synchronous prompt methods use call by reference (XPCOM `inout` or `out`) to
+return the updated prompt arguments to the caller.
+When prompting async the arguments are passed in by value. Prompt results are
+returned in an `nsIPropertyBag` when the Promise resolves.
+
+
+.. note::
+ If you don't provide a parent window or browsing context the prompt service
+ will fallback to a window prompt.
+ The same goes for browsing contexts of chrome windows, because there is no
+ clear association to a browser / tab.
+
+
+Examples
+--------
+
+JavaScript Sync
+~~~~~~~~~~~~~~~
+
+Here is an example of opening a confirm prompt from JavaScript. We are in the
+parent process and we want to show a tab prompt:
+
+.. code-block:: javascript
+
+ // Get the browsing context for the currently selected tab
+ let browsingContext = gBrowser.selectedBrowser.browsingContext;
+
+ // Specify prompt type, can be MODAL_TYPE_TAB, MODAL_TYPE_CONTENT,
+ // MODAL_TYPE_WINDOW
+ let modalType = Services.prompt.MODAL_TYPE_TAB;
+
+ // Object for checkbox state to pass by reference.
+ let check = { value: false };
+
+ // Prompt synchronously and store result
+ let confirmed = Services.prompt.confirmCheckBC(browsingContext, modalType,
+ "My Title", "Hello, World!", "Check this box if you agree", check);
+
+ // check.value now contains final checkbox state
+ // confirmed is a boolean which indicates whether the user pressed ok (true)
+ // or cancel (false)
+ console.debug("User checked checkbox?", check.value);
+ console.debug("User confirmed prompt?", confirmed);
+
+
+JavaScript Async
+~~~~~~~~~~~~~~~~
+
+The same prompt as above, but called async:
+
+.. code-block:: javascript
+
+ // Get the browsing context for the currently selected tab
+ let browsingContext = gBrowser.selectedBrowser.browsingContext;
+
+ // Specify prompt type, can be MODAL_TYPE_TAB, MODAL_TYPE_CONTENT,
+ // MODAL_TYPE_WINDOW
+ let modalType = Services.prompt.MODAL_TYPE_TAB;
+
+ // Note that the checkbox state variable is not an object in this case,
+ because we get the checkbox result via the result object.
+ let check = false;
+
+ // Prompt asynchronously and await result
+ let propBag = await Services.prompt.asyncConfirmCheck(browsingContext,
+ modalType,
+ "My Title",
+ "Hello, World!",
+ "Check this box if you agree",
+ check);
+
+ let ok = propBag.getProperty("ok");
+ let checked = propBag.getProperty("checked");
+
+ // ok is the boolean indicating if the user clicked "ok" (true) or
+ // "cancel" (false).
+ // checked is a boolean indicating the final checkbox state
+ console.debug("User checked checkbox?", checked);
+ console.debug("User confirmed prompt?", ok);
+
+
+C++ Sync
+~~~~~~~~
+
+.. code-block:: c++
+
+ nsCOMPtr<nsIPromptService> promptSvc =
+ do_GetService("@mozilla.org/prompter;1");
+
+ if(!promptSvc) {
+ // Error handling
+ return;
+ }
+
+ // Assuming you have the browsing context as a member.
+ // You might need to get the browsing context from somewhere else.
+ BrowsingContext* bc = mBrowsingContext;
+
+ bool ok;
+ bool checked = false;
+
+ nsresult rv = promptSvc->confirmCheck(mBrowsingContext,
+ nsIPromptService::MODAL_TYPE_TAB,
+ "My Title"_ns
+ "Hello, World!"_ns,
+ "Check this box if you agree"_ns,
+ &checked, &ok);
+
+ // ok is the boolean indicating if the user clicked "ok" (true) or
+ // "cancel" (false).
+ // checked is a boolean indicating the final checkbox state
+
+C++ Async
+~~~~~~~~~
+
+.. code-block:: c++
+
+ nsCOMPtr<nsIPromptService> promptSvc =
+ do_GetService("@mozilla.org/prompter;1");
+
+ if(!promptSvc) {
+ // Error handling
+ return;
+ }
+
+ bool checked = false;
+ Promise* promise;
+
+ // Assuming you have the browsing context as a member.
+ // You might need to get the browsing context from somewhere else.
+ BrowsingContext* bc = mBrowsingContext;
+
+ // As opposed to the sync case, here we pass the checked flag by value
+ nsresult rv = promptSvc->confirmCheckAsync(mBrowsingContext,
+ nsIPromptService::MODAL_TYPE_TAB, "My Title"_ns,
+ "Hello, World!"_ns,
+ "Check this box if you agree"_ns,
+ checked, promise);
+
+ // Attach a promise handler
+ RefPtr<PromptHandler> handler = new PromptHandler(promise);
+ promise->AppendNativeHandler(handler);
+
+Then, in your promise handler callback function:
+
+.. code-block:: c++
+
+ void PromptHandler::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue) {
+ JS::Rooted<JSObject*> detailObj(aCx, &aValue.toObject());
+
+ // Convert the JSObject back to a property bag
+ nsresult rv;
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ rv = UnwrapArg<nsIPropertyBag2>(aCx, detailObj, getter_AddRefs(propBag));
+ if (NS_FAILED(rv)) return;
+
+ bool ok;
+ bool checked;
+ propBag->GetPropertyAsBool(u"ok"_ns, &ok);
+ propBag->GetPropertyAsBool(u"checked"_ns, &checked);
+
+ // ok is the boolean indicating if the user clicked "ok" (true) or
+ // "cancel" (false).
+ // checked is a boolean indicating the final checkbox state.
+ }
+
+
+
+
+
+For a full list of prompt methods check
+`nsIPromptService reference <nsIPromptService-reference.html>`_.
diff --git a/toolkit/components/prompts/jar.mn b/toolkit/components/prompts/jar.mn
new file mode 100644
index 0000000000..819af6dc1d
--- /dev/null
+++ b/toolkit/components/prompts/jar.mn
@@ -0,0 +1,13 @@
+# 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/.
+
+toolkit.jar:
+ content/global/adjustableTitle.js (content/adjustableTitle.js)
+ content/global/commonDialog.js (content/commonDialog.js)
+ content/global/commonDialog.xhtml (content/commonDialog.xhtml)
+ content/global/commonDialog.css (content/commonDialog.css)
+ content/global/selectDialog.js (content/selectDialog.js)
+ content/global/selectDialog.xhtml (content/selectDialog.xhtml)
+ content/global/tabprompts.css (content/tabprompts.css)
+ content/global/tabprompts.sys.mjs (content/tabprompts.sys.mjs)
diff --git a/toolkit/components/prompts/moz.build b/toolkit/components/prompts/moz.build
new file mode 100644
index 0000000000..64a3115cbe
--- /dev/null
+++ b/toolkit/components/prompts/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Notifications and Alerts")
+
+DIRS += ["src"]
+
+SPHINX_TREES["prompts"] = "docs"
+
+with Files("docs/**"):
+ SCHEDULES.exclusive = ["docs"]
+
+TESTING_JS_MODULES += [
+ "test/PromptTestUtils.sys.mjs",
+]
+
+MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/toolkit/components/prompts/src/CommonDialog.sys.mjs b/toolkit/components/prompts/src/CommonDialog.sys.mjs
new file mode 100644
index 0000000000..a0812aa8ec
--- /dev/null
+++ b/toolkit/components/prompts/src/CommonDialog.sys.mjs
@@ -0,0 +1,381 @@
+/* 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, {
+ EnableDelayHelper: "resource://gre/modules/PromptUtils.sys.mjs",
+});
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+export function CommonDialog(args, ui) {
+ this.args = args;
+ this.ui = ui;
+ this.initialFocusPromise = new Promise(resolve => {
+ this.initialFocusResolver = resolve;
+ });
+}
+
+CommonDialog.prototype = {
+ args: null,
+ ui: null,
+
+ hasInputField: true,
+ numButtons: undefined,
+ iconClass: undefined,
+ soundID: undefined,
+ focusTimer: null,
+ initialFocusPromise: null,
+ initialFocusResolver: null,
+
+ /**
+ * @param [commonDialogEl] - Dialog element from commonDialog.xhtml,
+ * null for TabModalPrompts.
+ */
+ async onLoad(commonDialogEl = null) {
+ let isEmbedded = !!commonDialogEl?.ownerGlobal.docShell.chromeEventHandler;
+
+ switch (this.args.promptType) {
+ case "alert":
+ case "alertCheck":
+ this.hasInputField = false;
+ this.numButtons = 1;
+ this.iconClass = ["alert-icon"];
+ this.soundID = Ci.nsISound.EVENT_ALERT_DIALOG_OPEN;
+ break;
+ case "confirmCheck":
+ case "confirm":
+ this.hasInputField = false;
+ this.numButtons = 2;
+ this.iconClass = ["question-icon"];
+ this.soundID = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
+ break;
+ case "confirmEx":
+ var numButtons = 0;
+ if (this.args.button0Label) {
+ numButtons++;
+ }
+ if (this.args.button1Label) {
+ numButtons++;
+ }
+ if (this.args.button2Label) {
+ numButtons++;
+ }
+ if (this.args.button3Label) {
+ numButtons++;
+ }
+ if (numButtons == 0) {
+ throw new Error("A dialog with no buttons? Can not haz.");
+ }
+ this.numButtons = numButtons;
+ this.hasInputField = false;
+ this.iconClass = ["question-icon"];
+ this.soundID = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
+ break;
+ case "prompt":
+ this.numButtons = 2;
+ this.iconClass = ["question-icon"];
+ this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
+ this.initTextbox("login", this.args.value);
+ // Clear the label, since this isn't really a username prompt.
+ this.ui.loginLabel.setAttribute("value", "");
+ // Ensure the labeling for the prompt is correct.
+ this.ui.loginTextbox.setAttribute("aria-labelledby", "infoBody");
+ break;
+ case "promptUserAndPass":
+ this.numButtons = 2;
+ this.iconClass = ["authentication-icon", "question-icon"];
+ this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
+ this.initTextbox("login", this.args.user);
+ this.initTextbox("password1", this.args.pass);
+ break;
+ case "promptPassword":
+ this.numButtons = 2;
+ this.iconClass = ["authentication-icon", "question-icon"];
+ this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
+ this.initTextbox("password1", this.args.pass);
+ // Clear the label, since the message presumably indicates its purpose.
+ this.ui.password1Label.setAttribute("value", "");
+ break;
+ default:
+ console.error(
+ "commonDialog opened for unknown type: ",
+ this.args.promptType
+ );
+ throw new Error("unknown dialog type");
+ }
+
+ if (commonDialogEl) {
+ commonDialogEl.setAttribute(
+ "windowtype",
+ "prompt:" + this.args.promptType
+ );
+ }
+
+ // set the document title
+ let title = this.args.title;
+ let infoTitle = this.ui.infoTitle;
+ infoTitle.appendChild(infoTitle.ownerDocument.createTextNode(title));
+
+ // Specific check to prevent showing the title on the old content prompts for macOS.
+ // This should be removed when the old content prompts are removed.
+ let contentSubDialogPromptEnabled = Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog"
+ );
+ let isOldContentPrompt =
+ !contentSubDialogPromptEnabled &&
+ this.args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT;
+
+ // After making these preventative checks, we can determine to show it if we're on
+ // macOS (where there is no titlebar) or if the prompt is a common dialog document
+ // and has been embedded (has a chromeEventHandler).
+ infoTitle.hidden =
+ isOldContentPrompt || !(AppConstants.platform === "macosx" || isEmbedded);
+
+ if (commonDialogEl) {
+ commonDialogEl.ownerDocument.title = title;
+ }
+
+ // Set button labels and visibility
+ //
+ // This assumes that button0 defaults to a visible "ok" button, and
+ // button1 defaults to a visible "cancel" button. The other 2 buttons
+ // have no default labels (and are hidden).
+ switch (this.numButtons) {
+ case 4:
+ this.setLabelForNode(this.ui.button3, this.args.button3Label);
+ this.ui.button3.hidden = false;
+ // fall through
+ case 3:
+ this.setLabelForNode(this.ui.button2, this.args.button2Label);
+ this.ui.button2.hidden = false;
+ // fall through
+ case 2:
+ // Defaults to a visible "cancel" button
+ if (this.args.button1Label) {
+ this.setLabelForNode(this.ui.button1, this.args.button1Label);
+ }
+ break;
+
+ case 1:
+ this.ui.button1.hidden = true;
+ break;
+ }
+ // Defaults to a visible "ok" button
+ if (this.args.button0Label) {
+ this.setLabelForNode(this.ui.button0, this.args.button0Label);
+ }
+
+ // display the main text
+ let croppedMessage = "";
+ if (this.args.text) {
+ // Bug 317334 - crop string length as a workaround.
+ croppedMessage = this.args.text.substr(0, 10000);
+ // TabModalPrompts don't have an infoRow to hide / not hide here, so
+ // guard on that here so long as they are in use.
+ if (this.ui.infoRow) {
+ this.ui.infoRow.hidden = false;
+ }
+ }
+ let infoBody = this.ui.infoBody;
+ infoBody.appendChild(infoBody.ownerDocument.createTextNode(croppedMessage));
+
+ let label = this.args.checkLabel;
+ if (label) {
+ // Only show the checkbox if label has a value.
+ this.ui.checkboxContainer.hidden = false;
+ this.ui.checkboxContainer.clientTop; // style flush to assure binding is attached
+ this.setLabelForNode(this.ui.checkbox, label);
+ this.ui.checkbox.checked = this.args.checked;
+ }
+
+ // set the icon
+ let icon = this.ui.infoIcon;
+ if (icon) {
+ this.iconClass.forEach((el, idx, arr) => icon.classList.add(el));
+ }
+
+ // set default result to cancelled
+ this.args.ok = false;
+ this.args.buttonNumClicked = 1;
+
+ // Set the default button
+ let b = this.args.defaultButtonNum || 0;
+ let button = this.ui["button" + b];
+
+ if (commonDialogEl) {
+ commonDialogEl.defaultButton = ["accept", "cancel", "extra1", "extra2"][
+ b
+ ];
+ } else {
+ button.setAttribute("default", "true");
+ }
+
+ if (!isEmbedded && !this.ui.promptContainer?.hidden) {
+ // Set default focus and select textbox contents if applicable. If we're
+ // embedded SubDialogManager will call setDefaultFocus for us.
+ this.setDefaultFocus(true);
+ }
+
+ if (this.args.enableDelay) {
+ this.delayHelper = new lazy.EnableDelayHelper({
+ disableDialog: () => this.setButtonsEnabledState(false),
+ enableDialog: () => this.setButtonsEnabledState(true),
+ focusTarget: this.ui.focusTarget,
+ });
+ }
+
+ // Play a sound (unless we're showing a content prompt -- don't want those
+ // to feel like OS prompts).
+ try {
+ if (commonDialogEl && this.soundID && !this.args.openedWithTabDialog) {
+ Cc["@mozilla.org/sound;1"]
+ .getService(Ci.nsISound)
+ .playEventSound(this.soundID);
+ }
+ } catch (e) {
+ console.error("Couldn't play common dialog event sound: ", e);
+ }
+
+ if (commonDialogEl) {
+ if (isEmbedded) {
+ // If we delayed default focus above, wait for it to be ready before
+ // sending the notification.
+ await this.initialFocusPromise;
+ }
+ Services.obs.notifyObservers(this.ui.prompt, "common-dialog-loaded");
+ } else {
+ // ui.promptContainer is the <tabmodalprompt> element.
+ Services.obs.notifyObservers(
+ this.ui.promptContainer,
+ "tabmodal-dialog-loaded"
+ );
+ }
+ },
+
+ setLabelForNode(aNode, aLabel) {
+ // This is for labels which may contain embedded access keys.
+ // If we end in (&X) where X represents the access key, optionally preceded
+ // by spaces and/or followed by the ':' character, store the access key and
+ // remove the access key placeholder + leading spaces from the label.
+ // Otherwise a character preceded by one but not two &s is the access key.
+ // Store it and remove the &.
+
+ // Note that if you change the following code, see the comment of
+ // nsTextBoxFrame::UpdateAccessTitle.
+ var accessKey = null;
+ if (/ *\(\&([^&])\)(:?)$/.test(aLabel)) {
+ aLabel = RegExp.leftContext + RegExp.$2;
+ accessKey = RegExp.$1;
+ } else if (/^([^&]*)\&(([^&]).*$)/.test(aLabel)) {
+ aLabel = RegExp.$1 + RegExp.$2;
+ accessKey = RegExp.$3;
+ }
+
+ // && is the magic sequence to embed an & in your label.
+ aLabel = aLabel.replace(/\&\&/g, "&");
+ aNode.label = aLabel;
+
+ // XXXjag bug 325251
+ // Need to set this after aNode.setAttribute("value", aLabel);
+ if (accessKey) {
+ aNode.accessKey = accessKey;
+ }
+ },
+
+ initTextbox(aName, aValue) {
+ this.ui[aName + "Container"].hidden = false;
+ this.ui[aName + "Textbox"].setAttribute(
+ "value",
+ aValue !== null ? aValue : ""
+ );
+ },
+
+ setButtonsEnabledState(enabled) {
+ this.ui.button0.disabled = !enabled;
+ // button1 (cancel) remains enabled.
+ this.ui.button2.disabled = !enabled;
+ this.ui.button3.disabled = !enabled;
+ },
+
+ setDefaultFocus(isInitialLoad) {
+ let b = this.args.defaultButtonNum || 0;
+ let button = this.ui["button" + b];
+
+ if (!this.hasInputField) {
+ let isOSX = "nsILocalFileMac" in Ci;
+ // If the infoRow exists and is is hidden, then the infoBody is also hidden,
+ // which means it can't be focused. At that point, we fall back to focusing
+ // the default button, regardless of platform.
+ if (isOSX && !(this.ui.infoRow && this.ui.infoRow.hidden)) {
+ this.ui.infoBody.focus();
+ } else {
+ button.focus({ focusVisible: false });
+ }
+ } else if (this.args.promptType == "promptPassword") {
+ // When the prompt is initialized, focus and select the textbox
+ // contents. Afterwards, only focus the textbox.
+ if (isInitialLoad) {
+ this.ui.password1Textbox.select();
+ } else {
+ this.ui.password1Textbox.focus();
+ }
+ } else if (isInitialLoad) {
+ this.ui.loginTextbox.select();
+ } else {
+ this.ui.loginTextbox.focus();
+ }
+
+ if (isInitialLoad) {
+ this.initialFocusResolver();
+ }
+ },
+
+ onCheckbox() {
+ this.args.checked = this.ui.checkbox.checked;
+ },
+
+ onButton0() {
+ this.args.promptActive = false;
+ this.args.ok = true;
+ this.args.buttonNumClicked = 0;
+
+ let username = this.ui.loginTextbox.value;
+ let password = this.ui.password1Textbox.value;
+
+ // Return textfield values
+ switch (this.args.promptType) {
+ case "prompt":
+ this.args.value = username;
+ break;
+ case "promptUserAndPass":
+ this.args.user = username;
+ this.args.pass = password;
+ break;
+ case "promptPassword":
+ this.args.pass = password;
+ break;
+ }
+ },
+
+ onButton1() {
+ this.args.promptActive = false;
+ this.args.buttonNumClicked = 1;
+ },
+
+ onButton2() {
+ this.args.promptActive = false;
+ this.args.buttonNumClicked = 2;
+ },
+
+ onButton3() {
+ this.args.promptActive = false;
+ this.args.buttonNumClicked = 3;
+ },
+
+ abortPrompt() {
+ this.args.promptActive = false;
+ this.args.promptAborted = true;
+ },
+};
diff --git a/toolkit/components/prompts/src/PromptUtils.sys.mjs b/toolkit/components/prompts/src/PromptUtils.sys.mjs
new file mode 100644
index 0000000000..7a15802b84
--- /dev/null
+++ b/toolkit/components/prompts/src/PromptUtils.sys.mjs
@@ -0,0 +1,186 @@
+/* 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/. */
+
+export var PromptUtils = {
+ // Fire a dialog open/close event. Used by tabbrowser to focus the
+ // tab which is triggering a prompt.
+ // For remote dialogs, we pass in a different DOM window and a separate
+ // target. If the caller doesn't pass in the target, then we'll simply use
+ // the passed-in DOM window.
+ // The detail may contain information about the principal on which the
+ // prompt is triggered, as well as whether or not this is a tabprompt
+ // (ie tabmodal alert/prompt/confirm and friends)
+ fireDialogEvent(domWin, eventName, maybeTarget, detail) {
+ let target = maybeTarget || domWin;
+ let eventOptions = { cancelable: true, bubbles: true };
+ if (detail) {
+ eventOptions.detail = detail;
+ }
+ let event = new domWin.CustomEvent(eventName, eventOptions);
+ let winUtils = domWin.windowUtils;
+ winUtils.dispatchEventToChromeOnly(target, event);
+ },
+
+ objectToPropBag(obj) {
+ let bag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag2
+ );
+ bag.QueryInterface(Ci.nsIWritablePropertyBag);
+
+ for (let propName in obj) {
+ bag.setProperty(propName, obj[propName]);
+ }
+
+ return bag;
+ },
+
+ propBagToObject(propBag, obj) {
+ // Here we iterate over the object's original properties, not the bag
+ // (ie, the prompt can't return more/different properties than were
+ // passed in). This just helps ensure that the caller provides default
+ // values, lest the prompt forget to set them.
+ for (let propName in obj) {
+ obj[propName] = propBag.getProperty(propName);
+ }
+ },
+};
+
+/**
+ * This helper handles the enabling/disabling of dialogs that might
+ * be subject to fast-clicking attacks. It handles the initial delayed
+ * enabling of the dialog, as well as disabling it on blur and reapplying
+ * the delay when the dialog regains focus.
+ *
+ * @param enableDialog A custom function to be called when the dialog
+ * is to be enabled.
+ * @param diableDialog A custom function to be called when the dialog
+ * is to be disabled.
+ * @param focusTarget The window used to watch focus/blur events.
+ */
+export var EnableDelayHelper = function ({
+ enableDialog,
+ disableDialog,
+ focusTarget,
+}) {
+ this.enableDialog = makeSafe(enableDialog);
+ this.disableDialog = makeSafe(disableDialog);
+ this.focusTarget = focusTarget;
+
+ this.disableDialog();
+
+ this.focusTarget.addEventListener("blur", this);
+ this.focusTarget.addEventListener("focus", this);
+ // While the user key-repeats, we want to renew the timer until keyup:
+ this.focusTarget.addEventListener("keyup", this, true);
+ this.focusTarget.addEventListener("keydown", this, true);
+ this.focusTarget.document.addEventListener("unload", this);
+
+ this.startOnFocusDelay();
+};
+
+EnableDelayHelper.prototype = {
+ get delayTime() {
+ return Services.prefs.getIntPref("security.dialog_enable_delay");
+ },
+
+ handleEvent(event) {
+ if (
+ !event.type.startsWith("key") &&
+ event.target != this.focusTarget &&
+ event.target != this.focusTarget.document
+ ) {
+ return;
+ }
+
+ switch (event.type) {
+ case "keyup":
+ // As soon as any key goes up, we can stop treating keypresses
+ // as indicative of key-repeating that should prolong the timer.
+ this.focusTarget.removeEventListener("keyup", this, true);
+ this.focusTarget.removeEventListener("keydown", this, true);
+ break;
+
+ case "keydown":
+ // Renew timer for repeating keydowns:
+ if (this._focusTimer) {
+ this._focusTimer.cancel();
+ this._focusTimer = null;
+ this.startOnFocusDelay();
+ event.preventDefault();
+ }
+ break;
+
+ case "blur":
+ this.onBlur();
+ break;
+
+ case "focus":
+ this.onFocus();
+ break;
+
+ case "unload":
+ this.onUnload();
+ break;
+ }
+ },
+
+ onBlur() {
+ this.disableDialog();
+ // If we blur while waiting to enable the buttons, just cancel the
+ // timer to ensure the delay doesn't fire while not focused.
+ if (this._focusTimer) {
+ this._focusTimer.cancel();
+ this._focusTimer = null;
+ }
+ },
+
+ onFocus() {
+ this.startOnFocusDelay();
+ },
+
+ onUnload() {
+ this.focusTarget.removeEventListener("blur", this);
+ this.focusTarget.removeEventListener("focus", this);
+ this.focusTarget.removeEventListener("keyup", this, true);
+ this.focusTarget.removeEventListener("keydown", this, true);
+ this.focusTarget.document.removeEventListener("unload", this);
+
+ if (this._focusTimer) {
+ this._focusTimer.cancel();
+ this._focusTimer = null;
+ }
+
+ this.focusTarget = this.enableDialog = this.disableDialog = null;
+ },
+
+ startOnFocusDelay() {
+ if (this._focusTimer) {
+ return;
+ }
+
+ this._focusTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._focusTimer.initWithCallback(
+ () => {
+ this.onFocusTimeout();
+ },
+ this.delayTime,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ },
+
+ onFocusTimeout() {
+ this._focusTimer = null;
+ this.enableDialog();
+ },
+};
+
+function makeSafe(fn) {
+ return function () {
+ // The dialog could be gone by now (if the user closed it),
+ // which makes it likely that the given fn might throw.
+ try {
+ fn();
+ } catch (e) {}
+ };
+}
diff --git a/toolkit/components/prompts/src/Prompter.sys.mjs b/toolkit/components/prompts/src/Prompter.sys.mjs
new file mode 100644
index 0000000000..fe44992378
--- /dev/null
+++ b/toolkit/components/prompts/src/Prompter.sys.mjs
@@ -0,0 +1,1824 @@
+/* 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";
+
+// This is redefined below, for strange and unfortunate reasons.
+import { PromptUtils } from "resource://gre/modules/PromptUtils.sys.mjs";
+
+const {
+ MODAL_TYPE_TAB,
+ MODAL_TYPE_CONTENT,
+ MODAL_TYPE_WINDOW,
+ MODAL_TYPE_INTERNAL_WINDOW,
+} = Ci.nsIPrompt;
+
+const COMMON_DIALOG = "chrome://global/content/commonDialog.xhtml";
+const SELECT_DIALOG = "chrome://global/content/selectDialog.xhtml";
+
+export function Prompter() {
+ // Note that EmbedPrompter clones this implementation.
+}
+
+/**
+ * Implements nsIPromptService and nsIPromptFactory
+ * @class Prompter
+ */
+Prompter.prototype = {
+ classID: Components.ID("{1c978d25-b37f-43a8-a2d6-0c7a239ead87}"),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPromptFactory",
+ "nsIPromptService",
+ ]),
+
+ /* ---------- private members ---------- */
+
+ pickPrompter(options) {
+ return new ModalPrompter(options);
+ },
+
+ /* ---------- nsIPromptFactory ---------- */
+
+ getPrompt(domWin, iid) {
+ // This is still kind of dumb; the C++ code delegated to login manager
+ // here, which in turn calls back into us via nsIPromptService.
+ if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPrompt)) {
+ try {
+ let pwmgr = Cc[
+ "@mozilla.org/passwordmanager/authpromptfactory;1"
+ ].getService(Ci.nsIPromptFactory);
+ return pwmgr.getPrompt(domWin, iid);
+ } catch (e) {
+ console.error("nsPrompter: Delegation to password manager failed: ", e);
+ }
+ }
+
+ let p = new ModalPrompter({ domWin });
+ p.QueryInterface(iid);
+ return p;
+ },
+
+ /* ---------- nsIPromptService ---------- */
+
+ /**
+ * Puts up an alert dialog with an OK button.
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ */
+ alert(domWin, title, text) {
+ let p = this.pickPrompter({ domWin });
+ p.alert(title, text);
+ },
+
+ /**
+ * Puts up an alert dialog with an OK button.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ */
+ alertBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ p.alert(...promptArgs);
+ },
+
+ /**
+ * Puts up an alert dialog with an OK button.
+ *
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @returns {Promise} A promise which resolves when the prompt is dismissed.
+ */
+ asyncAlert(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.alert(...promptArgs);
+ },
+
+ /**
+ * Puts up an alert dialog with an OK button and a labeled checkbox.
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * @param {Object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state
+ * after this method returns.
+ */
+ alertCheck(domWin, title, text, checkLabel, checkValue) {
+ let p = this.pickPrompter({ domWin });
+ p.alertCheck(title, text, checkLabel, checkValue);
+ },
+
+ /**
+ * Puts up an alert dialog with an OK button and a labeled checkbox.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * @param {Object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state
+ * after this method returns.
+ */
+ alertCheckBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ p.alertCheck(...promptArgs);
+ },
+
+ /**
+ * Puts up an alert dialog with an OK button and a labeled checkbox.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * @param {Boolean} checkValue - The initial checked state of the checkbox.
+ * @returns {Promise<nsIPropertyBag<{ checked: Boolean }>>}
+ * A promise which resolves when the prompt is dismissed.
+ */
+ asyncAlertCheck(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.alertCheck(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with OK and Cancel buttons.
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ confirm(domWin, title, text) {
+ let p = this.pickPrompter({ domWin });
+ return p.confirm(title, text);
+ },
+
+ /**
+ * Puts up a dialog with OK and Cancel buttons.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ confirmBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ return p.confirm(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with OK and Cancel buttons.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @returns {Promise<nsIPropertyBag<{ ok: Boolean }>>}
+ * A promise which resolves when the prompt is dismissed.
+ */
+ asyncConfirm(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.confirm(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with OK and Cancel buttons and a labeled checkbox.
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * @param {Object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state
+ * after this method returns.
+ */
+ confirmCheck(domWin, title, text, checkLabel, checkValue) {
+ let p = this.pickPrompter({ domWin });
+ return p.confirmCheck(title, text, checkLabel, checkValue);
+ },
+
+ /**
+ * Puts up a dialog with OK and Cancel buttons and a labeled checkbox.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * @param {Object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state
+ * after this method returns.
+ * @returns {Boolean} true for OK, false for Cancel
+ */
+ confirmCheckBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ return p.confirmCheck(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with OK and Cancel buttons and a labeled checkbox.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * @param {Boolean} checkValue - The initial checked state of the checkbox.
+ * @returns {Promise<nsIPropertyBag<{ ok: Boolean, checked: Boolean }>>}
+ * A promise which resolves when the prompt is dismissed.
+ */
+ asyncConfirmCheck(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.confirmCheck(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with up to 3 buttons and an optional, labeled checkbox.
+ *
+ * Buttons are numbered 0 - 2. Button 0 is the default button unless one of
+ * the Button Default Flags is specified.
+ *
+ * A button may use a predefined title, specified by one of the Button Title
+ * Flags values. Each title value can be multiplied by a position value to
+ * assign the title to a particular button. If BUTTON_TITLE_IS_STRING is
+ * used for a button, the string parameter for that button will be used. If
+ * the value for a button position is zero, the button will not be shown.
+ *
+ * In general, flags is constructed per the following example:
+ *
+ * flags = (BUTTON_POS_0) * (BUTTON_TITLE_AAA) +
+ * (BUTTON_POS_1) * (BUTTON_TITLE_BBB) +
+ * BUTTON_POS_1_DEFAULT;
+ *
+ * where "AAA" and "BBB" correspond to one of the button titles.
+ *
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {Number} flags - A combination of Button Flags.
+ * @param {String} button0 - Used when button 0 uses TITLE_IS_STRING.
+ * @param {String} button1 - Used when button 1 uses TITLE_IS_STRING.
+ * @param {String} button2 - Used when button 2 uses TITLE_IS_STRING.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * Null if no checkbox.
+ * @param {Object} checkValue - Contains the initial checked state of the
+ * checkbox when this method
+ * is called and the final checked state after this method returns.
+ * @returns {Number} The index of the button pressed.
+ */
+ confirmEx(
+ domWin,
+ title,
+ text,
+ flags,
+ button0,
+ button1,
+ button2,
+ checkLabel,
+ checkValue
+ ) {
+ let p = this.pickPrompter({ domWin });
+ return p.confirmEx(
+ title,
+ text,
+ flags,
+ button0,
+ button1,
+ button2,
+ checkLabel,
+ checkValue
+ );
+ },
+
+ /**
+ * Puts up a dialog with up to 3 buttons and an optional, labeled checkbox.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {Number} flags - A combination of Button Flags.
+ * @param {String} button0 - Used when button 0 uses TITLE_IS_STRING.
+ * @param {String} button1 - Used when button 1 uses TITLE_IS_STRING.
+ * @param {String} button2 - Used when button 2 uses TITLE_IS_STRING.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * Null if no checkbox.
+ * @param {Object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state
+ * after this method returns.
+ * @returns {Number} The index of the button pressed.
+ */
+ confirmExBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ return p.confirmEx(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with up to 3 buttons and an optional, labeled checkbox.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {Number} flags - A combination of Button Flags.
+ * @param {String} button0 - Used when button 0 uses TITLE_IS_STRING.
+ * @param {String} button1 - Used when button 1 uses TITLE_IS_STRING.
+ * @param {String} button2 - Used when button 2 uses TITLE_IS_STRING.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * Null if no checkbox.
+ * @param {Boolean} checkValue - The initial checked state of the checkbox.
+ * @param {Object} [extraArgs] - Extra arguments for the prompt metadata.
+ * @returns {Promise<nsIPropertyBag<{ buttonNumClicked: Number, checked: Boolean }>>}
+ */
+ asyncConfirmEx(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.confirmEx(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with an edit field and an optional, labeled checkbox.
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {Object} value - Contains the default value for the dialog field
+ * when this method is called (null value is ok). Upon return, if
+ * the user pressed OK, then this parameter contains a newly
+ * allocated string value.
+ * Otherwise, the parameter's value is unmodified.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * If null, check box will not be shown.
+ * @param {Object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state
+ * after this method returns.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ prompt(domWin, title, text, value, checkLabel, checkValue) {
+ let p = this.pickPrompter({ domWin });
+ return p.nsIPrompt_prompt(title, text, value, checkLabel, checkValue);
+ },
+
+ /**
+ * Puts up a dialog with an edit field and an optional, labeled checkbox.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {Object} value - Contains the default value for the dialog field
+ * when this method is called (null value is ok). Upon return, if
+ * the user pressed OK, then this parameter contains a newly
+ * allocated string value.
+ * Otherwise, the parameter's value is unmodified.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * If null, check box will not be shown.
+ * @param {Object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state
+ * after this method returns.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ promptBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ return p.nsIPrompt_prompt(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with an edit field and an optional, labeled checkbox.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String} value - The default value for the dialog text field.
+ * @param {String} checkLabel - Text to appear with the checkbox.
+ * If null, check box will not be shown.
+ * @param {Boolean} checkValue - The initial checked state of the checkbox.
+ * @returns {Promise<nsIPropertyBag<{ ok: Boolean, checked: Boolean, value: String }>>}
+ * A promise which resolves when the prompt is dismissed.
+ */
+ asyncPrompt(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.nsIPrompt_prompt(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with an edit field and a password field.
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {Object} user - Contains the default value for the username
+ * field when this method is called (null value is ok).
+ * Upon return, if the user pressed OK, then this parameter contains
+ * a newly allocated string value. Otherwise, the parameter's value
+ * is unmodified.
+ * @param {Object} pass - Contains the default value for the password field
+ * when this method is called (null value is ok). Upon return, if the
+ * user pressed OK, this parameter contains a newly allocated string
+ * value. Otherwise, the parameter's value is unmodified.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ promptUsernameAndPassword(domWin, title, text, user, pass) {
+ let p = this.pickPrompter({ domWin });
+ return p.nsIPrompt_promptUsernameAndPassword(null, title, text, user, pass);
+ },
+
+ /**
+ * Puts up a dialog with an edit field and a password field.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {Object} user - Contains the default value for the username
+ * field when this method is called (null value is ok).
+ * Upon return, if the user pressed OK, then this parameter contains
+ * a newly allocated string value. Otherwise, the parameter's value
+ * is unmodified.
+ * @param {Object} pass - Contains the default value for the password field
+ * when this method is called (null value is ok). Upon return, if the
+ * user pressed OK, this parameter contains a newly allocated string
+ * value. Otherwise, the parameter's value is unmodified.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ promptUsernameAndPasswordBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ return p.nsIPrompt_promptUsernameAndPassword(null, ...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with an edit field and a password field.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String} user - Default value for the username field.
+ * @param {String} pass - Contains the default value for the password field.
+ * @returns {Promise<nsIPropertyBag<{ ok: Boolean, user: String, pass: String }>>}
+ * A promise which resolves when the prompt is dismissed.
+ */
+ asyncPromptUsernameAndPassword(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.nsIPrompt_promptUsernameAndPassword(null, ...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with a password field.
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {Object} pass - Contains the default value for the password field
+ * when this method is called (null value is ok). Upon return, if the
+ * user pressed OK, this parameter contains a newly allocated string
+ * value. Otherwise, the parameter's value is unmodified.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ promptPassword(domWin, title, text, pass) {
+ let p = this.pickPrompter({ domWin });
+ return p.nsIPrompt_promptPassword(
+ null, // no channel.
+ title,
+ text,
+ pass
+ );
+ },
+
+ /**
+ * Puts up a dialog with a password field.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {Object} pass - Contains the default value for the password field
+ * when this method is called (null value is ok). Upon return, if the
+ * user pressed OK, this parameter contains a newly allocated string
+ * value. Otherwise, the parameter's value is unmodified.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ promptPasswordBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ return p.nsIPrompt_promptPassword(null, ...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog with a password field.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String} pass - Contains the default value for the password field.
+ * @returns {Promise<nsIPropertyBag<{ ok: Boolean, pass: String }>>}
+ * A promise which resolves when the prompt is dismissed.
+ */
+ asyncPromptPassword(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.nsIPrompt_promptPassword(null, ...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog box which has a list box of strings from which the user
+ * may make a single selection.
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String[]} list - The list of strings to display.
+ * @param {Object} selected - Contains the index of the selected item in the
+ * list when this method returns true.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ select(domWin, title, text, list, selected) {
+ let p = this.pickPrompter({ domWin });
+ return p.select(title, text, list, selected);
+ },
+
+ /**
+ * Puts up a dialog box which has a list box of strings from which the user
+ * may make a single selection.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String[]} list - The list of strings to display.
+ * @param {Object} selected - Contains the index of the selected item in the
+ * list when this method returns true.
+ * @returns {Boolean} true for OK, false for Cancel.
+ */
+ selectBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ return p.select(...promptArgs);
+ },
+
+ /**
+ * Puts up a dialog box which has a list box of strings from which the user
+ * may make a single selection.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} title - Text to appear in the title of the dialog.
+ * @param {String} text - Text to appear in the body of the dialog.
+ * @param {String[]} list - The list of strings to display.
+ * @returns {Promise<nsIPropertyBag<{ selected: Number, ok: Boolean }>>}
+ * A promise which resolves when the prompt is dismissed.
+ */
+ asyncSelect(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.select(...promptArgs);
+ },
+
+ /**
+ * Requests a username and a password. Shows a dialog with username and
+ * password field, depending on flags also a domain field.
+ * @param {mozIDOMWindowProxy} domWin - The parent window or null.
+ * @param {nsIChannel} channel - The channel that requires authentication.
+ * @param {Number} level - Security level of the credential transmission.
+ * Any of nsIAuthPrompt2.<LEVEL_NONE|LEVEL_PW_ENCRYPTED|LEVEL_SECURE>
+ * @param {nsIAuthInformation} authInfo - Authentication information object.
+ * @returns {Boolean}
+ * true: Authentication can proceed using the values
+ * in the authInfo object.
+ * false: Authentication should be cancelled, usually because the
+ * user did not provide username/password.
+ */
+ promptAuth(domWin, channel, level, authInfo) {
+ let p = this.pickPrompter({ domWin });
+ return p.promptAuth(channel, level, authInfo);
+ },
+
+ /**
+ * Requests a username and a password. Shows a dialog with username and
+ * password field, depending on flags also a domain field.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {nsIChannel} channel - The channel that requires authentication.
+ * @param {Number} level - Security level of the credential transmission.
+ * Any of nsIAuthPrompt2.<LEVEL_NONE|LEVEL_PW_ENCRYPTED|LEVEL_SECURE>
+ * @param {nsIAuthInformation} authInfo - Authentication information object.
+ * @returns {Boolean}
+ * true: Authentication can proceed using the values
+ * in the authInfo object.
+ * false: Authentication should be cancelled, usually because the
+ * user did not provide username/password.
+ */
+ promptAuthBC(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType });
+ return p.promptAuth(...promptArgs);
+ },
+
+ /**
+ * Requests a username and a password. Shows a dialog with username and
+ * password field, depending on flags also a domain field.
+ * @param {BrowsingContext} browsingContext - The browsing context the
+ * prompt should be opened for.
+ * @param {Number} modalType - The modal type of the prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {nsIChannel} channel - The channel that requires authentication.
+ * @param {Number} level - Security level of the credential transmission.
+ * Any of nsIAuthPrompt2.<LEVEL_NONE|LEVEL_PW_ENCRYPTED|LEVEL_SECURE>
+ * @param {nsIAuthInformation} authInfo - Authentication information object.
+ * @returns {Promise<nsIPropertyBag<{ ok: Boolean }>>}
+ * A promise which resolves when the prompt is dismissed.
+ */
+ asyncPromptAuth(browsingContext, modalType, ...promptArgs) {
+ let p = this.pickPrompter({ browsingContext, modalType, async: true });
+ return p.promptAuth(...promptArgs);
+ },
+};
+
+// Common utils not specific to a particular prompter style.
+var InternalPromptUtils = {
+ getLocalizedString(key, formatArgs) {
+ if (formatArgs) {
+ return this.strBundle.formatStringFromName(key, formatArgs);
+ }
+ return this.strBundle.GetStringFromName(key);
+ },
+
+ confirmExHelper(flags, button0, button1, button2) {
+ const BUTTON_DEFAULT_MASK = 0x03000000;
+ let defaultButtonNum = (flags & BUTTON_DEFAULT_MASK) >> 24;
+ let isDelayEnabled = flags & Ci.nsIPrompt.BUTTON_DELAY_ENABLE;
+
+ // Flags can be used to select a specific pre-defined button label or
+ // a caller-supplied string (button0/button1/button2). If no flags are
+ // set for a button, then the button won't be shown.
+ let argText = [button0, button1, button2];
+ let buttonLabels = [null, null, null];
+ for (let i = 0; i < 3; i++) {
+ let buttonLabel;
+ switch (flags & 0xff) {
+ case Ci.nsIPrompt.BUTTON_TITLE_OK:
+ buttonLabel = this.getLocalizedString("OK");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
+ buttonLabel = this.getLocalizedString("Cancel");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_YES:
+ buttonLabel = this.getLocalizedString("Yes");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_NO:
+ buttonLabel = this.getLocalizedString("No");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
+ buttonLabel = this.getLocalizedString("Save");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
+ buttonLabel = this.getLocalizedString("DontSave");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
+ buttonLabel = this.getLocalizedString("Revert");
+ break;
+ case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
+ buttonLabel = argText[i];
+ break;
+ }
+ if (buttonLabel) {
+ buttonLabels[i] = buttonLabel;
+ }
+ flags >>= 8;
+ }
+
+ return [
+ buttonLabels[0],
+ buttonLabels[1],
+ buttonLabels[2],
+ defaultButtonNum,
+ isDelayEnabled,
+ ];
+ },
+
+ getAuthInfo(authInfo) {
+ let username, password;
+
+ let flags = authInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && authInfo.domain) {
+ username = authInfo.domain + "\\" + authInfo.username;
+ } else {
+ username = authInfo.username;
+ }
+
+ password = authInfo.password;
+
+ return [username, password];
+ },
+
+ setAuthInfo(authInfo, username, password) {
+ let flags = authInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
+ // Domain is separated from username by a backslash
+ let idx = username.indexOf("\\");
+ if (idx == -1) {
+ authInfo.username = username;
+ } else {
+ authInfo.domain = username.substring(0, idx);
+ authInfo.username = username.substring(idx + 1);
+ }
+ } else {
+ authInfo.username = username;
+ }
+ authInfo.password = password;
+ },
+
+ /**
+ * Strip out things like userPass and path for display.
+ */
+ getFormattedHostname(uri) {
+ return uri.scheme + "://" + uri.hostPort;
+ },
+
+ // Note: there's a similar implementation in the login manager.
+ getAuthTarget(aChannel, aAuthInfo) {
+ let displayHost, realm;
+
+ // If our proxy is demanding authentication, don't use the
+ // channel's actual destination.
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
+ if (!(aChannel instanceof Ci.nsIProxiedChannel)) {
+ throw new Error("proxy auth needs nsIProxiedChannel");
+ }
+
+ let info = aChannel.proxyInfo;
+ if (!info) {
+ throw new Error("proxy auth needs nsIProxyInfo");
+ }
+
+ // Proxies don't have a scheme, but we'll use "moz-proxy://"
+ // so that it's more obvious what the login is for.
+ let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+ displayHost =
+ "moz-proxy://" +
+ idnService.convertUTF8toACE(info.host) +
+ ":" +
+ info.port;
+ realm = aAuthInfo.realm;
+ if (!realm) {
+ realm = displayHost;
+ }
+
+ return { realm, displayHost };
+ }
+
+ displayHost = this.getFormattedHostname(aChannel.URI);
+ let displayHostOnly = aChannel.URI.hostPort;
+
+ // If a HTTP WWW-Authenticate header specified a realm, that value
+ // will be available here. If it wasn't set or wasn't HTTP, we'll use
+ // the formatted hostname instead.
+ realm = aAuthInfo.realm;
+ if (!realm) {
+ realm = displayHost;
+ }
+
+ return { realm, displayHostOnly, displayHost };
+ },
+
+ makeAuthMessage(prompt, channel, authInfo) {
+ if (prompt.modalType != MODAL_TYPE_TAB) {
+ return this._legacyMakeAuthMessage(channel, authInfo);
+ }
+
+ let isProxy = authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY;
+ let isPassOnly = authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD;
+ let isCrossOrig =
+ authInfo.flags & Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE;
+ let username = authInfo.username;
+
+ // We use the realm and displayHost only for proxy auth,
+ // and the displayHostOnly (hostPort) only for x-origin auth prompts.
+ // Otherwise we rely on the title of the dialog displaying the correct
+ // title.
+ let { displayHost, realm, displayHostOnly } = this.getAuthTarget(
+ channel,
+ authInfo
+ );
+
+ if (isProxy) {
+ // The realm is server-controlled. Trim it if it's very long, to
+ // avoid the dialog becoming unusable.
+ // For background, see https://bugzilla.mozilla.org/show_bug.cgi?id=244273
+ if (realm.length > 150) {
+ realm = realm.substring(0, 150);
+ // Append "..." (or localized equivalent).
+ realm += this.ellipsis;
+ }
+
+ return this.getLocalizedString("EnterLoginForProxy3", [
+ realm,
+ displayHost,
+ ]);
+ }
+ if (isPassOnly) {
+ return this.getLocalizedString("EnterPasswordOnlyFor", [username]);
+ }
+ if (isCrossOrig) {
+ return this.getLocalizedString("EnterCredentialsCrossOrigin", [
+ displayHostOnly,
+ ]);
+ }
+ return this.getLocalizedString("EnterCredentials");
+ },
+
+ _legacyMakeAuthMessage(channel, authInfo) {
+ let isProxy = authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY;
+ let isPassOnly = authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD;
+ let isCrossOrig =
+ authInfo.flags & Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE;
+
+ let username = authInfo.username;
+ let { displayHost, realm } = this.getAuthTarget(channel, authInfo);
+
+ // Suppress "the site says: $realm" when we synthesized a missing realm.
+ if (!authInfo.realm && !isProxy) {
+ realm = "";
+ }
+
+ // The realm is server-controlled. Trim it if it's very long, to
+ // avoid the dialog becoming unusable.
+ // For background, see https://bugzilla.mozilla.org/show_bug.cgi?id=244273
+ if (realm.length > 150) {
+ realm = realm.substring(0, 150);
+ // Append "..." (or localized equivalent).
+ realm += this.ellipsis;
+ }
+
+ let text;
+ if (isProxy) {
+ text = this.getLocalizedString("EnterLoginForProxy3", [
+ realm,
+ displayHost,
+ ]);
+ } else if (isPassOnly) {
+ text = this.getLocalizedString("EnterPasswordFor", [
+ username,
+ displayHost,
+ ]);
+ } else if (isCrossOrig) {
+ text = this.getLocalizedString("EnterUserPasswordForCrossOrigin2", [
+ displayHost,
+ ]);
+ } else if (!realm) {
+ text = this.getLocalizedString("EnterUserPasswordFor2", [displayHost]);
+ } else {
+ text = this.getLocalizedString("EnterLoginForRealm3", [
+ realm,
+ displayHost,
+ ]);
+ }
+
+ return text;
+ },
+
+ getBrandFullName() {
+ return this.brandBundle.GetStringFromName("brandFullName");
+ },
+};
+
+XPCOMUtils.defineLazyGetter(InternalPromptUtils, "strBundle", function () {
+ let bundle = Services.strings.createBundle(
+ "chrome://global/locale/commonDialogs.properties"
+ );
+ if (!bundle) {
+ throw new Error("String bundle for Prompter not present!");
+ }
+ return bundle;
+});
+
+XPCOMUtils.defineLazyGetter(InternalPromptUtils, "brandBundle", function () {
+ let bundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+ if (!bundle) {
+ throw new Error("String bundle for branding not present!");
+ }
+ return bundle;
+});
+
+XPCOMUtils.defineLazyGetter(InternalPromptUtils, "ellipsis", function () {
+ let ellipsis = "\u2026";
+ try {
+ ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis",
+ Ci.nsIPrefLocalizedString
+ ).data;
+ } catch (e) {}
+ return ellipsis;
+});
+
+class ModalPrompter {
+ constructor({
+ browsingContext = null,
+ domWin = null,
+ modalType = null,
+ async = false,
+ }) {
+ if (browsingContext && domWin) {
+ throw new Error("Pass either browsingContext or domWin");
+ }
+
+ if (domWin) {
+ // We have a domWin, get the associated browsing context
+ this.browsingContext = BrowsingContext.getFromWindow(domWin);
+ } else {
+ this.browsingContext = browsingContext;
+ }
+
+ if (
+ domWin &&
+ (!modalType || modalType == MODAL_TYPE_WINDOW) &&
+ !this.browsingContext?.isContent &&
+ this.browsingContext?.associatedWindow?.gDialogBox
+ ) {
+ modalType = MODAL_TYPE_INTERNAL_WINDOW;
+ }
+
+ // Use given modal type or fallback to default
+ this.modalType = modalType || ModalPrompter.defaultModalType;
+
+ this.async = async;
+
+ this.QueryInterface = ChromeUtils.generateQI([
+ "nsIPrompt",
+ "nsIAuthPrompt",
+ "nsIAuthPrompt2",
+ "nsIWritablePropertyBag2",
+ ]);
+ }
+
+ set modalType(modalType) {
+ // Setting modal type window is always allowed
+ if (modalType == MODAL_TYPE_WINDOW) {
+ this._modalType = modalType;
+ return;
+ }
+
+ // For content prompts for non-content windows, use window prompts:
+ if (modalType == MODAL_TYPE_CONTENT && !this.browsingContext?.isContent) {
+ this._modalType = MODAL_TYPE_WINDOW;
+ return;
+ }
+
+ // We can't use content / tab prompts if we don't have a suitable parent.
+ if (
+ !this.browsingContext?.isContent &&
+ modalType != MODAL_TYPE_INTERNAL_WINDOW
+ ) {
+ // Only show this error if we're not about to fall back again and show a different one.
+ if (this.browsingContext?.associatedWindow?.gDialogBox) {
+ console.error(
+ "Prompter: Browser not available. Falling back to internal window prompt."
+ );
+ }
+ modalType = MODAL_TYPE_INTERNAL_WINDOW;
+ }
+
+ if (
+ modalType == MODAL_TYPE_INTERNAL_WINDOW &&
+ (this.browsingContext?.isContent ||
+ !this.browsingContext?.associatedWindow?.gDialogBox)
+ ) {
+ console.error(
+ "Prompter: internal dialogs not available in this context. Falling back to window prompt."
+ );
+ modalType = MODAL_TYPE_WINDOW;
+ }
+
+ this._modalType = modalType;
+ }
+
+ get modalType() {
+ return this._modalType;
+ }
+
+ /* ---------- internal methods ---------- */
+
+ /**
+ * Synchronous wrapper around {@link ModalPrompter#openPrompt}
+ * @param {Object} args Prompt arguments. When prompt has been closed, they are updated to reflect the result state.
+ */
+ openPromptSync(args) {
+ let closed = false;
+ this.openPrompt(args)
+ .then(returnedArgs => {
+ if (returnedArgs) {
+ for (let key in returnedArgs) {
+ args[key] = returnedArgs[key];
+ }
+ }
+ })
+ .finally(() => {
+ closed = true;
+ });
+ Services.tm.spinEventLoopUntilOrQuit(
+ "prompts/Prompter.jsm:openPromptSync",
+ () => closed
+ );
+ }
+
+ async openPrompt(args) {
+ if (!this.browsingContext) {
+ // We don't have a browsing context, fallback to a window prompt.
+ args.modalType = MODAL_TYPE_WINDOW;
+ this.openWindowPrompt(null, args);
+ return args;
+ }
+
+ if (this._modalType == MODAL_TYPE_INTERNAL_WINDOW) {
+ await this.openInternalWindowPrompt(
+ this.browsingContext.associatedWindow,
+ args
+ );
+ return args;
+ }
+
+ // Select prompts are not part of CommonDialog
+ // and thus not supported as tab or content prompts yet. See Bug 1622817.
+ // Once they are integrated this override should be removed.
+ if (args.promptType == "select" && this.modalType !== MODAL_TYPE_WINDOW) {
+ console.error(
+ "Prompter: 'select' prompts do not support tab/content prompting. Falling back to window prompt."
+ );
+ args.modalType = MODAL_TYPE_WINDOW;
+ } else {
+ args.modalType = this.modalType;
+ }
+
+ const IS_CONTENT =
+ Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+
+ let actor;
+ try {
+ if (IS_CONTENT) {
+ // When in the content, get the PromptChild actor.
+ actor =
+ this.browsingContext.window.windowGlobalChild.getActor("Prompt");
+ } else {
+ // When in the parent, get the PromptParent actor.
+ actor = this.browsingContext.currentWindowGlobal.getActor("Prompt");
+ }
+ } catch (_) {
+ // We can't get the prompt actor, fallback to window prompt.
+ let parentWin;
+ // If given a chrome BC we can try to get its window
+ if (!this.browsingContext.isContent && this.browsingContext.window) {
+ parentWin = this.browsingContext.window;
+ } else {
+ // Try to get the window which is the browsers parent
+ parentWin = this.browsingContext.top?.embedderElement?.ownerGlobal;
+ }
+ this.openWindowPrompt(parentWin, args);
+ return args;
+ }
+
+ /* For prompts with a channel, we want to show the origin requesting
+ * authentication. This is different from the prompt principal,
+ * which is based on the document loaded in the browsing context over
+ * which the prompt appears. So if page foo.com loads bar.com, and the
+ * latter asks for auth, we want that bar.com's origin, not foo.com.
+ * To avoid confusion, we use different properties
+ * (authOrigin / promptPrincipal) to track this information.
+ */
+ if (args.channel) {
+ try {
+ args.authOrigin = args.channel.URI.hostPort;
+ } catch (ex) {
+ args.authOrigin = args.channel.URI.prePath;
+ }
+ args.isInsecureAuth =
+ args.channel.URI.schemeIs("http") &&
+ !args.channel.loadInfo.isTopLevelLoad;
+ // whether we are going to prompt the user for their credentials for a different base domain.
+ // When true, auth prompt spoofing protection mechanisms will be triggered (see bug 791594).
+ args.isTopLevelCrossDomainAuth = false;
+ // We don't support auth prompt spoofing protections for sub resources and window prompts
+ if (
+ args.modalType == MODAL_TYPE_TAB &&
+ args.channel.loadInfo.isTopLevelLoad
+ ) {
+ // check if this is a request from a third party
+ try {
+ args.isTopLevelCrossDomainAuth =
+ this.browsingContext.currentWindowGlobal?.documentPrincipal?.isThirdPartyURI(
+ args.channel.URI
+ );
+ } catch (e) {
+ // isThirdPartyURI failes for about:/blob/data URIs
+ console.warn("nsPrompter: isThirdPartyURI failed: " + e);
+ }
+ }
+ } else {
+ args.promptPrincipal =
+ this.browsingContext.window?.document.nodePrincipal;
+ }
+ if (IS_CONTENT) {
+ let docShell = this.browsingContext.docShell;
+ let inPermitUnload = docShell?.contentViewer?.inPermitUnload;
+ args.inPermitUnload = inPermitUnload;
+ let eventDetail = Cu.cloneInto(
+ {
+ tabPrompt: this.modalType != MODAL_TYPE_WINDOW,
+ inPermitUnload,
+ },
+ this.browsingContext.window
+ );
+ PromptUtils.fireDialogEvent(
+ this.browsingContext.window,
+ "DOMWillOpenModalDialog",
+ null,
+ eventDetail
+ );
+
+ // Put content window in the modal state while the prompt is open.
+ let windowUtils = this.browsingContext.window?.windowUtils;
+ if (windowUtils) {
+ windowUtils.enterModalState();
+ }
+ } else if (args.inPermitUnload) {
+ args.promptPrincipal =
+ this.browsingContext.currentWindowGlobal.documentPrincipal;
+ }
+
+ // It is technically possible for multiple prompts to be sent from a single
+ // BrowsingContext. See bug 1266353. We use a randomly generated UUID to
+ // differentiate between the different prompts.
+ let id = "id" + Services.uuid.generateUUID().toString();
+
+ args._remoteId = id;
+
+ let returnedArgs;
+ try {
+ if (IS_CONTENT) {
+ // If we're in the content process, send a message to the PromptParent
+ // window actor.
+ returnedArgs = await actor.sendQuery("Prompt:Open", args);
+ } else {
+ // If we're in the parent process we already have the parent actor.
+ // We can call its message handler directly.
+ returnedArgs = await actor.receiveMessage({
+ name: "Prompt:Open",
+ data: args,
+ });
+ }
+
+ if (returnedArgs?.promptAborted) {
+ throw Components.Exception(
+ "prompt aborted by user",
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ }
+ } finally {
+ if (IS_CONTENT) {
+ let windowUtils = this.browsingContext.window?.windowUtils;
+ if (windowUtils) {
+ windowUtils.leaveModalState();
+ }
+ PromptUtils.fireDialogEvent(
+ this.browsingContext.window,
+ "DOMModalDialogClosed"
+ );
+ }
+ }
+ return returnedArgs;
+ }
+
+ /**
+ * Open a window modal prompt
+ *
+ * There's an implied contract that says modal prompts should still work when
+ * no "parent" window is passed for the dialog (eg, the "Master Password"
+ * dialog does this). These prompts must be shown even if there are *no*
+ * visible windows at all.
+ * We try and find a window to use as the parent, but don't consider if that
+ * is visible before showing the prompt. parentWindow may still be null if
+ * there are _no_ windows open.
+ * @param {Window} [parentWindow] - The parent window for the prompt, may be
+ * null.
+ * @param {Object} args - Prompt options and return values.
+ */
+ openWindowPrompt(parentWindow = null, args) {
+ let uri = args.promptType == "select" ? SELECT_DIALOG : COMMON_DIALOG;
+ let propBag = PromptUtils.objectToPropBag(args);
+ Services.ww.openWindow(
+ parentWindow || Services.ww.activeWindow,
+ uri,
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ propBag
+ );
+ PromptUtils.propBagToObject(propBag, args);
+ }
+
+ async openInternalWindowPrompt(parentWindow, args) {
+ if (!parentWindow?.gDialogBox || !ModalPrompter.windowPromptSubDialog) {
+ this.openWindowPrompt(parentWindow, args);
+ return;
+ }
+ let propBag = PromptUtils.objectToPropBag(args);
+ propBag.setProperty("async", this.async);
+ let uri = args.promptType == "select" ? SELECT_DIALOG : COMMON_DIALOG;
+ await parentWindow.gDialogBox.open(uri, propBag);
+ propBag.deleteProperty("async");
+ PromptUtils.propBagToObject(propBag, args);
+ }
+
+ /**
+ * Calls async prompt method and optionally runs promise chained task on
+ * result data. Converts result data to nsIPropertyBag.
+ * @param {Object} args - Prompt arguments.
+ * @param {Function} [task] - Function which is called with the modified
+ * prompt args object once the prompt has been closed. Must return a
+ * result object for the prompt caller.
+ * @returns {Promise<nsIPropertyBag>} - Resolves with a property bag holding the
+ * prompt result properties. Resolves once prompt has been closed.
+ */
+ async openPromptAsync(args, task) {
+ let result = await this.openPrompt(args);
+ // If task is not defined, the prompt method does not return
+ // anything. In this case we can resolve without value.
+ if (!task) {
+ return undefined;
+ }
+ // Convert task result to nsIPropertyBag and resolve
+ let taskResult = task(result);
+ if (!(taskResult instanceof Object)) {
+ throw new Error("task must return object");
+ }
+ return PromptUtils.objectToPropBag(taskResult);
+ }
+
+ /*
+ * ---------- interface disambiguation ----------
+ *
+ * nsIPrompt and nsIAuthPrompt share 3 method names with slightly
+ * different arguments. All but prompt() have the same number of
+ * arguments, so look at the arg types to figure out how we're being
+ * called. :-(
+ */
+ prompt() {
+ // also, the nsIPrompt flavor has 5 args instead of 6.
+ if (typeof arguments[2] == "object") {
+ return this.nsIPrompt_prompt.apply(this, arguments);
+ }
+ return this.nsIAuthPrompt_prompt.apply(this, arguments);
+ }
+
+ promptUsernameAndPassword() {
+ // Both have 6 args, so use types.
+ if (typeof arguments[2] == "object") {
+ // Add the null channel:
+ let args = Array.from(arguments);
+ args.unshift(null);
+ return this.nsIPrompt_promptUsernameAndPassword.apply(this, args);
+ }
+ return this.nsIAuthPrompt_promptUsernameAndPassword.apply(this, arguments);
+ }
+
+ promptPassword() {
+ // Both have 5 args, so use types.
+ if (typeof arguments[2] == "object") {
+ // Add the null channel:
+ let args = Array.from(arguments);
+ args.unshift(null);
+ return this.nsIPrompt_promptPassword.apply(this, args);
+ }
+ return this.nsIAuthPrompt_promptPassword.apply(this, arguments);
+ }
+
+ /* ---------- nsIPrompt ---------- */
+
+ alert(title, text) {
+ if (!title) {
+ title = InternalPromptUtils.getLocalizedString("Alert");
+ }
+
+ let args = {
+ promptType: "alert",
+ title,
+ text,
+ };
+
+ if (this.async) {
+ return this.openPromptAsync(args);
+ }
+
+ return this.openPromptSync(args);
+ }
+
+ alertCheck(title, text, checkLabel, checkValue) {
+ if (!title) {
+ title = InternalPromptUtils.getLocalizedString("Alert");
+ }
+
+ // For sync calls checkValue is an XPCOM inout. XPCOM wraps primitves in
+ // objects for call by reference.
+ // The async version of this method uses call by value.
+ let checked = this.async ? checkValue : checkValue.value;
+
+ let args = {
+ promptType: "alertCheck",
+ title,
+ text,
+ checkLabel,
+ checked,
+ };
+
+ if (this.async) {
+ return this.openPromptAsync(args, result => ({
+ checked: result.checked,
+ }));
+ }
+
+ this.openPromptSync(args);
+ checkValue.value = args.checked;
+ return undefined;
+ }
+
+ confirm(title, text) {
+ if (!title) {
+ title = InternalPromptUtils.getLocalizedString("Confirm");
+ }
+
+ let args = {
+ promptType: "confirm",
+ title,
+ text,
+ ok: false,
+ };
+
+ if (this.async) {
+ return this.openPromptAsync(args, result => ({ ok: result.ok }));
+ }
+
+ this.openPromptSync(args);
+ return args.ok;
+ }
+
+ confirmCheck(title, text, checkLabel, checkValue) {
+ if (!title) {
+ title = InternalPromptUtils.getLocalizedString("ConfirmCheck");
+ }
+
+ let checked = this.async ? checkValue : checkValue.value;
+
+ let args = {
+ promptType: "confirmCheck",
+ title,
+ text,
+ checkLabel,
+ checked,
+ ok: false,
+ };
+
+ if (this.async) {
+ return this.openPromptAsync(args, result => ({
+ // Checkbox state always returned, even if cancel clicked.
+ checked: result.checked,
+ // Did user click Ok or Cancel?
+ ok: result.ok,
+ }));
+ }
+
+ this.openPromptSync(args);
+ checkValue.value = args.checked;
+ return args.ok;
+ }
+
+ confirmEx(
+ title,
+ text,
+ flags,
+ button0,
+ button1,
+ button2,
+ checkLabel,
+ checkValue,
+ extraArgs = {}
+ ) {
+ if (!title) {
+ title = InternalPromptUtils.getLocalizedString("Confirm");
+ }
+
+ let args = {
+ promptType: "confirmEx",
+ title,
+ text,
+ checkLabel,
+ checked: this.async ? checkValue : checkValue.value,
+ ok: false,
+ buttonNumClicked: 1,
+ ...extraArgs,
+ };
+
+ let [label0, label1, label2, defaultButtonNum, isDelayEnabled] =
+ InternalPromptUtils.confirmExHelper(flags, button0, button1, button2);
+
+ args.defaultButtonNum = defaultButtonNum;
+ args.enableDelay = isDelayEnabled;
+
+ if (label0) {
+ args.button0Label = label0;
+ if (label1) {
+ args.button1Label = label1;
+ if (label2) {
+ args.button2Label = label2;
+ }
+ }
+ }
+
+ if (this.async) {
+ return this.openPromptAsync(args, result => ({
+ checked: !!result.checked,
+ buttonNumClicked: result.buttonNumClicked,
+ }));
+ }
+
+ this.openPromptSync(args);
+ checkValue.value = args.checked;
+ return args.buttonNumClicked;
+ }
+
+ nsIPrompt_prompt(title, text, value, checkLabel, checkValue) {
+ if (!title) {
+ title = InternalPromptUtils.getLocalizedString("Prompt");
+ }
+
+ let args = {
+ promptType: "prompt",
+ title,
+ text,
+ value: this.async ? value : value.value,
+ checkLabel,
+ checked: this.async ? checkValue : checkValue.value,
+ ok: false,
+ };
+
+ if (this.async) {
+ return this.openPromptAsync(args, result => ({
+ checked: !!result.checked,
+ value: result.value,
+ ok: result.ok,
+ }));
+ }
+
+ this.openPromptSync(args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ value.value = args.value;
+ }
+
+ return ok;
+ }
+
+ nsIPrompt_promptUsernameAndPassword(channel, title, text, user, pass) {
+ if (!title) {
+ title = InternalPromptUtils.getLocalizedString(
+ "PromptUsernameAndPassword3",
+ [InternalPromptUtils.getBrandFullName()]
+ );
+ }
+
+ let args = {
+ channel,
+ promptType: "promptUserAndPass",
+ title,
+ text,
+ user: this.async ? user : user.value,
+ pass: this.async ? pass : pass.value,
+ button0Label: InternalPromptUtils.getLocalizedString("SignIn"),
+ ok: false,
+ };
+
+ if (this.async) {
+ return this.openPromptAsync(args, result => ({
+ user: result.user,
+ pass: result.pass,
+ ok: result.ok,
+ }));
+ }
+
+ this.openPromptSync(args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ user.value = args.user;
+ pass.value = args.pass;
+ }
+
+ return ok;
+ }
+
+ nsIPrompt_promptPassword(channel, title, text, pass) {
+ if (!title) {
+ title = InternalPromptUtils.getLocalizedString("PromptPassword3", [
+ InternalPromptUtils.getBrandFullName(),
+ ]);
+ }
+
+ let args = {
+ channel,
+ promptType: "promptPassword",
+ title,
+ text,
+ pass: this.async ? pass : pass.value,
+ button0Label: InternalPromptUtils.getLocalizedString("SignIn"),
+ ok: false,
+ };
+
+ if (this.async) {
+ return this.openPromptAsync(args, result => ({
+ pass: result.pass,
+ ok: result.ok,
+ }));
+ }
+
+ this.openPromptSync(args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ pass.value = args.pass;
+ }
+
+ return ok;
+ }
+
+ select(title, text, list, selected) {
+ if (!title) {
+ title = InternalPromptUtils.getLocalizedString("Select");
+ }
+
+ let args = {
+ promptType: "select",
+ title,
+ text,
+ list,
+ selected: -1,
+ ok: false,
+ };
+
+ if (this.async) {
+ return this.openPromptAsync(args, result => ({
+ selected: result.selected,
+ ok: result.ok,
+ }));
+ }
+
+ this.openPromptSync(args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ selected.value = args.selected;
+ }
+
+ return ok;
+ }
+
+ /* ---------- nsIAuthPrompt ---------- */
+
+ nsIAuthPrompt_prompt(
+ title,
+ text,
+ passwordRealm,
+ savePassword,
+ defaultText,
+ result
+ ) {
+ // The passwordRealm and savePassword args were ignored by nsPrompt.cpp
+ if (defaultText) {
+ result.value = defaultText;
+ }
+ return this.nsIPrompt_prompt(title, text, result, null, {});
+ }
+
+ nsIAuthPrompt_promptUsernameAndPassword(
+ title,
+ text,
+ passwordRealm,
+ savePassword,
+ user,
+ pass
+ ) {
+ // The passwordRealm and savePassword args were ignored by nsPrompt.cpp
+ return this.nsIPrompt_promptUsernameAndPassword(
+ null,
+ title,
+ text,
+ user,
+ pass
+ );
+ }
+
+ nsIAuthPrompt_promptPassword(title, text, passwordRealm, savePassword, pass) {
+ // The passwordRealm and savePassword args were ignored by nsPrompt.cpp,
+ // and we don't have a channel here.
+ return this.nsIPrompt_promptPassword(null, title, text, pass);
+ }
+
+ /* ---------- nsIAuthPrompt2 ---------- */
+
+ promptAuth(channel, level, authInfo) {
+ let message = InternalPromptUtils.makeAuthMessage(this, channel, authInfo);
+
+ let [username, password] = InternalPromptUtils.getAuthInfo(authInfo);
+
+ let userParam = this.async ? username : { value: username };
+ let passParam = this.async ? password : { value: password };
+
+ let result;
+ if (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) {
+ result = this.nsIPrompt_promptPassword(channel, null, message, passParam);
+ } else {
+ result = this.nsIPrompt_promptUsernameAndPassword(
+ channel,
+ null,
+ message,
+ userParam,
+ passParam
+ );
+ }
+
+ // For the async case result is an nsIPropertyBag with prompt results.
+ if (this.async) {
+ return result.then(bag => {
+ let ok = bag.getProperty("ok");
+ if (ok) {
+ let username = bag.getProperty("user");
+ let password = bag.getProperty("pass");
+ InternalPromptUtils.setAuthInfo(authInfo, username, password);
+ }
+ return ok;
+ });
+ }
+
+ // For the sync case result is the "ok" boolean which indicates whether
+ // the user has confirmed the dialog.
+ if (result) {
+ InternalPromptUtils.setAuthInfo(
+ authInfo,
+ userParam.value,
+ passParam.value
+ );
+ }
+ return result;
+ }
+
+ asyncPromptAuth(
+ channel,
+ callback,
+ context,
+ level,
+ authInfo,
+ checkLabel,
+ checkValue
+ ) {
+ // Nothing calls this directly; netwerk ends up going through
+ // nsIPromptService::GetPrompt, which delegates to login manager.
+ // Login manger handles the async bits itself, and only calls out
+ // promptAuth, never asyncPromptAuth.
+ //
+ // Bug 565582 will change this.
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ /* ---------- nsIWritablePropertyBag2 ---------- */
+ // Legacy way to set modal type when prompting via nsIPrompt.
+ // Please prompt via nsIPromptService. This will be removed in the future.
+ setPropertyAsUint32(name, value) {
+ if (name == "modalType") {
+ this.modalType = value;
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ }
+}
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ ModalPrompter,
+ "defaultModalType",
+ "prompts.defaultModalType",
+ MODAL_TYPE_WINDOW
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ ModalPrompter,
+ "windowPromptSubDialog",
+ "prompts.windowPromptSubDialog",
+ false
+);
+
+export function AuthPromptAdapterFactory() {}
+AuthPromptAdapterFactory.prototype = {
+ classID: Components.ID("{6e134924-6c3a-4d86-81ac-69432dd971dc}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPromptAdapterFactory"]),
+
+ /* ---------- nsIAuthPromptAdapterFactory ---------- */
+
+ createAdapter(oldPrompter) {
+ return new AuthPromptAdapter(oldPrompter);
+ },
+};
+
+// Takes an nsIAuthPrompt implementation, wraps it with a nsIAuthPrompt2 shell.
+function AuthPromptAdapter(oldPrompter) {
+ this.oldPrompter = oldPrompter;
+}
+AuthPromptAdapter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
+ oldPrompter: null,
+
+ /* ---------- nsIAuthPrompt2 ---------- */
+
+ promptAuth(channel, level, authInfo, checkLabel, checkValue) {
+ let message = InternalPromptUtils.makeAuthMessage(
+ this.oldPrompter,
+ channel,
+ authInfo
+ );
+
+ let [username, password] = InternalPromptUtils.getAuthInfo(authInfo);
+ let userParam = { value: username };
+ let passParam = { value: password };
+
+ let { displayHost, realm } = InternalPromptUtils.getAuthTarget(
+ channel,
+ authInfo
+ );
+ let authTarget = displayHost + " (" + realm + ")";
+
+ let ok;
+ if (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) {
+ ok = this.oldPrompter.promptPassword(
+ null,
+ message,
+ authTarget,
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
+ passParam
+ );
+ } else {
+ ok = this.oldPrompter.promptUsernameAndPassword(
+ null,
+ message,
+ authTarget,
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
+ userParam,
+ passParam
+ );
+ }
+
+ if (ok) {
+ InternalPromptUtils.setAuthInfo(
+ authInfo,
+ userParam.value,
+ passParam.value
+ );
+ }
+ return ok;
+ },
+
+ asyncPromptAuth(
+ channel,
+ callback,
+ context,
+ level,
+ authInfo,
+ checkLabel,
+ checkValue
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
diff --git a/toolkit/components/prompts/src/components.conf b/toolkit/components/prompts/src/components.conf
new file mode 100644
index 0000000000..7be17963ed
--- /dev/null
+++ b/toolkit/components/prompts/src/components.conf
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{6e134924-6c3a-4d86-81ac-69432dd971dc}',
+ 'contract_ids': ['@mozilla.org/network/authprompt-adapter-factory;1'],
+ 'esModule': 'resource://gre/modules/Prompter.sys.mjs',
+ 'constructor': 'AuthPromptAdapterFactory',
+ },
+]
+
+if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android':
+ Classes += [
+ {
+ 'js_name': 'prompt',
+ 'cid': '{1c978d25-b37f-43a8-a2d6-0c7a239ead87}',
+ 'contract_ids': ['@mozilla.org/prompter;1'],
+ 'interfaces': ['nsIPromptService'],
+ 'esModule': 'resource://gre/modules/Prompter.sys.mjs',
+ 'constructor': 'Prompter',
+ },
+ ]
diff --git a/toolkit/components/prompts/src/moz.build b/toolkit/components/prompts/src/moz.build
new file mode 100644
index 0000000000..00eea500c5
--- /dev/null
+++ b/toolkit/components/prompts/src/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "CommonDialog.sys.mjs",
+ "Prompter.sys.mjs",
+ "PromptUtils.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/toolkit/components/prompts/test/.eslintrc.js b/toolkit/components/prompts/test/.eslintrc.js
new file mode 100644
index 0000000000..af973e82fe
--- /dev/null
+++ b/toolkit/components/prompts/test/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+ rules: {
+ // ownerGlobal doesn't exist in content privileged windows.
+ "mozilla/use-ownerGlobal": "off",
+ },
+};
diff --git a/toolkit/components/prompts/test/PromptTestUtils.sys.mjs b/toolkit/components/prompts/test/PromptTestUtils.sys.mjs
new file mode 100644
index 0000000000..c55259ce1b
--- /dev/null
+++ b/toolkit/components/prompts/test/PromptTestUtils.sys.mjs
@@ -0,0 +1,237 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Utility module for tests to interact with prompts spawned by nsIPrompt or
+ * nsIPromptService.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs";
+import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs";
+
+const kPrefs = {};
+
+// Whether prompts with modal type TAB are shown as SubDialog (true) or
+// TabModalPrompt (false).
+XPCOMUtils.defineLazyPreferenceGetter(
+ kPrefs,
+ "tabPromptSubDialogEnabled",
+ "prompts.tabChromePromptSubDialog",
+ false
+);
+
+// Whether web content prompts (alert etc.) are shown as SubDialog (true)
+// or TabModalPrompt (false)
+XPCOMUtils.defineLazyPreferenceGetter(
+ kPrefs,
+ "contentPromptSubDialogEnabled",
+ "prompts.contentPromptSubDialog",
+ false
+);
+
+function isCommonDialog(modalType) {
+ return (
+ modalType === Services.prompt.MODAL_TYPE_WINDOW ||
+ (kPrefs.tabPromptSubDialogEnabled &&
+ modalType === Services.prompt.MODAL_TYPE_TAB) ||
+ (kPrefs.contentPromptSubDialogEnabled &&
+ modalType === Services.prompt.MODAL_TYPE_CONTENT)
+ );
+}
+
+export let PromptTestUtils = {
+ /**
+ * Wait for a prompt from nsIPrompt or nsIPromptsService, interact with it and
+ * click the specified button to close it.
+ * @param {Browser|Window} [parent] - Parent of the prompt. This can be
+ * either the parent window or the browser. For tab prompts, if given a
+ * window, the currently selected browser in that window will be used.
+ * @param {Object} promptOptions - @see waitForPrompt
+ * @param {Object} promptActions - @see handlePrompt
+ * @returns {Promise} - A promise which resolves once the prompt has been
+ * closed.
+ */
+ async handleNextPrompt(parent, promptOptions, promptActions) {
+ let dialog = await this.waitForPrompt(parent, promptOptions);
+ return this.handlePrompt(dialog, promptActions);
+ },
+
+ /**
+ * Interact with an existing prompt and close it.
+ * @param {Dialog} dialog - The dialog instance associated with the prompt.
+ * @param {Object} [actions] - Options on how to interact with the
+ * prompt and how to close it.
+ * @param {Boolean} [actions.checkboxState] - Set the checkbox state.
+ * true = checked, false = unchecked.
+ * @param {Number} [actions.buttonNumClick] - Which button to click to close
+ * the prompt.
+ * @param {String} [actions.loginInput] - Input text for the login text field.
+ * This field is also used for text input for the "prompt" type.
+ * @param {String} [actions.passwordInput] - Input text for the password text
+ * field.
+ * @returns {Promise} - A promise which resolves once the prompt has been
+ * closed.
+ */
+ handlePrompt(
+ dialog,
+ {
+ checkboxState = null,
+ buttonNumClick = 0,
+ loginInput = null,
+ passwordInput = null,
+ } = {}
+ ) {
+ let promptClosePromise;
+
+ // Get parent window to listen for prompt close event
+ let win;
+ if (isCommonDialog(dialog.args.modalType)) {
+ win = dialog.ui.prompt?.opener;
+ } else {
+ // Tab prompts should always have a parent window
+ win = dialog.ui.prompt.win;
+ }
+
+ if (win) {
+ promptClosePromise = BrowserTestUtils.waitForEvent(
+ win,
+ "DOMModalDialogClosed"
+ );
+ } else {
+ // We don't have a parent, wait for window close instead
+ promptClosePromise = BrowserTestUtils.windowClosed(dialog.ui.prompt);
+ }
+
+ if (typeof checkboxState == "boolean") {
+ dialog.ui.checkbox.checked = checkboxState;
+ }
+
+ if (loginInput != null) {
+ dialog.ui.loginTextbox.value = loginInput;
+ }
+
+ if (passwordInput != null) {
+ dialog.ui.password1Textbox.value = passwordInput;
+ }
+
+ let button = dialog.ui["button" + buttonNumClick];
+ if (!button) {
+ throw new Error("Could not find button with index " + buttonNumClick);
+ }
+ button.click();
+
+ return promptClosePromise;
+ },
+
+ /**
+ * Wait for a prompt from nsIPrompt or nsIPromptsService to open.
+ * @param {Browser|Window} [parent] - Parent of the prompt. This can be either
+ * the parent window or the browser. For tab prompts, if given a window, the
+ * currently selected browser in that window will be used.
+ * If not given a parent, the method will return on prompts of any window.
+ * @param {Object} attrs - The prompt attributes to filter for.
+ * @param {Number} attrs.modalType - Whether the expected prompt is a content, tab or window prompt.
+ * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT>
+ * @param {String} [attrs.promptType] - Common dialog type of the prompt to filter for.
+ * @see {@link CommonDialog} for possible prompt types.
+ * @returns {Promise<CommonDialog>} - A Promise which resolves with a dialog
+ * object once the prompt has loaded.
+ */
+ async waitForPrompt(parent, { modalType, promptType = null } = {}) {
+ if (!modalType) {
+ throw new Error("modalType is mandatory");
+ }
+
+ // Get window by browser or browser by window, depending on what is passed
+ // via the parent arg. If the caller passes parent=null, both will be null.
+ let parentWindow;
+ let parentBrowser;
+ if (parent) {
+ if (Element.isInstance(parent)) {
+ // Parent is browser
+ parentBrowser = parent;
+ parentWindow = parentBrowser.ownerGlobal;
+ } else if (parent instanceof Ci.nsIDOMChromeWindow) {
+ // Parent is window
+ parentWindow = parent;
+ parentBrowser = parentWindow.gBrowser?.selectedBrowser;
+ } else {
+ throw new Error("Invalid parent. Expected browser or dom window");
+ }
+ }
+
+ let topic = isCommonDialog(modalType)
+ ? "common-dialog-loaded"
+ : "tabmodal-dialog-loaded";
+
+ let dialog;
+ await TestUtils.topicObserved(topic, subject => {
+ // If we are not given a browser, use the currently selected browser of the window
+ let browser =
+ parentBrowser || subject.ownerGlobal.gBrowser?.selectedBrowser;
+ if (isCommonDialog(modalType)) {
+ // Is not associated with given parent window, skip
+ if (parentWindow && subject.opener !== parentWindow) {
+ return false;
+ }
+
+ // For tab prompts, ensure that the associated browser matches.
+ if (browser && modalType == Services.prompt.MODAL_TYPE_TAB) {
+ let dialogBox = parentWindow.gBrowser.getTabDialogBox(browser);
+ let hasMatchingDialog = dialogBox
+ .getTabDialogManager()
+ ._dialogs.some(
+ d => d._frame?.browsingContext == subject.browsingContext
+ );
+ if (!hasMatchingDialog) {
+ return false;
+ }
+ }
+
+ if (browser && modalType == Services.prompt.MODAL_TYPE_CONTENT) {
+ let dialogBox = parentWindow.gBrowser.getTabDialogBox(browser);
+ let hasMatchingDialog = dialogBox
+ .getContentDialogManager()
+ ._dialogs.some(
+ d => d._frame?.browsingContext == subject.browsingContext
+ );
+ if (!hasMatchingDialog) {
+ return false;
+ }
+ }
+
+ // subject is the window object of the prompt which has a Dialog object
+ // attached.
+ dialog = subject.Dialog;
+ } else {
+ // subject is the tabprompt dom node
+ // Get the full prompt object which has the dialog object
+ let prompt = browser.tabModalPromptBox.getPrompt(subject);
+
+ // Is not associated with given parent browser, skip.
+ if (!prompt) {
+ return false;
+ }
+
+ dialog = prompt.Dialog;
+ }
+
+ // Not the modalType we're looking for.
+ // For window prompts dialog.args.modalType is undefined.
+ if (isCommonDialog(modalType) && dialog.args.modalType !== modalType) {
+ return false;
+ }
+
+ // Not the promptType we're looking for.
+ if (promptType && dialog.args.promptType !== promptType) {
+ return false;
+ }
+
+ // Prompt found
+ return true;
+ });
+
+ return dialog;
+ },
+};
diff --git a/toolkit/components/prompts/test/bug619644_inner.html b/toolkit/components/prompts/test/bug619644_inner.html
new file mode 100644
index 0000000000..cecfa78bad
--- /dev/null
+++ b/toolkit/components/prompts/test/bug619644_inner.html
@@ -0,0 +1,7 @@
+<head></head><body><p>Original content</p>
+<script>
+ window.opener.postMessage("", "*");
+ confirm("Message");
+ document.write("Extra content");
+ window.opener.postMessage(document.documentElement.innerHTML, "*");
+</script></body>
diff --git a/toolkit/components/prompts/test/bug625187_iframe.html b/toolkit/components/prompts/test/bug625187_iframe.html
new file mode 100644
index 0000000000..740d59a617
--- /dev/null
+++ b/toolkit/components/prompts/test/bug625187_iframe.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <title>Test for Bug 625187 - the iframe</title>
+<!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -
+ - Contributor(s):
+ - Mihai Sucan <mihai.sucan@gmail.com>
+ -->
+</head>
+<body>
+<p><button id="btn1" onclick="alert('hello world 2')">Button 2</button></p>
+<p><button id="btn2" onclick="window.parent.alert('hello world 3')">Button 3</button></p>
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/chrome.ini b/toolkit/components/prompts/test/chrome.ini
new file mode 100644
index 0000000000..90eee3a761
--- /dev/null
+++ b/toolkit/components/prompts/test/chrome.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+support-files =
+ prompt_common.js
+ chromeScript.js
+
+[test_modal_prompts.html]
+skip-if =
+ toolkit == 'android' #android: TIMED_OUT
+ os == 'linux' && (debug || asan || tsan)
+[test_modal_select.html]
+skip-if = toolkit == 'android' #android: TIMED_OUT
diff --git a/toolkit/components/prompts/test/chromeScript.js b/toolkit/components/prompts/test/chromeScript.js
new file mode 100644
index 0000000000..5bdf3606d4
--- /dev/null
+++ b/toolkit/components/prompts/test/chromeScript.js
@@ -0,0 +1,353 @@
+/* eslint-env mozilla/chrome-script */
+
+const { clearInterval, setInterval, setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+);
+
+var tabSubDialogsEnabled = Services.prefs.getBoolPref(
+ "prompts.tabChromePromptSubDialog",
+ false
+);
+
+var contentPromptSubdialogsEnabled = Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog",
+ false
+);
+
+// Define these to make EventUtils happy.
+let window = this;
+let parent = {};
+
+let EventUtils = {};
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
+ EventUtils
+);
+
+addMessageListener("handlePrompt", msg => {
+ info("Received handlePrompt message");
+ handlePromptWhenItAppears(msg.action, msg.modalType, msg.isSelect);
+});
+
+async function handlePromptWhenItAppears(action, modalType, isSelect) {
+ try {
+ if (!(await handlePrompt(action, modalType, isSelect))) {
+ setTimeout(
+ () => this.handlePromptWhenItAppears(action, modalType, isSelect),
+ 100
+ );
+ }
+ } catch (e) {
+ info(`handlePromptWhenItAppears: exception: ${e}`);
+ }
+}
+
+function checkTabModal(prompt, browser) {
+ let doc = browser.ownerDocument;
+
+ let { bottom: toolboxBottom } = doc
+ .getElementById("navigator-toolbox")
+ .getBoundingClientRect();
+
+ let { mainContainer } = prompt.ui;
+
+ let { x, y } = mainContainer.getBoundingClientRect();
+ ok(y > 0, "Container should have y > 0");
+ // Inset by 1px since the corner point doesn't return the frame due to the
+ // border-radius.
+ is(
+ doc.elementFromPoint(x + 1, y + 1).parentNode,
+ mainContainer,
+ "Check tabmodalprompt is visible"
+ );
+
+ info("Click to the left of the dialog over the content area");
+ isnot(
+ doc.elementFromPoint(x - 10, y + 50),
+ browser,
+ "Check clicks on the content area don't go to the browser"
+ );
+ is(
+ doc.elementFromPoint(x - 10, y + 50),
+ prompt.element,
+ "Check clicks on the content area go to the prompt dialog background"
+ );
+
+ if (prompt.args.modalType == Ci.nsIPrompt.MODAL_TYPE_TAB) {
+ ok(
+ y <= toolboxBottom - 5,
+ "Dialog should overlap the toolbox by at least 5px"
+ );
+ } else {
+ ok(y >= toolboxBottom, "Dialog must not overlap with toolbox.");
+ }
+
+ ok(
+ browser.hasAttribute("tabmodalPromptShowing"),
+ "Check browser has @tabmodalPromptShowing"
+ );
+}
+
+async function handlePrompt(action, modalType, isSelect) {
+ info(`handlePrompt: modalType=${modalType}`);
+
+ let ui;
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+
+ if (
+ (!contentPromptSubdialogsEnabled &&
+ modalType === Services.prompt.MODAL_TYPE_CONTENT) ||
+ (!tabSubDialogsEnabled && modalType === Services.prompt.MODAL_TYPE_TAB)
+ ) {
+ let gBrowser = browserWin.gBrowser;
+ let promptManager = gBrowser.getTabModalPromptBox(gBrowser.selectedBrowser);
+ let prompts = promptManager.listPrompts();
+ if (!prompts.length) {
+ info("handlePrompt: no prompt found. retrying...");
+ return false; // try again in a bit
+ }
+
+ ui = prompts[0].Dialog.ui;
+ checkTabModal(prompts[0], gBrowser.selectedBrowser);
+ } else {
+ let doc = getDialogDoc();
+ if (!doc) {
+ info("handlePrompt: no document found. retrying...");
+ return false; // try again in a bit
+ }
+
+ if (isSelect) {
+ ui = doc;
+ } else {
+ ui = doc.defaultView.Dialog.ui;
+ }
+ }
+
+ let dialogClosed = BrowserTestUtils.waitForEvent(
+ browserWin,
+ "DOMModalDialogClosed"
+ );
+
+ let promptState;
+ if (isSelect) {
+ promptState = getSelectState(ui);
+ dismissSelect(ui, action);
+ } else {
+ promptState = getPromptState(ui);
+ dismissPrompt(ui, action);
+ }
+
+ // Wait until the prompt has been closed before sending callback msg.
+ // Unless the test explicitly doesn't request a button click.
+ if (action.buttonClick !== "none") {
+ info(`handlePrompt: wait for dialogClosed`);
+ await dialogClosed;
+ }
+
+ info(`handlePrompt: send promptHandled`);
+ sendAsyncMessage("promptHandled", { promptState });
+ return true;
+}
+
+function getSelectState(ui) {
+ let listbox = ui.getElementById("list");
+
+ let state = {};
+ state.msg = ui.getElementById("info.txt").value;
+ state.selectedIndex = listbox.selectedIndex;
+ state.items = [];
+
+ for (let i = 0; i < listbox.itemCount; i++) {
+ let item = listbox.getItemAtIndex(i).label;
+ state.items.push(item);
+ }
+
+ return state;
+}
+
+function getPromptState(ui) {
+ let state = {};
+ state.msg = ui.infoBody.textContent;
+ state.infoRowHidden = ui.infoRow?.hidden || false;
+ state.titleHidden = ui.infoTitle.hidden;
+ state.textHidden = ui.loginContainer.hidden;
+ state.passHidden = ui.password1Container.hidden;
+ state.checkHidden = ui.checkboxContainer.hidden;
+ state.checkMsg = state.checkHidden ? "" : ui.checkbox.label;
+ state.checked = state.checkHidden ? false : ui.checkbox.checked;
+ // TabModalPrompts don't have an infoIcon
+ state.iconClass = ui.infoIcon ? ui.infoIcon.className : null;
+ state.textValue = ui.loginTextbox.value;
+ state.passValue = ui.password1Textbox.value;
+
+ state.butt0Label = ui.button0.label;
+ state.butt1Label = ui.button1.label;
+ state.butt2Label = ui.button2.label;
+
+ state.butt0Disabled = ui.button0.disabled;
+ state.butt1Disabled = ui.button1.disabled;
+ state.butt2Disabled = ui.button2.disabled;
+
+ function isDefaultButton(b) {
+ return b.hasAttribute("default") && b.getAttribute("default") == "true";
+ }
+ state.defButton0 = isDefaultButton(ui.button0);
+ state.defButton1 = isDefaultButton(ui.button1);
+ state.defButton2 = isDefaultButton(ui.button2);
+
+ let e = Services.focus.focusedElement;
+
+ if (e == null) {
+ state.focused = null;
+ } else if (ui.button0.isSameNode(e)) {
+ state.focused = "button0";
+ } else if (ui.button1.isSameNode(e)) {
+ state.focused = "button1";
+ } else if (ui.button2.isSameNode(e)) {
+ state.focused = "button2";
+ } else if (e.isSameNode(ui.loginTextbox)) {
+ state.focused = "textField";
+ } else if (e.isSameNode(ui.password1Textbox)) {
+ state.focused = "passField";
+ } else if (ui.infoBody.isSameNode(e)) {
+ state.focused = "infoBody";
+ } else {
+ state.focused =
+ "ERROR: unexpected element focused: " + (e ? e.localName : "<null>");
+ }
+
+ let treeOwner =
+ ui.prompt && ui.prompt.docShell && ui.prompt.docShell.treeOwner;
+ if (treeOwner && treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)) {
+ // Check that the dialog is modal, chrome and dependent;
+ // We can't just check window.opener because that'll be
+ // a content window, which therefore isn't exposed (it'll lie and
+ // be null).
+ let flags = treeOwner.getInterface(Ci.nsIAppWindow).chromeFlags;
+ state.chrome = (flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0;
+ state.dialog = (flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0;
+ state.chromeDependent =
+ (flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0;
+ let wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome);
+ state.isWindowModal = wbc.isWindowModal();
+ }
+
+ // Check the dialog is a common dialog document and has been embedded.
+ let isEmbedded = !!ui.prompt?.docShell?.chromeEventHandler;
+ let isCommonDialogDoc =
+ getDialogDoc()?.location.href.includes("commonDialog.xhtml");
+ state.isSubDialogPrompt = isCommonDialogDoc && isEmbedded;
+ state.showCallerOrigin = ui.prompt.args.showCallerOrigin;
+
+ return state;
+}
+
+function dismissSelect(ui, action) {
+ let dialog = ui.getElementsByTagName("dialog")[0];
+ let listbox = ui.getElementById("list");
+
+ if (action.selectItem) {
+ listbox.selectedIndex = 1;
+ }
+
+ if (action.buttonClick == "ok") {
+ dialog.acceptDialog();
+ } else if (action.buttonClick == "cancel") {
+ dialog.cancelDialog();
+ }
+}
+
+function dismissPrompt(ui, action) {
+ info(`dismissPrompt: action=${JSON.stringify(action)}`);
+ if (action.setCheckbox) {
+ // Annoyingly, the prompt code is driven by oncommand.
+ ui.checkbox.checked = true;
+ ui.checkbox.doCommand();
+ }
+
+ if ("textField" in action) {
+ ui.loginTextbox.setAttribute("value", action.textField);
+ }
+
+ if ("passField" in action) {
+ ui.password1Textbox.setAttribute("value", action.passField);
+ }
+
+ switch (action.buttonClick) {
+ case "ok":
+ case 0:
+ ui.button0.click();
+ break;
+ case "cancel":
+ case 1:
+ ui.button1.click();
+ break;
+ case 2:
+ ui.button2.click();
+ break;
+ case "ESC":
+ // XXX This is assuming tab-modal.
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ EventUtils.synthesizeKey("KEY_Escape", {}, browserWin);
+ break;
+ case "pollOK":
+ // Buttons are disabled at the moment, poll until they're reenabled.
+ // Can't use setInterval here, because the window's in a modal state
+ // and thus DOM events are suppressed.
+ let interval = setInterval(() => {
+ if (ui.button0.disabled) {
+ return;
+ }
+ ui.button0.click();
+ clearInterval(interval);
+ }, 100);
+ break;
+ case "none":
+ break;
+
+ default:
+ throw new Error("dismissPrompt action listed unknown button.");
+ }
+}
+
+function getDialogDoc() {
+ // Trudge through all the open windows, until we find the one
+ // that has either commonDialog.xhtml or selectDialog.xhtml loaded.
+ // var enumerator = Services.wm.getEnumerator("navigator:browser");
+ for (let { docShell } of Services.wm.getEnumerator(null)) {
+ var containedDocShells = docShell.getAllDocShellsInSubtree(
+ docShell.typeChrome,
+ docShell.ENUMERATE_FORWARDS
+ );
+ for (let childDocShell of containedDocShells) {
+ // Get the corresponding document for this docshell
+ // We don't want it if it's not done loading.
+ if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) {
+ continue;
+ }
+ var childDoc = childDocShell.contentViewer.DOMDocument;
+
+ if (
+ childDoc.location.href !=
+ "chrome://global/content/commonDialog.xhtml" &&
+ childDoc.location.href != "chrome://global/content/selectDialog.xhtml"
+ ) {
+ continue;
+ }
+
+ // We're expecting the dialog to be focused. If it's not yet, try later.
+ // (In particular, this is needed on Linux to reliably check focused elements.)
+ if (Services.focus.focusedWindow != childDoc.defaultView) {
+ continue;
+ }
+
+ return childDoc;
+ }
+ }
+
+ return null;
+}
diff --git a/toolkit/components/prompts/test/mochitest.ini b/toolkit/components/prompts/test/mochitest.ini
new file mode 100644
index 0000000000..9334a9bdb4
--- /dev/null
+++ b/toolkit/components/prompts/test/mochitest.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+support-files =
+ ../../passwordmgr/test/authenticate.sjs
+ bug619644_inner.html
+ bug625187_iframe.html
+ prompt_common.js
+ chromeScript.js
+
+[test_bug619644.html]
+skip-if = toolkit == 'android' # No tab prompts on android
+[test_bug620145.html]
+skip-if = toolkit == 'android' #TIMED_OUT
+[test_subresources_prompts.html]
+skip-if =
+ toolkit == 'android' || verify
+ http3
+fail-if = xorigin
+[test_dom_prompts.html]
+skip-if = toolkit == 'android' #android: bug 1267092
diff --git a/toolkit/components/prompts/test/prompt_common.js b/toolkit/components/prompts/test/prompt_common.js
new file mode 100644
index 0000000000..4b3a2262aa
--- /dev/null
+++ b/toolkit/components/prompts/test/prompt_common.js
@@ -0,0 +1,445 @@
+const { Cc, Ci, Cu: ChromeUtils } = SpecialPowers;
+
+/**
+ * Converts a property bag to object.
+ * @param {nsIPropertyBag} bag - The property bag to convert
+ * @returns {Object} - The object representation of the nsIPropertyBag
+ */
+function propBagToObject(bag) {
+ if (!(bag instanceof Ci.nsIPropertyBag)) {
+ throw new TypeError("Not a property bag");
+ }
+ let result = {};
+ for (let { name, value } of bag.enumerator) {
+ result[name] = value;
+ }
+ return result;
+}
+
+var modalType;
+var tabSubDialogsEnabled = SpecialPowers.Services.prefs.getBoolPref(
+ "prompts.tabChromePromptSubDialog",
+ false
+);
+var contentSubDialogsEnabled = SpecialPowers.Services.prefs.getBoolPref(
+ "prompts.contentPromptSubDialog",
+ false
+);
+var isSelectDialog = false;
+var isOSX = "nsILocalFileMac" in SpecialPowers.Ci;
+var isE10S = SpecialPowers.Services.appinfo.processType == 2;
+
+var gChromeScript = SpecialPowers.loadChromeScript(
+ SimpleTest.getTestFileURL("chromeScript.js")
+);
+SimpleTest.registerCleanupFunction(() => gChromeScript.destroy());
+
+async function runPromptCombinations(window, testFunc) {
+ let util = new PromptTestUtil(window);
+ let run = () => {
+ info(
+ `Running tests (modalType=${modalType}, usePromptService=${util.usePromptService}, useBrowsingContext=${util.useBrowsingContext}, useAsync=${util.useAsync})`
+ );
+ return testFunc(util);
+ };
+
+ // Prompt service with dom window parent only supports window prompts
+ util.usePromptService = true;
+ util.useBrowsingContext = false;
+ util.modalType = Ci.nsIPrompt.MODAL_TYPE_WINDOW;
+ modalType = util.modalType;
+ util.useAsync = false;
+ await run();
+
+ let modalTypes = [
+ Ci.nsIPrompt.MODAL_TYPE_WINDOW,
+ Ci.nsIPrompt.MODAL_TYPE_TAB,
+ Ci.nsIPrompt.MODAL_TYPE_CONTENT,
+ ];
+
+ for (let type of modalTypes) {
+ util.modalType = type;
+ modalType = type;
+
+ // Prompt service with browsing context sync
+ util.usePromptService = true;
+ util.useBrowsingContext = true;
+ util.useAsync = false;
+ await run();
+
+ // Prompt service with browsing context async
+ util.usePromptService = true;
+ util.useBrowsingContext = true;
+ util.useAsync = true;
+ await run();
+
+ // nsIPrompt
+ // modalType is set via nsIWritablePropertyBag (legacy)
+ util.usePromptService = false;
+ util.useBrowsingContext = false;
+ util.useAsync = false;
+ await run();
+ }
+}
+
+class PromptTestUtil {
+ constructor(window) {
+ this.window = window;
+ this.browsingContext =
+ SpecialPowers.wrap(window).windowGlobalChild.browsingContext;
+ this.promptService = SpecialPowers.Services.prompt;
+ this.nsPrompt = Cc["@mozilla.org/prompter;1"]
+ .getService(Ci.nsIPromptFactory)
+ .getPrompt(window, Ci.nsIPrompt);
+
+ this.usePromptService = null;
+ this.useBrowsingContext = null;
+ this.useAsync = null;
+ this.modalType = null;
+ }
+
+ get _prompter() {
+ if (this.usePromptService) {
+ return this.promptService;
+ }
+ return this.nsPrompt;
+ }
+
+ async prompt(funcName, promptArgs) {
+ if (
+ this.useBrowsingContext == null ||
+ this.usePromptService == null ||
+ this.useAsync == null ||
+ this.modalType == null
+ ) {
+ throw new Error("Not initialized");
+ }
+ let args = [];
+ if (this.usePromptService) {
+ if (this.useBrowsingContext) {
+ if (this.useAsync) {
+ funcName = `async${funcName[0].toUpperCase()}${funcName.substring(
+ 1
+ )}`;
+ } else {
+ funcName += "BC";
+ }
+ args = [this.browsingContext, this.modalType];
+ } else {
+ args = [this.window];
+ }
+ } else {
+ let bag = this.nsPrompt.QueryInterface(Ci.nsIWritablePropertyBag2);
+ bag.setPropertyAsUint32("modalType", this.modalType);
+ }
+ // Append the prompt arguments
+ args = args.concat(promptArgs);
+
+ let interfaceName = this.usePromptService ? "Services.prompt" : "prompt";
+ ok(
+ this._prompter[funcName],
+ `${interfaceName} should have method ${funcName}.`
+ );
+
+ info(`Calling ${interfaceName}.${funcName}(${args})`);
+ let result = this._prompter[funcName](...args);
+ is(
+ this.useAsync,
+ result != null &&
+ result.constructor != null &&
+ result.constructor.name === "Promise",
+ "If method is async it should return a promise."
+ );
+
+ if (this.useAsync) {
+ let propBag = await result;
+ return propBag && propBagToObject(propBag);
+ }
+ return result;
+ }
+}
+
+function onloadPromiseFor(id) {
+ var iframe = document.getElementById(id);
+ return new Promise(resolve => {
+ iframe.addEventListener(
+ "load",
+ function (e) {
+ resolve(true);
+ },
+ { once: true }
+ );
+ });
+}
+
+/**
+ * Take an action on the next prompt that appears without checking the state in advance.
+ * This is useful when the action doesn't depend on which prompt is shown and you
+ * are expecting multiple prompts at once in an indeterminate order.
+ * If you know the state of the prompt you expect you should use `handlePrompt` instead.
+ * @param {object} action defining how to handle the prompt
+ * @returns {Promise} resolving with the prompt state.
+ */
+function handlePromptWithoutChecks(action) {
+ return new Promise(resolve => {
+ gChromeScript.addMessageListener("promptHandled", function handled(msg) {
+ gChromeScript.removeMessageListener("promptHandled", handled);
+ resolve(msg.promptState);
+ });
+ gChromeScript.sendAsyncMessage("handlePrompt", { action, modalType });
+ });
+}
+
+async function handlePrompt(state, action) {
+ let actualState = await handlePromptWithoutChecks(action);
+ checkPromptState(actualState, state);
+}
+
+function checkPromptState(promptState, expectedState) {
+ info(`checkPromptState: Expected: ${expectedState.msg}`);
+ // XXX check title? OS X has title in content
+ is(promptState.msg, expectedState.msg, "Checking expected message");
+
+ let isOldContentPrompt =
+ !promptState.isSubDialogPrompt &&
+ modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT;
+
+ if (isOldContentPrompt && !promptState.showCallerOrigin) {
+ ok(
+ promptState.titleHidden,
+ "The title should be hidden for content prompts opened with tab modal prompt."
+ );
+ } else if (
+ isOSX ||
+ promptState.isSubDialogPrompt ||
+ promptState.showCallerOrigin
+ ) {
+ ok(
+ !promptState.titleHidden,
+ "Checking title always visible on OS X or when opened with common dialog"
+ );
+ } else {
+ is(
+ promptState.titleHidden,
+ expectedState.titleHidden,
+ "Checking title visibility"
+ );
+ }
+ is(
+ promptState.textHidden,
+ expectedState.textHidden,
+ "Checking textbox visibility"
+ );
+ is(
+ promptState.passHidden,
+ expectedState.passHidden,
+ "Checking passbox visibility"
+ );
+ is(
+ promptState.checkHidden,
+ expectedState.checkHidden,
+ "Checking checkbox visibility"
+ );
+ is(promptState.checkMsg, expectedState.checkMsg, "Checking checkbox label");
+ is(promptState.checked, expectedState.checked, "Checking checkbox checked");
+ if (
+ modalType === Ci.nsIPrompt.MODAL_TYPE_WINDOW ||
+ (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB && tabSubDialogsEnabled)
+ ) {
+ is(
+ promptState.iconClass,
+ expectedState.iconClass,
+ "Checking expected icon CSS class"
+ );
+ }
+ is(promptState.textValue, expectedState.textValue, "Checking textbox value");
+ is(promptState.passValue, expectedState.passValue, "Checking passbox value");
+
+ if (expectedState.butt0Label) {
+ is(
+ promptState.butt0Label,
+ expectedState.butt0Label,
+ "Checking accept-button label"
+ );
+ }
+ if (expectedState.butt1Label) {
+ is(
+ promptState.butt1Label,
+ expectedState.butt1Label,
+ "Checking cancel-button label"
+ );
+ }
+ if (expectedState.butt2Label) {
+ is(
+ promptState.butt2Label,
+ expectedState.butt2Label,
+ "Checking extra1-button label"
+ );
+ }
+
+ // For prompts with a time-delay button.
+ if (expectedState.butt0Disabled) {
+ is(promptState.butt0Disabled, true, "Checking accept-button is disabled");
+ is(
+ promptState.butt1Disabled,
+ false,
+ "Checking cancel-button isn't disabled"
+ );
+ }
+
+ is(
+ promptState.defButton0,
+ expectedState.defButton == "button0",
+ "checking button0 default"
+ );
+ is(
+ promptState.defButton1,
+ expectedState.defButton == "button1",
+ "checking button1 default"
+ );
+ is(
+ promptState.defButton2,
+ expectedState.defButton == "button2",
+ "checking button2 default"
+ );
+
+ if (
+ isOSX &&
+ expectedState.focused &&
+ expectedState.focused.startsWith("button") &&
+ !promptState.infoRowHidden
+ ) {
+ is(
+ promptState.focused,
+ "infoBody",
+ "buttons don't focus on OS X, but infoBody does instead"
+ );
+ } else {
+ is(promptState.focused, expectedState.focused, "Checking focused element");
+ }
+
+ if (expectedState.hasOwnProperty("chrome")) {
+ is(
+ promptState.chrome,
+ expectedState.chrome,
+ "Dialog should be opened as chrome"
+ );
+ }
+ if (expectedState.hasOwnProperty("dialog")) {
+ is(
+ promptState.dialog,
+ expectedState.dialog,
+ "Dialog should be opened as a dialog"
+ );
+ }
+ if (expectedState.hasOwnProperty("chromeDependent")) {
+ is(
+ promptState.chromeDependent,
+ expectedState.chromeDependent,
+ "Dialog should be opened as dependent"
+ );
+ }
+ if (expectedState.hasOwnProperty("isWindowModal")) {
+ is(
+ promptState.isWindowModal,
+ expectedState.isWindowModal,
+ "Dialog should be modal"
+ );
+ }
+}
+
+function checkEchoedAuthInfo(expectedState, browsingContext) {
+ return SpecialPowers.spawn(
+ browsingContext,
+ [expectedState.user, expectedState.pass],
+ (expectedUser, expectedPass) => {
+ let doc = this.content.document;
+
+ // The server echos back the HTTP auth info it received.
+ let username = doc.getElementById("user").textContent;
+ let password = doc.getElementById("pass").textContent;
+ let authok = doc.getElementById("ok").textContent;
+
+ Assert.equal(authok, "PASS", "Checking for successful authentication");
+ Assert.equal(username, expectedUser, "Checking for echoed username");
+ Assert.equal(password, expectedPass, "Checking for echoed password");
+ }
+ );
+}
+
+/**
+ * Create a Proxy to relay method calls on an nsIAuthPrompt[2] prompter to a chrome script which can
+ * perform the calls in the parent. Out and inout params will be copied back from the parent to
+ * content.
+ *
+ * @param chromeScript The reference to the chrome script that will listen to `proxyPrompter`
+ * messages in the parent and call the `methodName` method.
+ * The return value from the message handler should be an object with properties:
+ * `rv` - containing the return value of the method call.
+ * `args` - containing the array of arguments passed to the method since out or inout ones could have
+ * been modified.
+ */
+function PrompterProxy(chromeScript) {
+ return new Proxy(
+ {},
+ {
+ get(target, prop, receiver) {
+ return (...args) => {
+ // Array of indices of out/inout params to copy from the parent back to the caller.
+ let outParams = [];
+
+ switch (prop) {
+ case "prompt": {
+ outParams = [/* result */ 5];
+ break;
+ }
+ case "promptAuth": {
+ outParams = [];
+ break;
+ }
+ case "promptPassword":
+ case "asyncPromptPassword": {
+ outParams = [/* pwd */ 4];
+ break;
+ }
+ case "promptUsernameAndPassword":
+ case "asyncPromptUsernameAndPassword": {
+ outParams = [/* user */ 4, /* pwd */ 5];
+ break;
+ }
+ default: {
+ throw new Error("Unknown nsIAuthPrompt method");
+ }
+ }
+
+ let result;
+ chromeScript
+ .sendQuery("proxyPrompter", {
+ args,
+ methodName: prop,
+ })
+ .then(val => {
+ result = val;
+ });
+ SpecialPowers.Services.tm.spinEventLoopUntil(
+ "Test(prompt_common.js:get)",
+ () => result
+ );
+
+ for (let outParam of outParams) {
+ // Copy the out or inout param value over the original
+ args[outParam].value = result.args[outParam].value;
+ }
+
+ if (prop == "promptAuth") {
+ args[2].username = result.args[2].username;
+ args[2].password = result.args[2].password;
+ args[2].domain = result.args[2].domain;
+ }
+
+ return result.rv;
+ };
+ },
+ }
+ );
+}
diff --git a/toolkit/components/prompts/test/test_bug619644.html b/toolkit/components/prompts/test/test_bug619644.html
new file mode 100644
index 0000000000..2b424c71a6
--- /dev/null
+++ b/toolkit/components/prompts/test/test_bug619644.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=619644
+-->
+<head>
+ <title>Test for Bug 619644</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=619644">Mozilla Bug 619644</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+// This is a little yucky, but it works
+// The contents of bug619644_inner.html
+const expectedFinalDoc =
+"<head><\/head><body><p>Original content<\/p>\n<script>\n window.opener.postMessage(\"\", \"*\");\n confirm(\"Message\");\n document.write(\"Extra content\");\n window.opener.postMessage(document.documentElement.innerHTML, \"*\");\n<\/script>Extra content<\/body>";
+
+inittest();
+
+var promptDone;
+
+function inittest() {
+ window.addEventListener("message", runtest);
+ window.open("bug619644_inner.html", "619644");
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+function runtest(e) {
+ modalType = Ci.nsIPrompt.MODAL_TYPE_CONTENT;
+
+ window.removeEventListener("message", runtest);
+ window.addEventListener("message", checktest);
+
+ let state = {
+ msg: "Message",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ let action = {
+ buttonClick: "ESC",
+ };
+
+ promptDone = handlePrompt(state, action);
+}
+
+function checktest(e) {
+ is(e.data, expectedFinalDoc, "ESC press should not abort document load");
+ e.source.close();
+ promptDone.then(endtest);
+}
+
+function endtest() {
+ info("Ending test");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_bug620145.html b/toolkit/components/prompts/test/test_bug620145.html
new file mode 100644
index 0000000000..3894a528ae
--- /dev/null
+++ b/toolkit/components/prompts/test/test_bug620145.html
@@ -0,0 +1,96 @@
+<html>
+<head>
+ <title>Test for Bug 620145</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=620145">Mozilla Bug 620145</a>
+<pre id="test">
+</pre>
+
+<div id="text" style="max-width: 100px" onmouseup="openAlert()">
+ This is a short piece of text used for testing that mouse selecting is
+ stopped when an alert appears.
+</div>
+<div id="text2" style="max-width: 100px">
+ This is another short piece of text used for testing that mouse selecting is
+ stopped when an alert appears.
+</div>
+<button id="button" onmouseup="openAlert()">Button</button>
+
+<script class="testbody" type="text/javascript">
+
+function openAlert() {
+ info("opening alert...");
+ alert("hello!");
+ info("...alert done.");
+}
+
+add_task(async function runTest() {
+ var state, action;
+ // The <button> in this test's HTML opens a prompt when clicked.
+ // Here we send the events to simulate clicking it.
+ modalType = Ci.nsIPrompt.MODAL_TYPE_CONTENT;
+
+ state = {
+ msg: "hello!",
+ iconClass: "alert-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ let promptDone = handlePrompt(state, action);
+
+ var button = $("button");
+ dispatchMouseEvent(button, "mousedown");
+ dispatchMouseEvent(button, "mouseup");
+ // alert appears at this point, to be closed by the chrome script.
+
+ await promptDone;
+ checkSelection();
+
+ // using same state and action.
+ promptDone = handlePrompt(state, action);
+
+ var text = $("text");
+ dispatchMouseEvent(text, "mousedown");
+ dispatchMouseEvent(text, "mouseup");
+ // alert appears at this point, to be closed by the chrome script.
+
+ await promptDone;
+ checkSelection();
+});
+
+function dispatchMouseEvent(target, type) {
+ var win = target.ownerDocument.defaultView;
+ let e = document.createEvent("MouseEvent");
+ e.initEvent(type, false, false, win, 0, 1, 1, 1, 1,
+ false, false, false, false, 0, null);
+ var utils = SpecialPowers.getDOMWindowUtils(win);
+ utils.dispatchDOMEventViaPresShellForTesting(target, e);
+ ok(true, type + " sent to " + target.id);
+}
+
+function checkSelection() {
+ synthesizeMouse($("text"), 25, 55, { type: "mousemove" });
+ is(window.getSelection().toString(), "", "selection not made");
+}
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_dom_prompts.html b/toolkit/components/prompts/test/test_dom_prompts.html
new file mode 100644
index 0000000000..95595b8df2
--- /dev/null
+++ b/toolkit/components/prompts/test/test_dom_prompts.html
@@ -0,0 +1,207 @@
+<html>
+<head>
+ <title>Test for DOM prompts</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+var rv;
+var state, action;
+modalType = Ci.nsIPrompt.MODAL_TYPE_CONTENT;
+
+add_task(async function test_alert_ok() {
+ info("Starting test: Alert");
+ state = {
+ msg: "This is the alert text.",
+ iconClass: "alert-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ let promptDone = handlePrompt(state, action);
+
+ alert("This is the alert text.");
+
+ await promptDone;
+});
+
+// bug 861605 made the arguments to alert/confirm optional (prompt already was).
+add_task(async function test_alert_noargs() {
+ info("Starting test: Alert with no args");
+ state = {
+ msg: "",
+ iconClass: "alert-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ let promptDone = handlePrompt(state, action);
+
+ try {
+ alert();
+ ok(true, "alert() without arguments should not throw!");
+ } catch (e) {
+ ok(false, "alert() without arguments should not throw!");
+ }
+
+ await promptDone;
+});
+
+add_task(async function test_confirm_ok() {
+ info("Starting test: Confirm");
+ state = {
+ msg: "This is the confirm text.",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ let promptDone = handlePrompt(state, action);
+
+ rv = confirm("This is the confirm text.");
+ is(rv, true, "check prompt return value");
+
+ await promptDone;
+});
+
+// bug 861605 made the arguments to alert/confirm optional (prompt already was).
+add_task(async function test_confirm_noargs() {
+ info("Starting test: Confirm with no args");
+ state = {
+ msg: "",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ let promptDone = handlePrompt(state, action);
+
+ try {
+ rv = confirm();
+ ok(true, "confirm() without arguments should not throw!");
+ } catch (e) {
+ ok(false, "confirm() without arguments should not throw!");
+ }
+ is(rv, true, "check prompt return value");
+
+ await promptDone;
+});
+
+
+add_task(async function test_prompt_ok() {
+ info("Starting test: Prompt");
+ state = {
+ msg: "This is the Prompt text.",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ let promptDone = handlePrompt(state, action);
+
+ rv = prompt("This is the Prompt text.");
+ is(rv, "", "check prompt return value");
+
+ await promptDone;
+});
+
+// bug 861605 made the arguments to alert/confirm optional (prompt already was).
+add_task(async function test_prompt_noargs() {
+ info("Starting test: Prompt with no args");
+ state = {
+ msg: "",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ let promptDone = handlePrompt(state, action);
+
+ try {
+ rv = prompt();
+ ok(true, "prompt() without arguments should not throw!");
+ } catch (e) {
+ ok(false, "prompt() without arguments should not throw!");
+ }
+ is(rv, "", "check prompt return value");
+
+ await promptDone;
+});
+
+</script>
+
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_modal_prompts.html b/toolkit/components/prompts/test/test_modal_prompts.html
new file mode 100644
index 0000000000..df3438dbba
--- /dev/null
+++ b/toolkit/components/prompts/test/test_modal_prompts.html
@@ -0,0 +1,1311 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Modal Prompts Test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+</head>
+<body>
+Prompter tests: modal prompts
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* eslint-disable complexity */
+async function runTests(util) {
+ const { NetUtil } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/NetUtil.jsm"
+ );
+
+ // The ConfirmEx + delay test has slightly different behavior with the focus
+ // fixup rule vs. without.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.focus.fixup", true]],
+ });
+
+ let state, action, promptDone;
+
+ let checkVal = {};
+ let textVal = {};
+ let passVal = {};
+ let flags;
+ let isOK;
+
+ // =====
+ info("Starting test: Alert");
+ state = {
+ msg: "This is the alert text.",
+ title: "TestTitle",
+ iconClass: "alert-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the alert text."];
+ await util.prompt("alert", promptArgs);
+
+ await promptDone;
+
+ // =====
+ info("Starting test: AlertCheck (null checkbox label, so it's hidden)");
+ state = {
+ msg: "This is the alertCheck text.",
+ title: "TestTitle",
+ iconClass: "alert-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = [
+ "TestTitle",
+ "This is the alertCheck text.",
+ null,
+ util.useAsync ? false : {},
+ ];
+ util.prompt("alertCheck", promptArgs);
+
+ await promptDone;
+
+ // =====
+ info("Starting test: AlertCheck");
+ state = {
+ msg: "This is the alertCheck text.",
+ title: "TestTitle",
+ iconClass: "alert-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: false,
+ textValue: "",
+ passValue: "",
+ checkMsg: "Check me out!",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ checkVal.value = false;
+ promptArgs = [
+ "TestTitle",
+ "This is the alertCheck text.",
+ "Check me out!",
+ util.useAsync ? checkVal.value : checkVal,
+ ];
+ let result = await util.prompt("alertCheck", promptArgs);
+ is(
+ util.useAsync ? result.checked : checkVal.value,
+ true,
+ "checkbox was checked"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: Confirm (ok)");
+ state = {
+ msg: "This is the confirm text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the confirm text."];
+ result = await util.prompt("confirm", promptArgs);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+
+ await promptDone;
+
+ // =====
+ info("Starting test: Confirm (cancel)");
+ state = {
+ msg: "This is the confirm text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the confirm text."];
+ result = await util.prompt("confirm", promptArgs);
+ is(util.useAsync ? result.ok : result, false, "checked expected retval");
+
+ await promptDone;
+
+ // =====
+ info("Starting test: ConfirmCheck (ok, null checkbox label)");
+ state = {
+ msg: "This is the confirmCheck text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmCheck text.",
+ null,
+ util.useAsync ? false : {},
+ ];
+ result = await util.prompt("confirmCheck", promptArgs);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+
+ await promptDone;
+
+ // =====
+ info("Starting test: ConfirmCheck (cancel, null checkbox label)");
+ state = {
+ msg: "This is the confirmCheck text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmCheck text.",
+ null,
+ util.useAsync ? false : {},
+ ];
+ result = await util.prompt("confirmCheck", promptArgs);
+ is(util.useAsync ? result.ok : result, false, "checked expected retval");
+
+ await promptDone;
+
+ // =====
+ info("Starting test: ConfirmCheck (ok)");
+ state = {
+ msg: "This is the confirmCheck text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: false,
+ textValue: "",
+ passValue: "",
+ checkMsg: "Check me out!",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ checkVal.value = false;
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmCheck text.",
+ "Check me out!",
+ util.useAsync ? checkVal.value : checkVal,
+ ];
+ result = await util.prompt("confirmCheck", promptArgs);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+ is(
+ util.useAsync ? result.checked : checkVal.value,
+ true,
+ "expected checkbox setting"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: ConfirmCheck (cancel)");
+ state = {
+ msg: "This is the confirmCheck text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: false,
+ textValue: "",
+ passValue: "",
+ checkMsg: "Check me out!",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ checkVal.value = false;
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmCheck text.",
+ "Check me out!",
+ util.useAsync ? checkVal.value : checkVal,
+ ];
+ result = await util.prompt("confirmCheck", promptArgs);
+ is(util.useAsync ? result.ok : result, false, "checked expected retval");
+ is(
+ util.useAsync ? result.checked : checkVal.value,
+ true,
+ "expected checkbox setting"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: Prompt (ok, no default text)");
+ state = {
+ msg: "This is the prompt text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "bacon",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "";
+ promptArgs = ["TestTitle", "This is the prompt text.", util.useAsync ? textVal.value : textVal , null, util.useAsync ? false : {}];
+ result = await util.prompt("prompt", promptArgs);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+ is(
+ util.useAsync ? result.value : textVal.value,
+ "bacon",
+ "checking expected text value"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: Prompt (ok, default text)");
+ state = {
+ msg: "This is the prompt text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "kittens",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "kittens";
+ promptArgs = ["TestTitle", "This is the prompt text.", util.useAsync ? textVal.value : textVal, null, util.useAsync ? false : {}];
+ result = await util.prompt("prompt", promptArgs);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+ is(
+ util.useAsync ? result.value : textVal.value,
+ "kittens",
+ "checking expected text value"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: Prompt (cancel, default text)");
+ state = {
+ msg: "This is the prompt text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "puppies",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "puppies";
+ promptArgs = ["TestTitle", "This is the prompt text.", util.useAsync ? textVal.value : textVal, null, util.useAsync ? false : {}];
+ result = await util.prompt("prompt", promptArgs);
+ is(util.useAsync ? result.ok : result, false, "checked expected retval");
+ is(
+ util.useAsync ? result.value : textVal.value,
+ "puppies",
+ "checking expected text value"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: Prompt (cancel, default text modified)");
+ state = {
+ msg: "This is the prompt text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "puppies",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ textField: "bacon",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "puppies";
+ promptArgs = ["TestTitle", "This is the prompt text.", util.useAsync ? textVal.value : textVal, null, util.useAsync ? false : {}];
+ result = await util.prompt("prompt", promptArgs);
+ is(util.useAsync ? result.ok : result, false, "checked expected retval");
+ is(
+ util.useAsync ? result.value : textVal.value,
+ "puppies",
+ "checking expected text value"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: Prompt (ok, with checkbox)");
+ state = {
+ msg: "This is the prompt text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: false,
+ textValue: "tribbles",
+ passValue: "",
+ checkMsg: "Check me out!",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "tribbles";
+ checkVal.value = false;
+ promptArgs = [
+ "TestTitle",
+ "This is the prompt text.",
+ util.useAsync ? textVal.value : textVal,
+ "Check me out!",
+ util.useAsync ? checkVal.value : checkVal,
+ ];
+ result = await util.prompt("prompt", promptArgs);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+ is(
+ util.useAsync ? result.value : textVal.value,
+ "tribbles",
+ "checking expected text value"
+ );
+ is(
+ util.useAsync ? result.checked : checkVal.value,
+ true,
+ "expected checkbox setting"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: Prompt (cancel, with checkbox)");
+ state = {
+ msg: "This is the prompt text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: true,
+ checkHidden: false,
+ textValue: "tribbles",
+ passValue: "",
+ checkMsg: "Check me out!",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ setCheckbox: true,
+ };
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "tribbles";
+ checkVal.value = false;
+ promptArgs = [
+ "TestTitle",
+ "This is the prompt text.",
+ util.useAsync ? textVal.value : textVal,
+ "Check me out!",
+ util.useAsync ? checkVal.value : checkVal,
+ ];
+ result = await util.prompt("prompt", promptArgs);
+ is(util.useAsync ? result.ok : result, false, "checked expected retval");
+ is(
+ util.useAsync ? result.value : textVal.value,
+ "tribbles",
+ "checking expected text value"
+ );
+ ok(
+ util.useAsync ? result.checked : !checkVal.value,
+ "expected checkbox setting"
+ );
+
+ await promptDone;
+
+ // =====
+ // Just two tests for this, since password manager already tests this extensively.
+ info("Starting test: PromptUsernameAndPassword (ok)");
+ state = {
+ msg: "This is the pUAP text.",
+ title: "TestTitle",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ textValue: "usr",
+ passValue: "ssh",
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "newusr",
+ passField: "newssh",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "usr";
+ passVal.value = "ssh";
+ promptArgs = [
+ "TestTitle",
+ "This is the pUAP text.",
+ util.useAsync ? textVal.value : textVal,
+ util.useAsync ? passVal.value : passVal
+ ];
+ result = await util.prompt("promptUsernameAndPassword", promptArgs);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+ is(
+ util.useAsync ? result.user : textVal.value,
+ "newusr",
+ "checking expected text value"
+ );
+ is(
+ util.useAsync ? result.pass : passVal.value,
+ "newssh",
+ "checking expected pass value"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: PromptUsernameAndPassword (cancel)");
+ state = {
+ msg: "This is the pUAP text.",
+ title: "TestTitle",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ textValue: "usr",
+ passValue: "ssh",
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ textField: "newusr",
+ passField: "newssh",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ textVal.value = "usr";
+ passVal.value = "ssh";
+ promptArgs = [
+ "TestTitle",
+ "This is the pUAP text.",
+ util.useAsync ? textVal.value : textVal,
+ util.useAsync ? passVal.value : passVal
+ ];
+ result = await util.prompt("promptUsernameAndPassword", promptArgs);
+ is(util.useAsync ? result.ok : result, false, "checked expected retval");
+ ok(
+ (util.useAsync && result.user == "newusr") || textVal.value == "usr",
+ "checking expected text value"
+ );
+ ok(
+ (util.useAsync && result.pass == "newpass") || passVal.value == "ssh",
+ "checking expected pass value"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: PromptPassword (ok)");
+ state = {
+ msg: "This is the promptPassword text.",
+ title: "TestTitle",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ textValue: "",
+ passValue: "ssh",
+ focused: "passField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ passField: "newssh",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ passVal.value = "ssh";
+ promptArgs = [
+ "TestTitle",
+ "This is the promptPassword text.",
+ util.useAsync ? passVal.value : passVal
+ ];
+ result = await util.prompt("promptPassword", promptArgs);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+ is(
+ util.useAsync ? result.pass : passVal.value,
+ "newssh",
+ "checking expected pass value"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: PromptPassword (cancel)");
+ state = {
+ msg: "This is the promptPassword text.",
+ title: "TestTitle",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ textValue: "",
+ passValue: "ssh",
+ focused: "passField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "cancel",
+ passField: "newssh",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ passVal.value = "ssh";
+ promptArgs = [
+ "TestTitle",
+ "This is the promptPassword text.",
+ util.useAsync ? passVal.value : passVal
+ ];
+ result = await util.prompt("promptPassword", promptArgs);
+ is(util.useAsync ? result.ok : result, false, "checked expected retval");
+ ok(
+ (util.useAsync && result.pass == "newssh") || passVal.value == "ssh",
+ "checking expected pass value"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (ok/cancel, ok)");
+ state = {
+ msg: "This is the confirmEx text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ butt0Label: "OK",
+ butt1Label: "Cancel",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ flags = Ci.nsIPromptService.STD_OK_CANCEL_BUTTONS;
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmEx text.",
+ flags,
+ null,
+ null,
+ null,
+ null,
+ util.useAsync ? false : {},
+ ];
+ result = await util.prompt("confirmEx", promptArgs);
+ is(
+ util.useAsync ? result.buttonNumClicked : result,
+ 0,
+ "checked expected button num click"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (yes/no, cancel)");
+ state = {
+ msg: "This is the confirmEx text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ butt0Label: "Yes",
+ butt1Label: "No",
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ flags = Ci.nsIPromptService.STD_YES_NO_BUTTONS;
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmEx text.",
+ flags,
+ null,
+ null,
+ null,
+ null,
+ util.useAsync ? false : {},
+ ];
+ result = await util.prompt("confirmEx", promptArgs);
+ is(
+ util.useAsync ? result.buttonNumClicked : result,
+ 1,
+ "checked expected button num click"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (buttons from args, checkbox, ok)");
+ state = {
+ msg: "This is the confirmEx text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: false,
+ textValue: "",
+ passValue: "",
+ checkMsg: "Check me out!",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ butt0Label: "butt0",
+ butt1Label: "butt1",
+ butt2Label: "butt2",
+ };
+ action = {
+ buttonClick: "ok",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ let b = Ci.nsIPromptService.BUTTON_TITLE_IS_STRING;
+ flags =
+ b * Ci.nsIPromptService.BUTTON_POS_2 +
+ b * Ci.nsIPromptService.BUTTON_POS_1 +
+ b * Ci.nsIPromptService.BUTTON_POS_0;
+ checkVal.value = false;
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmEx text.",
+ flags,
+ "butt0",
+ "butt1",
+ "butt2",
+ "Check me out!",
+ util.useAsync ? checkVal.value : checkVal,
+ ];
+ result = await util.prompt("confirmEx", promptArgs);
+ is(
+ util.useAsync ? result.buttonNumClicked : result,
+ 0,
+ "checked expected button num click"
+ );
+ is(
+ util.useAsync ? result.checked : checkVal.value,
+ true,
+ "expected checkbox setting"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (buttons from args, checkbox, cancel)");
+ state = {
+ msg: "This is the confirmEx text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: false,
+ textValue: "",
+ passValue: "",
+ checkMsg: "Check me out!",
+ checked: false,
+ focused: "button1", // Default changed!
+ defButton: "button1",
+ butt0Label: "butt0",
+ butt1Label: "butt1",
+ butt2Label: "butt2",
+ };
+ action = {
+ buttonClick: "cancel",
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ b = Ci.nsIPromptService.BUTTON_TITLE_IS_STRING;
+ flags =
+ b * Ci.nsIPromptService.BUTTON_POS_2 +
+ b * Ci.nsIPromptService.BUTTON_POS_1 +
+ b * Ci.nsIPromptService.BUTTON_POS_0;
+ flags ^= Ci.nsIPromptService.BUTTON_POS_1_DEFAULT;
+ checkVal.value = false;
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmEx text.",
+ flags,
+ "butt0",
+ "butt1",
+ "butt2",
+ "Check me out!",
+ util.useAsync ? checkVal.value : checkVal,
+ ];
+ result = await util.prompt("confirmEx", promptArgs);
+ is(
+ util.useAsync ? result.buttonNumClicked : result,
+ 1,
+ "checked expected button num click"
+ );
+ is(
+ util.useAsync ? result.checked : checkVal.value,
+ true,
+ "expected checkbox setting"
+ );
+
+ await promptDone;
+
+ // =====
+ info("Starting test: ConfirmEx (buttons from args, checkbox, button3)");
+ state = {
+ msg: "This is the confirmEx text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: false,
+ textValue: "",
+ passValue: "",
+ checkMsg: "Check me out!",
+ checked: false,
+ focused: "button2", // Default changed!
+ defButton: "button2",
+ butt0Label: "butt0",
+ butt1Label: "butt1",
+ butt2Label: "butt2",
+ };
+ action = {
+ buttonClick: 2,
+ setCheckbox: true,
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ b = Ci.nsIPromptService.BUTTON_TITLE_IS_STRING;
+ flags =
+ b * Ci.nsIPromptService.BUTTON_POS_2 +
+ b * Ci.nsIPromptService.BUTTON_POS_1 +
+ b * Ci.nsIPromptService.BUTTON_POS_0;
+ flags ^= Ci.nsIPromptService.BUTTON_POS_2_DEFAULT;
+ checkVal.value = false;
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmEx text.",
+ flags,
+ "butt0",
+ "butt1",
+ "butt2",
+ "Check me out!",
+ util.useAsync ? checkVal.value : checkVal,
+ ];
+ result = await util.prompt("confirmEx", promptArgs);
+ is(
+ util.useAsync ? result.buttonNumClicked : result,
+ 2,
+ "checked expected button num click"
+ );
+ is(
+ util.useAsync ? result.checked : checkVal.value,
+ true,
+ "expected checkbox setting"
+ );
+
+ await promptDone;
+
+ // =====
+ // (skipped for E10S and tabmodal tests: window is required)
+ info("Starting test: Alert, no window");
+ state = {
+ msg: "This is the alert text.",
+ title: "TestTitle",
+ iconClass: "alert-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ if (util.modalType === Ci.nsIPrompt.MODAL_TYPE_WINDOW && !isE10S) {
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = ["TestTitle", "This is the alert text."];
+ await util.prompt("alert", promptArgs);
+
+ await promptDone;
+ }
+
+ // =====
+ // (skipped for tabmodal tests: delay not supported)
+ info("Starting test: ConfirmEx (delay, ok)");
+ state = {
+ msg: "This is the confirmEx delay text.",
+ title: "TestTitle",
+ iconClass: "question-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: null, // Nothing focused until the delay triggers.
+ defButton: "button0",
+ butt0Label: "OK",
+ butt1Label: "Cancel",
+ butt0Disabled: true,
+ };
+
+ if (isOSX) {
+ // OS X doesn't initially focus the button, but rather the infoBody.
+ // The focus stays there even after the button-enable delay has fired.
+ state.focused = "infoBody";
+ }
+
+ action = {
+ buttonClick: "pollOK",
+ };
+ if (util.modalType === Ci.nsIPrompt.MODAL_TYPE_WINDOW) {
+ promptDone = handlePrompt(state, action);
+
+ flags =
+ Ci.nsIPromptService.STD_OK_CANCEL_BUTTONS |
+ Ci.nsIPromptService.BUTTON_DELAY_ENABLE;
+ promptArgs = [
+ "TestTitle",
+ "This is the confirmEx delay text.",
+ flags,
+ null,
+ null,
+ null,
+ null,
+ util.useAsync ? false : {},
+ ];
+ result = await util.prompt("confirmEx", promptArgs);
+ is(
+ util.useAsync ? result.buttonNumClicked : result,
+ 0,
+ "checked expected button num click"
+ );
+
+ await promptDone;
+ }
+
+ // promptAuth already tested via password manager but do a few specific things here.
+ var channel = NetUtil.newChannel({
+ uri: "http://example.com",
+ loadUsingSystemPrincipal: true,
+ });
+
+ var level = Ci.nsIAuthPrompt2.LEVEL_NONE;
+ var authinfo = {
+ username: "",
+ password: "",
+ domain: "",
+ flags: Ci.nsIAuthInformation.AUTH_HOST,
+ authenticationScheme: "basic",
+ realm: "",
+ };
+
+ let msg =
+ util.modalType == Ci.nsIPrompt.MODAL_TYPE_TAB
+ ? "This site is asking you to sign in."
+ : "http://example.com is requesting your username and password.";
+ // =====
+ // (promptAuth is only accessible from the prompt service)
+ info("Starting test: promptAuth with empty realm");
+ state = {
+ msg,
+ title: "TestTitle",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ textValue: "",
+ passValue: "",
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "username",
+ passField: "password",
+ };
+ if (util.usePromptService && !util.useAsync) {
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = [channel, level, authinfo];
+ isOK = await util.prompt("promptAuth", promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(authinfo.username, "username", "checking filled username");
+ is(authinfo.password, "password", "checking filled password");
+
+ await promptDone;
+ }
+
+ // =====
+ // (promptAuth is only accessible from the prompt service)
+ msg =
+ util.modalType == Ci.nsIPrompt.MODAL_TYPE_TAB
+ ? "This site is asking you to sign in."
+ : "http://example.com is requesting your username and password. The site " +
+ "says: \u201cabcdefghi abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi " +
+ "abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi abcdefghi " +
+ "abcdefghi \u2026\u201d";
+
+ info("Starting test: promptAuth with long realm");
+ state = {
+ msg,
+ title: "TestTitle",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ textValue: "",
+ passValue: "",
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "username",
+ passField: "password",
+ };
+ if (util.usePromptService && !util.useAsync) {
+ promptDone = handlePrompt(state, action);
+
+ var longString = "";
+ for (var i = 0; i < 20; i++) longString += "abcdefghi "; // 200 chars long
+ authinfo.realm = longString;
+ authinfo.username = "";
+ authinfo.password = "";
+ promptArgs = [channel, level, authinfo];
+ isOK = await util.prompt("promptAuth", promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(authinfo.username, "username", "checking filled username");
+ is(authinfo.password, "password", "checking filled password");
+
+ await promptDone;
+ }
+
+ msg =
+ util.modalType == Ci.nsIPrompt.MODAL_TYPE_TAB
+ ? ("This site is asking you to sign in. Warning: Your login information " +
+ "will be shared with example.com, not the website you are currently visiting.")
+ : ("http://example.com is requesting your username and password. " +
+ "WARNING: Your password will not be sent to the website you are currently visiting!");
+ info("Starting test: promptAuth for a cross-origin and a empty realm");
+ authinfo = {
+ username: "",
+ password: "",
+ domain: "",
+ flags:
+ Ci.nsIAuthInformation.AUTH_HOST |
+ Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE,
+ authenticationScheme: "basic",
+ realm: "",
+ };
+ state = {
+ msg,
+ title: "TestTitle",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ textValue: "",
+ passValue: "",
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "username",
+ passField: "password",
+ };
+ if (util.usePromptService && !util.useAsync) {
+ promptDone = handlePrompt(state, action);
+ promptArgs = [channel, level, authinfo];
+ isOK = await util.prompt("promptAuth", promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(authinfo.username, "username", "checking filled username");
+ is(authinfo.password, "password", "checking filled password");
+
+ await promptDone;
+ }
+
+ info("Starting test: promptAuth for a cross-origin with realm");
+ authinfo = {
+ username: "",
+ password: "",
+ domain: "",
+ flags:
+ Ci.nsIAuthInformation.AUTH_HOST |
+ Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE,
+ authenticationScheme: "basic",
+ realm: "Something!!!",
+ };
+ state = {
+ msg, // Same as previous test, see above.
+ title: "TestTitle",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ textValue: "",
+ passValue: "",
+ focused: "textField",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ textField: "username",
+ passField: "password",
+ };
+ if (util.usePromptService && !util.useAsync) {
+ promptDone = handlePrompt(state, action);
+
+ promptArgs = [channel, level, authinfo];
+ isOK = await util.prompt("promptAuth", promptArgs);
+ is(isOK, true, "checked expected retval");
+ is(authinfo.username, "username", "checking filled username");
+ is(authinfo.password, "password", "checking filled password");
+
+ await promptDone;
+ }
+}
+
+let promptArgs;
+
+add_task(async function runPromptTests() {
+ await runPromptCombinations(window, runTests);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_modal_select.html b/toolkit/components/prompts/test/test_modal_select.html
new file mode 100644
index 0000000000..27688cf329
--- /dev/null
+++ b/toolkit/components/prompts/test/test_modal_select.html
@@ -0,0 +1,138 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Modal Prompts Test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+</head>
+<body>
+Prompter tests: modal prompts
+<p id="display"></p>
+
+<div id="content" style="display: none">
+ <iframe id="iframe"></iframe>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+function checkPromptState(promptState, expectedState) {
+ // XXX check title? OS X has title in content
+ // XXX check focused element
+ // XXX check button labels?
+
+ is(promptState.msg, expectedState.msg, "Checking expected message");
+
+ // Compare listbox contents
+ is(promptState.items.length, expectedState.items.length, "Checking listbox length");
+
+ if (promptState.items.length)
+ is(promptState.selectedIndex, 0, "Checking selected index");
+
+ for (let i = 0; i < promptState.items; i++) {
+ is(promptState.items[i], expectedState.items[i], "Checking list item #" + i);
+ }
+}
+
+let selectVal = {};
+let isOK;
+
+function handlePrompt(state, action) {
+ return new Promise(resolve => {
+ gChromeScript.addMessageListener("promptHandled", function handled(msg) {
+ gChromeScript.removeMessageListener("promptHandled", handled);
+ checkPromptState(msg.promptState, state);
+ resolve(true);
+ });
+ gChromeScript.sendAsyncMessage("handlePrompt", { action, isSelect: true});
+ });
+}
+
+async function runTests(util) {
+ // Select prompt does not support tab or content prompts yet. See Bug 1622817.
+ if(util.modalType != Ci.nsIPrompt.MODAL_TYPE_WINDOW) {
+ info('Skipping modal type for select prompt...');
+ return;
+ }
+
+ // Empty list
+ info("Starting test: Select (0 items, ok)");
+ let state = {
+ msg: "This is the select text.",
+ title: "TestTitle",
+ items: [],
+ };
+ let action = {
+ buttonClick: "ok",
+ };
+ let promptDone = handlePrompt(state, action);
+ let items = [];
+ selectVal.value = null; // outparam, just making sure.
+ let result = await util.prompt("select", ["TestTitle", "This is the select text.", items, util.useAsync ? false : selectVal]);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+ is(util.useAsync ? result.selected : selectVal.value, -1, "checking selected index");
+ await promptDone;
+
+ // ok
+ info("Starting test: Select (3 items, ok)");
+ state = {
+ msg: "This is the select text.",
+ title: "TestTitle",
+ items: ["one", "two", "three"],
+ };
+ action = {
+ buttonClick: "ok",
+ };
+ promptDone = handlePrompt(state, action);
+ items = ["one", "two", "three"];
+ selectVal.value = null; // outparam, just making sure.
+ result = await util.prompt("select", ["TestTitle", "This is the select text.", items, util.useAsync ? false : selectVal]);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+ is(util.useAsync ? result.selected : selectVal.value, 0, "checking selected index");
+ await promptDone;
+
+ // select item
+ info("Starting test: Select (3 items, selection changed, ok)");
+ state = {
+ msg: "This is the select text.",
+ title: "TestTitle",
+ items: ["one", "two", "three"],
+ };
+ action = {
+ buttonClick: "ok",
+ selectItem: 1,
+ };
+ promptDone = handlePrompt(state, action);
+ items = ["one", "two", "three"];
+ selectVal.value = null; // outparam, just making sure.
+ result = await util.prompt("select", ["TestTitle", "This is the select text.", items, util.useAsync ? false : selectVal]);
+ is(util.useAsync ? result.ok : result, true, "checked expected retval");
+ is(util.useAsync ? result.selected : selectVal.value, 1, "checking selected index");
+ await promptDone;
+
+ // cancel prompt
+ info("Starting test: Select (3 items, cancel)");
+ state = {
+ msg: "This is the select text.",
+ title: "TestTitle",
+ items: ["one", "two", "three"],
+ };
+ action = {
+ buttonClick: "cancel",
+ };
+ promptDone = handlePrompt(state, action);
+ items = ["one", "two", "three"];
+ selectVal.value = null; // outparam, just making sure.
+ result = await util.prompt("select", ["TestTitle", "This is the select text.", items, util.useAsync ? false : selectVal]);
+ is(util.useAsync ? result.ok : result, false, "checked expected retval");
+ ok(util.useAsync && result.selected == -1 || selectVal.value == 0, "checking selected index");
+ await promptDone;
+}
+
+add_task(async function runPromptTests() {
+ await runPromptCombinations(window, runTests);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/components/prompts/test/test_subresources_prompts.html b/toolkit/components/prompts/test/test_subresources_prompts.html
new file mode 100644
index 0000000000..b71ad0694e
--- /dev/null
+++ b/toolkit/components/prompts/test/test_subresources_prompts.html
@@ -0,0 +1,200 @@
+<html>
+<head>
+ <title>Test subresources prompts (Bug 625187 and bug 1230462)</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="prompt_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<!--
+ - Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/
+ -
+ - Contributor(s):
+ - Mihai Sucan <mihai.sucan@gmail.com>
+ -->
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=625187">Mozilla Bug 625187</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1230462">Mozilla Bug 1230462</a>
+
+<p><button id="topbutton" onclick="alert('hello world')">Button</button></p>
+
+<iframe id="iframe_diff_origin" src="http://example.com/tests/toolkit/components/prompts/test/bug625187_iframe.html"></iframe>
+
+<iframe id="iframe_same_origin" src="bug625187_iframe.html"></iframe>
+
+<iframe id="iframe_prompt"></iframe>
+
+<pre id="test"></pre>
+
+<script class="testbody" type="text/javascript">
+var iframe1Loaded = onloadPromiseFor("iframe_diff_origin");
+var iframe2Loaded = onloadPromiseFor("iframe_same_origin");
+var iframe_prompt = document.getElementById("iframe_prompt");
+
+// Depending on pref state we either show auth prompts as windows or on tab level.
+let authPromptModalType = SpecialPowers.Services.prefs.getIntPref(
+ "prompts.modalType.httpAuth"
+);
+
+add_task(async function runTest() {
+ modalType = Ci.nsIPrompt.MODAL_TYPE_CONTENT;
+
+ info("Ensuring iframe1 has loaded...");
+ await iframe1Loaded;
+ info("Ensuring iframe2 has loaded...");
+ await iframe2Loaded;
+ let state, action;
+
+ state = {
+ msg: "hello world",
+ iconClass: "alert-icon",
+ titleHidden: true,
+ textHidden: true,
+ passHidden: true,
+ checkHidden: true,
+ textValue: "",
+ passValue: "",
+ checkMsg: "",
+ checked: false,
+ focused: "button0",
+ defButton: "button0",
+ };
+ action = {
+ buttonClick: "ok",
+ };
+
+ let promptDone = handlePrompt(state, action);
+
+ await SpecialPowers.spawn(window, ["topbutton", "click"], dispatchMouseEvent);
+
+ await promptDone;
+
+ // mostly reusing same state/action
+ state.titleHidden = false;
+ state.msg = "hello world 2";
+ promptDone = handlePrompt(state, action);
+
+ var iframe = document.getElementById("iframe_diff_origin");
+ await SpecialPowers.spawn(iframe.contentWindow, ["btn1", "click"], dispatchMouseEvent);
+
+ await promptDone;
+
+ // mostly reusing same state/action
+ state.titleHidden = true;
+ state.msg = "hello world 2";
+ promptDone = handlePrompt(state, action);
+
+ iframe = document.getElementById("iframe_same_origin");
+ await SpecialPowers.spawn(iframe.contentWindow, ["btn1", "click"], dispatchMouseEvent);
+
+ await promptDone;
+
+ // mostly reusing same state/action
+ state.msg = "hello world 3";
+ promptDone = handlePrompt(state, action);
+ await SpecialPowers.spawn(iframe.contentWindow, ["btn2", "click"], dispatchMouseEvent);
+
+ await promptDone;
+});
+
+add_task(async function runTestAuth() {
+ // Following tests check prompt message for a cross-origin and not
+ // cross-origin subresources load
+
+ // Let prompt_common know what kind of modal type is enabled for auth prompts.
+ modalType = authPromptModalType;
+
+ let state, action;
+
+ state = {
+ msg: "This site is asking you to sign in.",
+ title: "Authentication Required",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+
+ action = {
+ buttonClick: "ok",
+ setCheckbox: false,
+ textField: "mochiuser1",
+ passField: "mochipass1",
+ };
+
+ let promptDone = handlePrompt(state, action);
+
+ var iframe3Loaded = onloadPromiseFor("iframe_prompt");
+ iframe_prompt.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1";
+ await promptDone;
+ await iframe3Loaded;
+ await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"},
+ iframe_prompt);
+
+ // Cross-origin subresourse test.
+ state = {
+ msg: "This site is asking you to sign in. Warning: Your login information " +
+ "will be shared with example.com, not the website you are currently visiting.",
+ title: "Authentication Required",
+ textValue: "",
+ passValue: "",
+ iconClass: "authentication-icon question-icon",
+ titleHidden: true,
+ textHidden: false,
+ passHidden: false,
+ checkHidden: true,
+ checkMsg: "",
+ checked: false,
+ focused: "textField",
+ defButton: "button0",
+ };
+
+ action = {
+ buttonClick: "ok",
+ setCheckbox: false,
+ textField: "mochiuser2",
+ passField: "mochipass2",
+ };
+
+ promptDone = handlePrompt(state, action);
+
+ iframe3Loaded = onloadPromiseFor("iframe_prompt");
+ iframe_prompt.src = "http://example.com/tests/toolkit/components/prompts/test/authenticate.sjs?user=mochiuser2&pass=mochipass2&realm=mochitest";
+ await promptDone;
+ await iframe3Loaded;
+ await checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"},
+ iframe_prompt);
+});
+
+/**
+ * Function to be passed to SpecialPowers.spawn that dispatches a MouseEvent
+ * of a certain type to some element in a subframe.
+ *
+ * @param {String} targetID The ID of the element that will have the event
+ * dispatched on.
+ * @param {String} type The type of MouseEvent.
+ * @returns Promise
+ * @resolves Once the event has been dispatched.
+ */
+async function dispatchMouseEvent(targetID, type) {
+ /* eslint-disable no-undef */
+ let document = content.document;
+ let element = document.getElementById(targetID);
+ let event = document.createEvent("MouseEvent");
+ event.initEvent(type, false, false, content, 0, 1, 1, 1, 1,
+ false, false, false, false, 0, null);
+ content.windowUtils.dispatchDOMEventViaPresShellForTesting(element, event);
+ /* eslint-enable no-undef */
+}
+</script>
+</body>
+</html>