diff options
Diffstat (limited to 'toolkit/components/prompts')
36 files changed, 6983 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..16442142a8 --- /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.import( + "resource://gre/modules/SharedPromptUtils.jsm" +); + +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" + ) { + Cu.reportError(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..f11d3c3785 --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.js @@ -0,0 +1,147 @@ +/* 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.import( + "resource://gre/modules/CommonDialog.jsm" +); + +// 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..ec6dec010a --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.xhtml @@ -0,0 +1,81 @@ +<?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..d079beb755 --- /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") { + Cu.reportError("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..877e0e7140 --- /dev/null +++ b/toolkit/components/prompts/content/selectDialog.xhtml @@ -0,0 +1,22 @@ +<?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..9d9147d05d --- /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 { + -moz-box-pack: center; + -moz-box-orient: vertical; +} + +.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.jsm b/toolkit/components/prompts/content/tabprompts.jsm new file mode 100644 index 0000000000..13a75f01a9 --- /dev/null +++ b/toolkit/components/prompts/content/tabprompts.jsm @@ -0,0 +1,307 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["TabModalPrompt"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +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.import( + "resource://gre/modules/CommonDialog.jsm" + ); + 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 Binary files differnew file mode 100644 index 0000000000..730fe7828c --- /dev/null +++ b/toolkit/components/prompts/docs/modal-type-content.png diff --git a/toolkit/components/prompts/docs/modal-type-tab.png b/toolkit/components/prompts/docs/modal-type-tab.png Binary files differnew file mode 100644 index 0000000000..66da8ef782 --- /dev/null +++ b/toolkit/components/prompts/docs/modal-type-tab.png diff --git a/toolkit/components/prompts/docs/modal-type-window.png b/toolkit/components/prompts/docs/modal-type-window.png Binary files differnew file mode 100644 index 0000000000..505850e8de --- /dev/null +++ b/toolkit/components/prompts/docs/modal-type-window.png 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..ed674bc43e --- /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.jsm (content/tabprompts.jsm) diff --git a/toolkit/components/prompts/moz.build b/toolkit/components/prompts/moz.build new file mode 100644 index 0000000000..17fb416f06 --- /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.jsm", +] + +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] +MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"] + +JAR_MANIFESTS += ["jar.mn"] diff --git a/toolkit/components/prompts/src/CommonDialog.jsm b/toolkit/components/prompts/src/CommonDialog.jsm new file mode 100644 index 0000000000..8081bb34eb --- /dev/null +++ b/toolkit/components/prompts/src/CommonDialog.jsm @@ -0,0 +1,386 @@ +/* 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/. */ + +var EXPORTED_SYMBOLS = ["CommonDialog"]; + +const lazy = {}; +ChromeUtils.defineModuleGetter( + lazy, + "EnableDelayHelper", + "resource://gre/modules/SharedPromptUtils.jsm" +); + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +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: + Cu.reportError( + "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) { + Cu.reportError("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/Prompter.jsm b/toolkit/components/prompts/src/Prompter.jsm new file mode 100644 index 0000000000..caeef99f15 --- /dev/null +++ b/toolkit/components/prompts/src/Prompter.jsm @@ -0,0 +1,1818 @@ +/* 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"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +// This is redefined below, for strange and unfortunate reasons. +var { PromptUtils } = ChromeUtils.import( + "resource://gre/modules/SharedPromptUtils.jsm" +); + +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"; + +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) { + Cu.reportError( + "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) { + Cu.reportError( + "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) + ) { + Cu.reportError( + "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) { + Cu.reportError( + "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; + } 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 +); + +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); + }, +}; + +var EXPORTED_SYMBOLS = ["Prompter", "AuthPromptAdapterFactory"]; diff --git a/toolkit/components/prompts/src/SharedPromptUtils.jsm b/toolkit/components/prompts/src/SharedPromptUtils.jsm new file mode 100644 index 0000000000..805ffca5e1 --- /dev/null +++ b/toolkit/components/prompts/src/SharedPromptUtils.jsm @@ -0,0 +1,184 @@ +/* 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/. */ + +var EXPORTED_SYMBOLS = ["PromptUtils", "EnableDelayHelper"]; + +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. + */ +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/components.conf b/toolkit/components/prompts/src/components.conf new file mode 100644 index 0000000000..184f011bd5 --- /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'], + 'jsm': 'resource://gre/modules/Prompter.jsm', + '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'], + 'jsm': 'resource://gre/modules/Prompter.jsm', + 'constructor': 'Prompter', + }, + ] diff --git a/toolkit/components/prompts/src/moz.build b/toolkit/components/prompts/src/moz.build new file mode 100644 index 0000000000..3183266f2f --- /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.jsm", + "Prompter.jsm", + "SharedPromptUtils.jsm", +] + +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.jsm b/toolkit/components/prompts/test/PromptTestUtils.jsm new file mode 100644 index 0000000000..53c9da01cd --- /dev/null +++ b/toolkit/components/prompts/test/PromptTestUtils.jsm @@ -0,0 +1,248 @@ +/* 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. + */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const EXPORTED_SYMBOLS = ["PromptTestUtils"]; + +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) + ); +} + +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..e31fbdc851 --- /dev/null +++ b/toolkit/components/prompts/test/chromeScript.js @@ -0,0 +1,354 @@ +/* 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..6f00eb3815 --- /dev/null +++ b/toolkit/components/prompts/test/mochitest.ini @@ -0,0 +1,17 @@ +[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 +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..a47d421d3c --- /dev/null +++ b/toolkit/components/prompts/test/prompt_common.js @@ -0,0 +1,444 @@ +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": { + outParams = [/* pwd */ 4]; + break; + } + case "promptUsernameAndPassword": { + 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> |