diff options
Diffstat (limited to 'toolkit/components/prompts')
26 files changed, 4494 insertions, 0 deletions
diff --git a/toolkit/components/prompts/content/commonDialog.css b/toolkit/components/prompts/content/commonDialog.css new file mode 100644 index 000000000..89f88db7a --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.css @@ -0,0 +1,22 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */ + +#infoContainer { + max-width: 45em; +} + +#info\.body { + -moz-user-focus: normal; + -moz-user-select: text; + cursor: text !important; + white-space: pre-wrap; + unicode-bidi: plaintext; +} + +#loginLabel, #password1Label { + text-align: right; +} + diff --git a/toolkit/components/prompts/content/commonDialog.js b/toolkit/components/prompts/content/commonDialog.js new file mode 100644 index 000000000..ef4686654 --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.js @@ -0,0 +1,62 @@ +/* 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 Ci = Components.interfaces; +var Cr = Components.results; +var Cc = Components.classes; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/CommonDialog.jsm"); + +var propBag, args, Dialog; + +function commonDialogOnLoad() { + propBag = window.arguments[0].QueryInterface(Ci.nsIWritablePropertyBag2) + .QueryInterface(Ci.nsIWritablePropertyBag); + // Convert to a JS object + args = {}; + let propEnum = propBag.enumerator; + while (propEnum.hasMoreElements()) { + let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty); + args[prop.name] = prop.value; + } + + let dialog = document.documentElement; + + 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"), + infoBody : document.getElementById("info.body"), + infoTitle : document.getElementById("info.title"), + infoIcon : document.getElementById("info.icon"), + 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, + }; + + // limit the dialog to the screen width + document.getElementById("filler").maxWidth = screen.availWidth; + + Dialog = new CommonDialog(args, ui); + Dialog.onLoad(dialog); + // resize the window to the content + 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.xul b/toolkit/components/prompts/content/commonDialog.xul new file mode 100644 index 000000000..990b26586 --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.xul @@ -0,0 +1,97 @@ +<?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/" 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 dialog SYSTEM "chrome://global/locale/commonDialog.dtd"> + +<dialog id="commonDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + aria-describedby="info.body" + onunload="commonDialogOnUnload();" + ondialogaccept="Dialog.onButton0(); return true;" + ondialogcancel="Dialog.onButton1(); return true;" + ondialogextra1="Dialog.onButton2(); window.close();" + ondialogextra2="Dialog.onButton3(); window.close();" + buttonpack="center"> + + <script type="application/javascript" src="chrome://global/content/commonDialog.js"/> + <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript"> + 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" + label="©Cmd.label;" + accesskey="©Cmd.accesskey;" + command="cmd_copy" + disabled="true"/> + <menuitem id="context-selectall" + label="&selectAllCmd.label;" + accesskey="&selectAllCmd.accesskey;" + command="cmd_selectAll"/> + </menupopup> + </popupset> + + <hbox id="filler" style="min-width: 0%;"> + <spacer style="width: 29em;"/> + </hbox> + + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + + <rows> + <row> + <hbox id="iconContainer" align="start"> + <image id="info.icon" class="spaced"/> + </hbox> + <vbox id="infoContainer" +#ifndef XP_MACOSX + pack="center" +#endif + > + <!-- Only shown on OS X, since it has no dialog title --> + <description id="info.title" +#ifndef XP_MACOSX + hidden="true" +#else + style="margin-bottom: 1em" +#endif + /> + <description id="info.body" context="contentAreaContextMenu" noinitialfocus="true"/> + </vbox> + </row> + <row id="loginContainer" hidden="true" align="center"> + <label id="loginLabel" value="&editfield0.label;" control="loginTextbox"/> + <textbox id="loginTextbox"/> + </row> + <row id ="password1Container" hidden="true" align="center"> + <label id="password1Label" value="&editfield1.label;" control="password1Textbox"/> + <textbox type="password" id="password1Textbox"/> + </row> + <row id="checkboxContainer" hidden="true"> + <spacer/> + <checkbox id="checkbox" oncommand="Dialog.onCheckbox()"/> + </row> + </rows> + </grid> + +</dialog> diff --git a/toolkit/components/prompts/content/selectDialog.js b/toolkit/components/prompts/content/selectDialog.js new file mode 100644 index 000000000..7628dc8d9 --- /dev/null +++ b/toolkit/components/prompts/content/selectDialog.js @@ -0,0 +1,67 @@ +/* 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 Ci = Components.interfaces; +var Cr = Components.results; +var Cc = Components.classes; +var Cu = Components.utils; + +var gArgs, listBox; + +function dialogOnLoad() { + gArgs = window.arguments[0].QueryInterface(Ci.nsIWritablePropertyBag2) + .QueryInterface(Ci.nsIWritablePropertyBag); + + let promptType = gArgs.getProperty("promptType"); + if (promptType != "select") { + Cu.reportError("selectDialog opened for unknown type: " + promptType); + window.close(); + } + + // Default to canceled. + gArgs.setProperty("ok", false); + + document.title = gArgs.getProperty("title"); + + let text = gArgs.getProperty("text"); + document.getElementById("info.txt").setAttribute("value", text); + + let items = gArgs.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, false); + } + listBox.selectedIndex = 0; + listBox.focus(); + + // resize the window to the content + window.sizeToContent(); + + // Move to the right location + moveToAlertPosition(); + centerWindowOnScreen(); + + // play sound + try { + Cc["@mozilla.org/sound;1"]. + createInstance(Ci.nsISound). + playEventSound(Ci.nsISound.EVENT_SELECT_DIALOG_OPEN); + } catch (e) { } +} + +function dialogOK() { + gArgs.setProperty("selected", listBox.selectedIndex); + gArgs.setProperty("ok", true); + return true; +} + +function dialogDoubleClick() { + dialogOK(); + window.close(); +} diff --git a/toolkit/components/prompts/content/selectDialog.xul b/toolkit/components/prompts/content/selectDialog.xul new file mode 100644 index 000000000..9b72bcfb0 --- /dev/null +++ b/toolkit/components/prompts/content/selectDialog.xul @@ -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 dialog SYSTEM "chrome://global/locale/commonDialog.dtd"> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="dialogOnLoad()" + ondialogaccept="return dialogOK();"> + + <script type="application/javascript" src="chrome://global/content/selectDialog.js" /> + <keyset id="dialogKeys"/> + <vbox style="width: 24em;margin: 5px;"> + <label id="info.txt"/> + <vbox> + <listbox id="list" rows="4" flex="1"/> + </vbox> + </vbox> +</dialog> diff --git a/toolkit/components/prompts/content/tabprompts.css b/toolkit/components/prompts/content/tabprompts.css new file mode 100644 index 000000000..c4b0f7593 --- /dev/null +++ b/toolkit/components/prompts/content/tabprompts.css @@ -0,0 +1,35 @@ +/* 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 */ +tabmodalprompt { + width: 100%; + height: 100%; + -moz-box-pack: center; + -moz-box-orient: vertical; +} + +.mainContainer { + min-width: 20em; + min-height: 12em; + -moz-user-focus: normal; +} + +.info\.title { + margin-bottom: 1em !important; + font-weight: bold; +} + +.info\.body { + margin: 0 !important; + -moz-user-focus: normal; + -moz-user-select: text; + cursor: text !important; + white-space: pre-wrap; + unicode-bidi: plaintext; +} + +label[value=""] { + visibility: collapse; +} diff --git a/toolkit/components/prompts/content/tabprompts.xml b/toolkit/components/prompts/content/tabprompts.xml new file mode 100644 index 000000000..07c6c8efb --- /dev/null +++ b/toolkit/components/prompts/content/tabprompts.xml @@ -0,0 +1,352 @@ +<?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/. --> + +<!DOCTYPE bindings [ +<!ENTITY % commonDialogDTD SYSTEM "chrome://global/locale/commonDialog.dtd"> +<!ENTITY % dialogOverlayDTD SYSTEM "chrome://global/locale/dialogOverlay.dtd"> +%commonDialogDTD; +%dialogOverlayDTD; +]> + +<bindings id="tabPrompts" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="tabmodalprompt"> + + <resources> + <stylesheet src="chrome://global/content/tabprompts.css"/> + <stylesheet src="chrome://global/skin/tabprompts.css"/> + </resources> + + <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + role="dialog" + aria-describedby="info.body"> + + <!-- This is based on the guts of commonDialog.xul --> + <spacer flex="1"/> + <hbox pack="center"> + <vbox anonid="mainContainer" class="mainContainer"> + <grid class="topContainer" flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + + <rows> + <vbox anonid="infoContainer" align="center" pack="center" flex="1"> + <description anonid="info.title" class="info.title" hidden="true" /> + <description anonid="info.body" class="info.body"/> + </vbox> + + <row anonid="loginContainer" hidden="true" align="center"> + <label anonid="loginLabel" value="&editfield0.label;" control="loginTextbox"/> + <textbox anonid="loginTextbox"/> + </row> + + <row anonid="password1Container" hidden="true" align="center"> + <label anonid="password1Label" value="&editfield1.label;" control="password1Textbox"/> + <textbox anonid="password1Textbox" type="password"/> + </row> + + <row anonid="checkboxContainer" hidden="true"> + <spacer/> + <checkbox anonid="checkbox"/> + </row> + + <xbl:children includes="row"/> + </rows> + </grid> + <xbl:children/> + <hbox class="buttonContainer"> +#ifdef XP_UNIX + <button anonid="button3" hidden="true"/> + <button anonid="button2" hidden="true"/> + <spacer anonid="buttonSpacer" flex="1"/> + <button anonid="button1" label="&cancelButton.label;"/> + <button anonid="button0" label="&okButton.label;"/> +#else + <button anonid="button3" hidden="true"/> + <spacer anonid="buttonSpacer" flex="1"/> + <button anonid="button0" label="&okButton.label;"/> + <button anonid="button2" hidden="true"/> + <button anonid="button1" label="&cancelButton.label;"/> +#endif + </hbox> + </vbox> + </hbox> + <spacer flex="2"/> + </xbl:content> + + <implementation implements="nsIDOMEventListener"> + <constructor> + <![CDATA[ + let self = this; + function getElement(anonid) { + return document.getAnonymousElementByAttribute(self, "anonid", anonid); + } + + this.ui = { + prompt : this, + loginContainer : getElement("loginContainer"), + loginTextbox : getElement("loginTextbox"), + loginLabel : getElement("loginLabel"), + password1Container : getElement("password1Container"), + password1Textbox : getElement("password1Textbox"), + password1Label : getElement("password1Label"), + infoBody : getElement("info.body"), + infoTitle : getElement("info.title"), + infoIcon : null, + checkbox : getElement("checkbox"), + checkboxContainer : getElement("checkboxContainer"), + button3 : getElement("button3"), + button2 : getElement("button2"), + button1 : getElement("button1"), + button0 : getElement("button0"), + // focusTarget (for BUTTON_DELAY_ENABLE) not yet supported + }; + + this.ui.button0.addEventListener("command", this.onButtonClick.bind(this, 0), false); + this.ui.button1.addEventListener("command", this.onButtonClick.bind(this, 1), false); + this.ui.button2.addEventListener("command", this.onButtonClick.bind(this, 2), false); + this.ui.button3.addEventListener("command", this.onButtonClick.bind(this, 3), false); + // Anonymous wrapper used here because |Dialog| doesn't exist until init() is called! + this.ui.checkbox.addEventListener("command", function() { self.Dialog.onCheckbox(); }, false); + this.isLive = false; + ]]> + </constructor> + <destructor> + <![CDATA[ + if (this.isLive) { + this.abortPrompt(); + } + ]]> + </destructor> + + <field name="ui"/> + <field name="args"/> + <field name="linkedTab"/> + <field name="onCloseCallback"/> + <field name="Dialog"/> + <field name="isLive"/> + <field name="availWidth"/> + <field name="availHeight"/> + <field name="minWidth"/> + <field name="minHeight"/> + + <method name="init"> + <parameter name="args"/> + <parameter name="linkedTab"/> + <parameter name="onCloseCallback"/> + <body> + <![CDATA[ + this.args = args; + this.linkedTab = linkedTab; + this.onCloseCallback = onCloseCallback; + + if (args.enableDelay) + throw "BUTTON_DELAY_ENABLE not yet supported for tab-modal prompts"; + + // 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.isLive = true; + window.addEventListener("resize", this, false); + window.addEventListener("unload", this, false); + linkedTab.addEventListener("TabClose", this, false); + // 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 tmp = {}; + Components.utils.import("resource://gre/modules/CommonDialog.jsm", tmp); + this.Dialog = new tmp.CommonDialog(args, this.ui); + this.Dialog.onLoad(null); + + // 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.showAlertOrigin) + 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) + + this.onResize(); + ]]> + </body> + </method> + + <method name="shutdownPrompt"> + <body> + <![CDATA[ + // remove our event listeners + try { + window.removeEventListener("resize", this, false); + window.removeEventListener("unload", this, false); + this.linkedTab.removeEventListener("TabClose", this, false); + } catch (e) { } + this.isLive = false; + // invoke callback + this.onCloseCallback(); + ]]> + </body> + </method> + + <method name="abortPrompt"> + <body> + <![CDATA[ + // Called from other code when the page changes. + this.Dialog.abortPrompt(); + this.shutdownPrompt(); + ]]> + </body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + switch (aEvent.type) { + case "resize": + this.onResize(); + break; + case "unload": + case "TabClose": + this.abortPrompt(); + break; + } + ]]> + </body> + </method> + + <method name="onResize"> + <body> + <![CDATA[ + let availWidth = this.clientWidth; + let availHeight = this.clientHeight; + if (availWidth == this.availWidth && availHeight == this.availHeight) + return; + this.availWidth = availWidth; + this.availHeight = availHeight; + + let self = this; + function getElement(anonid) { + return document.getAnonymousElementByAttribute(self, "anonid", anonid); + } + let main = getElement("mainContainer"); + let info = getElement("infoContainer"); + let body = this.ui.infoBody; + + // cap prompt dimensions at 60% width and 60% height of content area + if (!this.minWidth) + this.minWidth = parseInt(window.getComputedStyle(main).minWidth); + if (!this.minHeight) + this.minHeight = parseInt(window.getComputedStyle(main).minHeight); + let maxWidth = Math.max(Math.floor(availWidth * 0.6), this.minWidth) + + info.clientWidth - main.clientWidth; + let maxHeight = Math.max(Math.floor(availHeight * 0.6), this.minHeight) + + info.clientHeight - main.clientHeight; + body.style.maxWidth = maxWidth + "px"; + info.style.overflow = info.style.width = info.style.height = ""; + + // when prompt text is too long, use scrollbars + if (info.clientWidth > maxWidth) { + info.style.overflow = "auto"; + info.style.width = maxWidth + "px"; + } + if (info.clientHeight > maxHeight) { + info.style.overflow = "auto"; + info.style.height = maxHeight + "px"; + } + ]]> + </body> + </method> + + <method name="onButtonClick"> + <parameter name="buttonNum"/> + <body> + <![CDATA[ + // 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.mainThread.dispatch(() => { + this.Dialog["onButton" + buttonNum](); + this.shutdownPrompt(); + }, + Ci.nsIThread.DISPATCH_NORMAL); + ]]> + </body> + </method> + + <method name="onKeyAction"> + <parameter name="action"/> + <parameter name="event"/> + <body> + <![CDATA[ + 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 + } + ]]> + </body> + </method> + </implementation> + + <handlers> + <!-- Based on dialog.xml handlers --> + <handler event="keypress" keycode="VK_RETURN" + group="system" action="this.onKeyAction('default', event);"/> + <handler event="keypress" keycode="VK_ESCAPE" + group="system" action="this.onKeyAction('cancel', event);"/> +#ifdef XP_MACOSX + <handler event="keypress" key="." modifiers="meta" + group="system" action="this.onKeyAction('cancel', event);"/> +#endif + <handler event="focus" phase="capturing"> + let bnum = this.args.defaultButtonNum || 0; + let defaultButton = this.ui["button" + bnum]; + + let { AppConstants } = + Components.utils.import("resource://gre/modules/AppConstants.jsm", {}); + 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 instanceof Ci.nsIDOMXULButtonElement; + defaultButton.setAttribute("default", focusedDefault || !someButtonFocused); + } + </handler> + <handler event="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.setAttribute("default", false); + </handler> + </handlers> + + </binding> +</bindings> diff --git a/toolkit/components/prompts/jar.mn b/toolkit/components/prompts/jar.mn new file mode 100644 index 000000000..60ecbdcbc --- /dev/null +++ b/toolkit/components/prompts/jar.mn @@ -0,0 +1,12 @@ +# 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/commonDialog.js (content/commonDialog.js) +* content/global/commonDialog.xul (content/commonDialog.xul) + content/global/commonDialog.css (content/commonDialog.css) + content/global/selectDialog.js (content/selectDialog.js) + content/global/selectDialog.xul (content/selectDialog.xul) + content/global/tabprompts.css (content/tabprompts.css) +* content/global/tabprompts.xml (content/tabprompts.xml) diff --git a/toolkit/components/prompts/moz.build b/toolkit/components/prompts/moz.build new file mode 100644 index 000000000..1dc21cca6 --- /dev/null +++ b/toolkit/components/prompts/moz.build @@ -0,0 +1,11 @@ +# -*- 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/. + +DIRS += ['src'] + +MOCHITEST_MANIFESTS += ['test/mochitest.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 000000000..c4200feb3 --- /dev/null +++ b/toolkit/components/prompts/src/CommonDialog.jsm @@ -0,0 +1,308 @@ +/* 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/. */ + +this.EXPORTED_SYMBOLS = ["CommonDialog"]; + +const Ci = Components.interfaces; +const Cr = Components.results; +const Cc = Components.classes; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "EnableDelayHelper", + "resource://gre/modules/SharedPromptUtils.jsm"); + + +this.CommonDialog = function CommonDialog(args, ui) { + this.args = args; + this.ui = ui; +} + +CommonDialog.prototype = { + args : null, + ui : null, + + hasInputField : true, + numButtons : undefined, + iconClass : undefined, + soundID : undefined, + focusTimer : null, + + onLoad : function(xulDialog) { + 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 "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", ""); + 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 "unknown dialog type"; + } + + // set the document title + let title = this.args.title; + // OS X doesn't have a title on modal dialogs, this is hidden on other platforms. + let infoTitle = this.ui.infoTitle; + infoTitle.appendChild(infoTitle.ownerDocument.createTextNode(title)); + if (xulDialog) + xulDialog.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); + } + 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.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 (xulDialog) + xulDialog.defaultButton = ['accept', 'cancel', 'extra1', 'extra2'][b]; + else + button.setAttribute("default", "true"); + + // Set default focus / selection. + this.setDefaultFocus(true); + + if (this.args.enableDelay) { + this.delayHelper = new EnableDelayHelper({ + disableDialog: () => this.setButtonsEnabledState(false), + enableDialog: () => this.setButtonsEnabledState(true), + focusTarget: this.ui.focusTarget + }); + } + + // Play a sound (unless we're tab-modal -- don't want those to feel like OS prompts). + try { + if (xulDialog && this.soundID) { + Cc["@mozilla.org/sound;1"]. + createInstance(Ci.nsISound). + playEventSound(this.soundID); + } + } catch (e) { + Cu.reportError("Couldn't play common dialog event sound: " + e); + } + + let topic = "common-dialog-loaded"; + if (!xulDialog) + topic = "tabmodal-dialog-loaded"; + Services.obs.notifyObservers(this.ui.prompt, topic, null); + }, + + setLabelForNode: function(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 : function (aName, aValue) { + this.ui[aName + "Container"].hidden = false; + this.ui[aName + "Textbox"].setAttribute("value", + aValue !== null ? aValue : ""); + }, + + setButtonsEnabledState : function(enabled) { + this.ui.button0.disabled = !enabled; + // button1 (cancel) remains enabled. + this.ui.button2.disabled = !enabled; + this.ui.button3.disabled = !enabled; + }, + + setDefaultFocus : function(isInitialLoad) { + let b = (this.args.defaultButtonNum || 0); + let button = this.ui["button" + b]; + + if (!this.hasInputField) { + let isOSX = ("nsILocalFileMac" in Components.interfaces); + if (isOSX) + this.ui.infoBody.focus(); + else + button.focus(); + } 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(); + } + }, + + onCheckbox : function() { + this.args.checked = this.ui.checkbox.checked; + }, + + onButton0 : function() { + 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 : function() { + this.args.promptActive = false; + this.args.buttonNumClicked = 1; + }, + + onButton2 : function() { + this.args.promptActive = false; + this.args.buttonNumClicked = 2; + }, + + onButton3 : function() { + this.args.promptActive = false; + this.args.buttonNumClicked = 3; + }, + + abortPrompt : function() { + this.args.promptActive = false; + this.args.promptAborted = true; + }, + +}; diff --git a/toolkit/components/prompts/src/SharedPromptUtils.jsm b/toolkit/components/prompts/src/SharedPromptUtils.jsm new file mode 100644 index 000000000..b27096ac2 --- /dev/null +++ b/toolkit/components/prompts/src/SharedPromptUtils.jsm @@ -0,0 +1,157 @@ +this.EXPORTED_SYMBOLS = [ "PromptUtils", "EnableDelayHelper" ]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +this.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 : function (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.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + winUtils.dispatchEventToChromeOnly(target, event); + }, + + objectToPropBag : function (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 : function (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. + */ +this.EnableDelayHelper = function({enableDialog, disableDialog, focusTarget}) { + this.enableDialog = makeSafe(enableDialog); + this.disableDialog = makeSafe(disableDialog); + this.focusTarget = focusTarget; + + this.disableDialog(); + + this.focusTarget.addEventListener("blur", this, false); + this.focusTarget.addEventListener("focus", this, false); + this.focusTarget.document.addEventListener("unload", this, false); + + this.startOnFocusDelay(); +}; + +this.EnableDelayHelper.prototype = { + get delayTime() { + return Services.prefs.getIntPref("security.dialog_enable_delay"); + }, + + handleEvent : function(event) { + if (event.target != this.focusTarget && + event.target != this.focusTarget.document) + return; + + switch (event.type) { + case "blur": + this.onBlur(); + break; + + case "focus": + this.onFocus(); + break; + + case "unload": + this.onUnload(); + break; + } + }, + + onBlur : function () { + 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 : function () { + this.startOnFocusDelay(); + }, + + onUnload: function() { + this.focusTarget.removeEventListener("blur", this, false); + this.focusTarget.removeEventListener("focus", this, false); + this.focusTarget.document.removeEventListener("unload", this, false); + + if (this._focusTimer) { + this._focusTimer.cancel(); + this._focusTimer = null; + } + + this.focusTarget = this.enableDialog = this.disableDialog = null; + }, + + startOnFocusDelay : function() { + 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 : function() { + 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/moz.build b/toolkit/components/prompts/src/moz.build new file mode 100644 index 000000000..b13e47b42 --- /dev/null +++ b/toolkit/components/prompts/src/moz.build @@ -0,0 +1,16 @@ +# -*- 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_COMPONENTS += [ + 'nsPrompter.js', + 'nsPrompter.manifest', +] + +EXTRA_JS_MODULES += [ + 'CommonDialog.jsm', + 'SharedPromptUtils.jsm', +] + diff --git a/toolkit/components/prompts/src/nsPrompter.js b/toolkit/components/prompts/src/nsPrompter.js new file mode 100644 index 000000000..26efe28cc --- /dev/null +++ b/toolkit/components/prompts/src/nsPrompter.js @@ -0,0 +1,958 @@ +/* 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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/SharedPromptUtils.jsm"); + +function Prompter() { + // Note that EmbedPrompter clones this implementation. +} + +Prompter.prototype = { + classID : Components.ID("{1c978d25-b37f-43a8-a2d6-0c7a239ead87}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIPromptService, Ci.nsIPromptService2]), + + + /* ---------- private members ---------- */ + + pickPrompter : function (domWin) { + return new ModalPrompter(domWin); + }, + + + /* ---------- nsIPromptFactory ---------- */ + + + getPrompt : function (domWin, iid) { + // This is still kind of dumb; the C++ code delegated to login manager + // here, which in turn calls back into us via nsIPromptService2. + 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 ---------- */ + + + alert : function (domWin, title, text) { + let p = this.pickPrompter(domWin); + p.alert(title, text); + }, + + alertCheck : function (domWin, title, text, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + p.alertCheck(title, text, checkLabel, checkValue); + }, + + confirm : function (domWin, title, text) { + let p = this.pickPrompter(domWin); + return p.confirm(title, text); + }, + + confirmCheck : function (domWin, title, text, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.confirmCheck(title, text, checkLabel, checkValue); + }, + + confirmEx : function (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); + }, + + prompt : function (domWin, title, text, value, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.nsIPrompt_prompt(title, text, value, checkLabel, checkValue); + }, + + promptUsernameAndPassword : function (domWin, title, text, user, pass, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.nsIPrompt_promptUsernameAndPassword(title, text, user, pass, checkLabel, checkValue); + }, + + promptPassword : function (domWin, title, text, pass, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.nsIPrompt_promptPassword(title, text, pass, checkLabel, checkValue); + }, + + select : function (domWin, title, text, count, list, selected) { + let p = this.pickPrompter(domWin); + return p.select(title, text, count, list, selected); + }, + + + /* ---------- nsIPromptService2 ---------- */ + + + promptAuth : function (domWin, channel, level, authInfo, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.promptAuth(channel, level, authInfo, checkLabel, checkValue); + }, + + asyncPromptAuth : function (domWin, channel, callback, context, level, authInfo, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.asyncPromptAuth(channel, callback, context, level, authInfo, checkLabel, checkValue); + }, + +}; + + +// Common utils not specific to a particular prompter style. +var PromptUtilsTemp = { + __proto__ : PromptUtils, + + getLocalizedString : function (key, formatArgs) { + if (formatArgs) + return this.strBundle.formatStringFromName(key, formatArgs, formatArgs.length); + return this.strBundle.GetStringFromName(key); + }, + + confirmExHelper : function (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 = PromptUtils.getLocalizedString("OK"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_CANCEL: + buttonLabel = PromptUtils.getLocalizedString("Cancel"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_YES: + buttonLabel = PromptUtils.getLocalizedString("Yes"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_NO: + buttonLabel = PromptUtils.getLocalizedString("No"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_SAVE: + buttonLabel = PromptUtils.getLocalizedString("Save"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE: + buttonLabel = PromptUtils.getLocalizedString("DontSave"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_REVERT: + buttonLabel = PromptUtils.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 : function (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 : function (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 : function (uri) { + return uri.scheme + "://" + uri.hostPort; + }, + + // Copied from login manager + getAuthTarget : function (aChannel, aAuthInfo) { + let hostname, 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 "proxy auth needs nsIProxiedChannel"; + + let info = aChannel.proxyInfo; + if (!info) + throw "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); + hostname = "moz-proxy://" + + idnService.convertUTF8toACE(info.host) + + ":" + info.port; + realm = aAuthInfo.realm; + if (!realm) + realm = hostname; + + return [hostname, realm]; + } + + hostname = this.getFormattedHostname(aChannel.URI); + + // 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 = hostname; + + return [hostname, realm]; + }, + + + makeAuthMessage : function (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 = ""; + + // Trim obnoxiously long realms. + if (realm.length > 150) { + realm = realm.substring(0, 150); + // Append "..." (or localized equivalent). + realm += this.ellipsis; + } + + let text; + if (isProxy) { + text = PromptUtils.getLocalizedString("EnterLoginForProxy3", [realm, displayHost]); + } else if (isPassOnly) { + text = PromptUtils.getLocalizedString("EnterPasswordFor", [username, displayHost]); + } else if (isCrossOrig) { + text = PromptUtils.getLocalizedString("EnterUserPasswordForCrossOrigin2", [displayHost]); + } else if (!realm) { + text = PromptUtils.getLocalizedString("EnterUserPasswordFor2", [displayHost]); + } else { + text = PromptUtils.getLocalizedString("EnterLoginForRealm3", [realm, displayHost]); + } + + return text; + }, + + getTabModalPrompt : function (domWin) { + var promptBox = null; + + try { + // Get the topmost window, in case we're in a frame. + var promptWin = domWin.top; + + // Get the chrome window for the content window we're using. + // (Unwrap because we need a non-IDL property below.) + var chromeWin = promptWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.ownerDocument + .defaultView.wrappedJSObject; + + if (chromeWin.getTabModalPromptBox) + promptBox = chromeWin.getTabModalPromptBox(promptWin); + } catch (e) { + // If any errors happen, just assume no tabmodal prompter. + } + + return promptBox; + }, +}; + +PromptUtils = PromptUtilsTemp; + +XPCOMUtils.defineLazyGetter(PromptUtils, "strBundle", function () { + let bunService = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService); + let bundle = bunService.createBundle("chrome://global/locale/commonDialogs.properties"); + if (!bundle) + throw "String bundle for Prompter not present!"; + return bundle; +}); + +XPCOMUtils.defineLazyGetter(PromptUtils, "ellipsis", function () { + let ellipsis = "\u2026"; + try { + ellipsis = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; + } catch (e) { } + return ellipsis; +}); + + + +function openModalWindow(domWin, uri, args) { + // 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. + // There's also a requirement for prompts to be blocked if a window is + // passed and that window is hidden (eg, auth prompts are supressed if the + // passed window is the hidden window). + // See bug 875157 comment 30 for more... + if (domWin) { + // a domWin was passed, so we can apply the check for it being hidden. + let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + if (winUtils && !winUtils.isParentWindowMainWidgetVisible) { + throw Components.Exception("Cannot call openModalWindow on a hidden window", + Cr.NS_ERROR_NOT_AVAILABLE); + } + } else { + // We try and find a window to use as the parent, but don't consider + // if that is visible before showing the prompt. + domWin = Services.ww.activeWindow; + // domWin may still be null here if there are _no_ windows open. + } + // Note that we don't need to fire DOMWillOpenModalDialog and + // DOMModalDialogClosed events here, wwatcher's OpenWindowInternal + // will do that. Similarly for enterModalState / leaveModalState. + + Services.ww.openWindow(domWin, uri, "_blank", "centerscreen,chrome,modal,titlebar", args); +} + +function openTabPrompt(domWin, tabPrompt, args) { + let docShell = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + let inPermitUnload = docShell.contentViewer && docShell.contentViewer.inPermitUnload; + let eventDetail = Cu.cloneInto({tabPrompt: true, inPermitUnload}, domWin); + PromptUtils.fireDialogEvent(domWin, "DOMWillOpenModalDialog", null, eventDetail); + + let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + winUtils.enterModalState(); + + let frameMM = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + frameMM.QueryInterface(Ci.nsIDOMEventTarget); + + // We provide a callback so the prompt can close itself. We don't want to + // wait for this event loop to return... Otherwise the presence of other + // prompts on the call stack would in this dialog appearing unresponsive + // until the other prompts had been closed. + let callbackInvoked = false; + let newPrompt; + function onPromptClose(forceCleanup) { + if (!newPrompt && !forceCleanup) + return; + callbackInvoked = true; + if (newPrompt) + tabPrompt.removePrompt(newPrompt); + + frameMM.removeEventListener("pagehide", pagehide, true); + + winUtils.leaveModalState(); + + PromptUtils.fireDialogEvent(domWin, "DOMModalDialogClosed"); + } + + frameMM.addEventListener("pagehide", pagehide, true); + function pagehide(e) { + // Check whether the event relates to our window or its ancestors + let window = domWin; + let eventWindow = e.target.defaultView; + while (window != eventWindow && window.parent != window) { + window = window.parent; + } + if (window != eventWindow) { + return; + } + frameMM.removeEventListener("pagehide", pagehide, true); + + if (newPrompt) { + newPrompt.abortPrompt(); + } + } + + try { + let topPrincipal = domWin.top.document.nodePrincipal; + let promptPrincipal = domWin.document.nodePrincipal; + args.showAlertOrigin = topPrincipal.equals(promptPrincipal); + args.promptActive = true; + + newPrompt = tabPrompt.appendPrompt(args, onPromptClose); + + // TODO since we don't actually open a window, need to check if + // there's other stuff in nsWindowWatcher::OpenWindowInternal + // that we might need to do here as well. + + let thread = Services.tm.currentThread; + while (args.promptActive) + thread.processNextEvent(true); + delete args.promptActive; + + if (args.promptAborted) + throw Components.Exception("prompt aborted by user", Cr.NS_ERROR_NOT_AVAILABLE); + } finally { + // If the prompt unexpectedly failed to invoke the callback, do so here. + if (!callbackInvoked) + onPromptClose(true); + } +} + +function openRemotePrompt(domWin, args, tabPrompt) { + let docShell = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + let messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsITabChild) + .messageManager; + + let inPermitUnload = docShell.contentViewer && docShell.contentViewer.inPermitUnload; + let eventDetail = Cu.cloneInto({tabPrompt, inPermitUnload}, domWin); + PromptUtils.fireDialogEvent(domWin, "DOMWillOpenModalDialog", null, eventDetail); + + let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + winUtils.enterModalState(); + let closed = false; + + let frameMM = docShell.getInterface(Ci.nsIContentFrameMessageManager); + frameMM.QueryInterface(Ci.nsIDOMEventTarget); + + // It should be hard or impossible to cause a window to create multiple + // prompts, but just in case, give our prompt an ID. + let id = "id" + Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator).generateUUID().toString(); + + messageManager.addMessageListener("Prompt:Close", function listener(message) { + if (message.data._remoteId !== id) { + return; + } + + messageManager.removeMessageListener("Prompt:Close", listener); + frameMM.removeEventListener("pagehide", pagehide, true); + + winUtils.leaveModalState(); + PromptUtils.fireDialogEvent(domWin, "DOMModalDialogClosed"); + + // Copy the response from the closed prompt into our args, it will be + // read by our caller. + if (message.data) { + for (let key in message.data) { + args[key] = message.data[key]; + } + } + + // Exit our nested event loop when we unwind. + closed = true; + }); + + frameMM.addEventListener("pagehide", pagehide, true); + function pagehide(e) { + // Check whether the event relates to our window or its ancestors + let window = domWin; + let eventWindow = e.target.defaultView; + while (window != eventWindow && window.parent != window) { + window = window.parent; + } + if (window != eventWindow) { + return; + } + frameMM.removeEventListener("pagehide", pagehide, true); + messageManager.sendAsyncMessage("Prompt:ForceClose", { _remoteId: id }); + } + + let topPrincipal = domWin.top.document.nodePrincipal; + let promptPrincipal = domWin.document.nodePrincipal; + args.promptPrincipal = promptPrincipal; + args.showAlertOrigin = topPrincipal.equals(promptPrincipal); + args.inPermitUnload = inPermitUnload; + + args._remoteId = id; + + messageManager.sendAsyncMessage("Prompt:Open", args, {}); + + let thread = Services.tm.currentThread; + while (!closed) { + thread.processNextEvent(true); + } +} + +function ModalPrompter(domWin) { + this.domWin = domWin; +} +ModalPrompter.prototype = { + domWin : null, + /* + * Default to not using a tab-modal prompt, unless the caller opts in by + * QIing to nsIWritablePropertyBag and setting the value of this property + * to true. + */ + allowTabModal : false, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIPrompt, Ci.nsIAuthPrompt, + Ci.nsIAuthPrompt2, + Ci.nsIWritablePropertyBag2]), + + + /* ---------- internal methods ---------- */ + + + openPrompt : function (args) { + // Check pref, if false/missing do not ever allow tab-modal prompts. + const prefName = "prompts.tab_modal.enabled"; + let prefValue = false; + if (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_BOOL) + prefValue = Services.prefs.getBoolPref(prefName); + + let allowTabModal = this.allowTabModal && prefValue; + + if (allowTabModal && this.domWin) { + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + openRemotePrompt(this.domWin, args, true); + return; + } + + let tabPrompt = PromptUtils.getTabModalPrompt(this.domWin); + if (tabPrompt) { + openTabPrompt(this.domWin, tabPrompt, args); + return; + } + } + + // If we can't do a tab modal prompt, fallback to using a window-modal dialog. + const COMMON_DIALOG = "chrome://global/content/commonDialog.xul"; + const SELECT_DIALOG = "chrome://global/content/selectDialog.xul"; + + let uri = (args.promptType == "select") ? SELECT_DIALOG : COMMON_DIALOG; + + if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { + args.uri = uri; + openRemotePrompt(this.domWin, args); + return; + } + + let propBag = PromptUtils.objectToPropBag(args); + openModalWindow(this.domWin, uri, propBag); + PromptUtils.propBagToObject(propBag, args); + }, + + + + /* + * ---------- 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 : function() { + // 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 : function() { + // Both have 6 args, so use types. + if (typeof arguments[2] == "object") + return this.nsIPrompt_promptUsernameAndPassword.apply(this, arguments); + return this.nsIAuthPrompt_promptUsernameAndPassword.apply(this, arguments); + }, + + promptPassword : function() { + // Both have 5 args, so use types. + if (typeof arguments[2] == "object") + return this.nsIPrompt_promptPassword.apply(this, arguments); + return this.nsIAuthPrompt_promptPassword.apply(this, arguments); + }, + + + /* ---------- nsIPrompt ---------- */ + + + alert : function (title, text) { + if (!title) + title = PromptUtils.getLocalizedString("Alert"); + + let args = { + promptType: "alert", + title: title, + text: text, + }; + + this.openPrompt(args); + }, + + alertCheck : function (title, text, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("Alert"); + + let args = { + promptType: "alertCheck", + title: title, + text: text, + checkLabel: checkLabel, + checked: checkValue.value, + }; + + this.openPrompt(args); + + // Checkbox state always returned, even if cancel clicked. + checkValue.value = args.checked; + }, + + confirm : function (title, text) { + if (!title) + title = PromptUtils.getLocalizedString("Confirm"); + + let args = { + promptType: "confirm", + title: title, + text: text, + ok: false, + }; + + this.openPrompt(args); + + // Did user click Ok or Cancel? + return args.ok; + }, + + confirmCheck : function (title, text, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("ConfirmCheck"); + + let args = { + promptType: "confirmCheck", + title: title, + text: text, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + }; + + this.openPrompt(args); + + // Checkbox state always returned, even if cancel clicked. + checkValue.value = args.checked; + + // Did user click Ok or Cancel? + return args.ok; + }, + + confirmEx : function (title, text, flags, button0, button1, button2, + checkLabel, checkValue) { + + if (!title) + title = PromptUtils.getLocalizedString("Confirm"); + + let args = { + promptType: "confirmEx", + title: title, + text: text, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + buttonNumClicked: 1, + }; + + let [label0, label1, label2, defaultButtonNum, isDelayEnabled] = + PromptUtils.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; + } + } + } + + this.openPrompt(args); + + // Checkbox state always returned, even if cancel clicked. + checkValue.value = args.checked; + + // Get the number of the button the user clicked. + return args.buttonNumClicked; + }, + + nsIPrompt_prompt : function (title, text, value, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("Prompt"); + + let args = { + promptType: "prompt", + title: title, + text: text, + value: value.value, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + }; + + this.openPrompt(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 : function (title, text, user, pass, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("PromptUsernameAndPassword2"); + + let args = { + promptType: "promptUserAndPass", + title: title, + text: text, + user: user.value, + pass: pass.value, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + }; + + this.openPrompt(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + checkValue.value = args.checked; + user.value = args.user; + pass.value = args.pass; + } + + return ok; + }, + + nsIPrompt_promptPassword : function (title, text, pass, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("PromptPassword2"); + + let args = { + promptType: "promptPassword", + title: title, + text: text, + pass: pass.value, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + } + + this.openPrompt(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + checkValue.value = args.checked; + pass.value = args.pass; + } + + return ok; + }, + + select : function (title, text, count, list, selected) { + if (!title) + title = PromptUtils.getLocalizedString("Select"); + + let args = { + promptType: "select", + title: title, + text: text, + list: list, + selected: -1, + ok: false, + }; + + this.openPrompt(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) + selected.value = args.selected; + + return ok; + }, + + + /* ---------- nsIAuthPrompt ---------- */ + + + nsIAuthPrompt_prompt : function (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 : function (title, text, passwordRealm, savePassword, user, pass) { + // The passwordRealm and savePassword args were ignored by nsPrompt.cpp + return this.nsIPrompt_promptUsernameAndPassword(title, text, user, pass, null, {}); + }, + + nsIAuthPrompt_promptPassword : function (title, text, passwordRealm, savePassword, pass) { + // The passwordRealm and savePassword args were ignored by nsPrompt.cpp + return this.nsIPrompt_promptPassword(title, text, pass, null, {}); + }, + + + /* ---------- nsIAuthPrompt2 ---------- */ + + + promptAuth : function (channel, level, authInfo, checkLabel, checkValue) { + let message = PromptUtils.makeAuthMessage(channel, authInfo); + + let [username, password] = PromptUtils.getAuthInfo(authInfo); + + let userParam = { value: username }; + let passParam = { value: password }; + + let ok; + if (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) + ok = this.nsIPrompt_promptPassword(null, message, passParam, checkLabel, checkValue); + else + ok = this.nsIPrompt_promptUsernameAndPassword(null, message, userParam, passParam, checkLabel, checkValue); + + if (ok) + PromptUtils.setAuthInfo(authInfo, userParam.value, passParam.value); + return ok; + }, + + asyncPromptAuth : function (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 Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /* ---------- nsIWritablePropertyBag2 ---------- */ + + // Only a partial implementation, for one specific use case... + + setPropertyAsBool : function(name, value) { + if (name == "allowTabModal") + this.allowTabModal = value; + else + throw Cr.NS_ERROR_ILLEGAL_VALUE; + }, +}; + + +function AuthPromptAdapterFactory() { +} +AuthPromptAdapterFactory.prototype = { + classID : Components.ID("{6e134924-6c3a-4d86-81ac-69432dd971dc}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPromptAdapterFactory]), + + /* ---------- nsIAuthPromptAdapterFactory ---------- */ + + createAdapter : function (oldPrompter) { + return new AuthPromptAdapter(oldPrompter); + } +}; + + +// Takes an nsIAuthPrompt implementation, wraps it with a nsIAuthPrompt2 shell. +function AuthPromptAdapter(oldPrompter) { + this.oldPrompter = oldPrompter; +} +AuthPromptAdapter.prototype = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]), + oldPrompter : null, + + /* ---------- nsIAuthPrompt2 ---------- */ + + promptAuth : function (channel, level, authInfo, checkLabel, checkValue) { + let message = PromptUtils.makeAuthMessage(channel, authInfo); + + let [username, password] = PromptUtils.getAuthInfo(authInfo); + let userParam = { value: username }; + let passParam = { value: password }; + + let [host, realm] = PromptUtils.getAuthTarget(channel, authInfo); + let authTarget = host + " (" + 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) + PromptUtils.setAuthInfo(authInfo, userParam.value, passParam.value); + return ok; + }, + + asyncPromptAuth : function (channel, callback, context, level, authInfo, checkLabel, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } +}; + + +// Wrapper using the old embedding contractID, since it's already common in +// the addon ecosystem. +function EmbedPrompter() { +} +EmbedPrompter.prototype = new Prompter(); +EmbedPrompter.prototype.classID = Components.ID("{7ad1b327-6dfa-46ec-9234-f2a620ea7e00}"); + +var component = [Prompter, EmbedPrompter, AuthPromptAdapterFactory]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); diff --git a/toolkit/components/prompts/src/nsPrompter.manifest b/toolkit/components/prompts/src/nsPrompter.manifest new file mode 100644 index 000000000..582f6bf59 --- /dev/null +++ b/toolkit/components/prompts/src/nsPrompter.manifest @@ -0,0 +1,6 @@ +component {1c978d25-b37f-43a8-a2d6-0c7a239ead87} nsPrompter.js +contract @mozilla.org/prompter;1 {1c978d25-b37f-43a8-a2d6-0c7a239ead87} +component {6e134924-6c3a-4d86-81ac-69432dd971dc} nsPrompter.js +contract @mozilla.org/network/authprompt-adapter-factory;1 {6e134924-6c3a-4d86-81ac-69432dd971dc} +component {7ad1b327-6dfa-46ec-9234-f2a620ea7e00} nsPrompter.js +contract @mozilla.org/embedcomp/prompt-service;1 {7ad1b327-6dfa-46ec-9234-f2a620ea7e00} diff --git a/toolkit/components/prompts/test/.eslintrc.js b/toolkit/components/prompts/test/.eslintrc.js new file mode 100644 index 000000000..3c788d6d6 --- /dev/null +++ b/toolkit/components/prompts/test/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/mochitest.eslintrc.js" + ] +}; diff --git a/toolkit/components/prompts/test/bug619644_inner.html b/toolkit/components/prompts/test/bug619644_inner.html new file mode 100644 index 000000000..f929c5649 --- /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 000000000..740d59a61 --- /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/chromeScript.js b/toolkit/components/prompts/test/chromeScript.js new file mode 100644 index 000000000..7b2d37100 --- /dev/null +++ b/toolkit/components/prompts/test/chromeScript.js @@ -0,0 +1,241 @@ +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/Timer.jsm"); + +// 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 => { + handlePromptWhenItAppears(msg.action, msg.isTabModal, msg.isSelect); +}); + +function handlePromptWhenItAppears(action, isTabModal, isSelect) { + let interval = setInterval(() => { + if (handlePrompt(action, isTabModal, isSelect)) { + clearInterval(interval); + } + }, 100); +} + +function handlePrompt(action, isTabModal, isSelect) { + let ui; + + if (isTabModal) { + let browserWin = Services.wm.getMostRecentWindow("navigator:browser"); + let gBrowser = browserWin.gBrowser; + let promptManager = gBrowser.getTabModalPromptBox(gBrowser.selectedBrowser); + let prompts = promptManager.listPrompts(); + if (!prompts.length) { + return false; // try again in a bit + } + + ui = prompts[0].Dialog.ui; + } else { + let doc = getDialogDoc(); + if (!doc) { + return false; // try again in a bit + } + + if (isSelect) + ui = doc; + else + ui = doc.defaultView.Dialog.ui; + + } + + let promptState; + if (isSelect) { + promptState = getSelectState(ui); + dismissSelect(ui, action); + } else { + promptState = getPromptState(ui); + dismissPrompt(ui, action); + } + sendAsyncMessage("promptHandled", { promptState: 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.titleHidden = ui.infoTitle.getAttribute("hidden") == "true"; + state.textHidden = ui.loginContainer.hidden; + state.passHidden = ui.password1Container.hidden; + state.checkHidden = ui.checkboxContainer.hidden; + state.checkMsg = ui.checkbox.label; + state.checked = ui.checkbox.checked; + // tab-modal prompts don't have an infoIcon + state.iconClass = ui.infoIcon ? ui.infoIcon.className : null; + state.textValue = ui.loginTextbox.getAttribute("value"); + state.passValue = ui.password1Textbox.getAttribute("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 fm = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + let e = fm.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 (ui.loginTextbox.inputField.isSameNode(e)) { + state.focused = "textField"; + } else if (ui.password1Textbox.inputField.isSameNode(e)) { + state.focused = "passField"; + } else if (ui.infoBody.isSameNode(e)) { + state.focused = "infoBody"; + } else { + state.focused = "ERROR: unexpected element focused: " + (e ? e.localName : "<null>"); + } + + 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) { + if (action.setCheckbox) { + // Annoyingly, the prompt code is driven by oncommand. + ui.checkbox.setChecked(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", { code: "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; + + default: + throw "dismissPrompt action listed unknown button."; + } +} + +function getDialogDoc() { + // Trudge through all the open windows, until we find the one + // that has either commonDialog.xul or selectDialog.xul loaded. + var wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + // var enumerator = wm.getEnumerator("navigator:browser"); + var enumerator = wm.getXULWindowEnumerator(null); + + while (enumerator.hasMoreElements()) { + var win = enumerator.getNext(); + var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell; + + var containedDocShells = windowDocShell.getDocShellEnumerator( + Ci.nsIDocShellTreeItem.typeChrome, + Ci.nsIDocShell.ENUMERATE_FORWARDS); + while (containedDocShells.hasMoreElements()) { + // Get the corresponding document for this docshell + var childDocShell = containedDocShells.getNext(); + // We don't want it if it's not done loading. + if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) + continue; + var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell) + .contentViewer + .DOMDocument; + + if (childDoc.location.href != "chrome://global/content/commonDialog.xul" && + childDoc.location.href != "chrome://global/content/selectDialog.xul") + 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.) + let fm = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + if (fm.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 000000000..7f87650d6 --- /dev/null +++ b/toolkit/components/prompts/test/mochitest.ini @@ -0,0 +1,19 @@ +[DEFAULT] +support-files = + ../../passwordmgr/test/authenticate.sjs + bug619644_inner.html + bug625187_iframe.html + prompt_common.js + chromeScript.js + +[test_bug619644.html] +[test_bug620145.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_subresources_prompts.html] +skip-if = toolkit == 'android' +[test_dom_prompts.html] +skip-if = toolkit == 'android' #android: bug 1267092 +[test_modal_prompts.html] +skip-if = toolkit == 'android' || (os == 'linux' && (debug || asan)) #android: TIMED_OUT (For Linux : 950636) +[test_modal_select.html] +skip-if = toolkit == 'android' #android: TIMED_OUT diff --git a/toolkit/components/prompts/test/prompt_common.js b/toolkit/components/prompts/test/prompt_common.js new file mode 100644 index 000000000..e3a69b347 --- /dev/null +++ b/toolkit/components/prompts/test/prompt_common.js @@ -0,0 +1,158 @@ +const Ci = SpecialPowers.Ci; +const Cc = SpecialPowers.Cc; +ok(Ci != null, "Access Ci"); +ok(Cc != null, "Access Cc"); + +function hasTabModalPrompts() { + var prefName = "prompts.tab_modal.enabled"; + var Services = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm").Services; + return Services.prefs.getPrefType(prefName) == Services.prefs.PREF_BOOL && + Services.prefs.getBoolPref(prefName); +} +var isTabModal = hasTabModalPrompts(); +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()); + +function onloadPromiseFor(id) { + var iframe = document.getElementById(id); + return new Promise(resolve => { + iframe.addEventListener("load", function onload(e) { + iframe.removeEventListener("load", onload); + resolve(true); + }); + }); +} + +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: action, isTabModal: isTabModal}); + }); +} + +function checkPromptState(promptState, expectedState) { + // XXX check title? OS X has title in content + is(promptState.msg, expectedState.msg, "Checking expected message"); + if (isOSX && !isTabModal) + ok(!promptState.titleHidden, "Checking title always visible on OS X"); + 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 (!isTabModal) + is(promptState.iconClass, "spaced " + 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")) { + is(promptState.focused, "infoBody", "buttons don't focus on OS X, but infoBody does instead"); + } else { + is(promptState.focused, expectedState.focused, "Checking focused element"); + } +} + +function checkEchoedAuthInfo(expectedState, doc) { + // 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; + + is(authok, "PASS", "Checking for successful authentication"); + is(username, expectedState.user, "Checking for echoed username"); + is(password, expectedState.pass, "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.sendSyncMessage("proxyPrompter", { + args, + methodName: prop, + })[0][0]; + + 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 000000000..9f61eb18b --- /dev/null +++ b/toolkit/components/prompts/test/test_bug619644.html @@ -0,0 +1,76 @@ +<!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 type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" 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>"; + +if (!isTabModal) { + todo(false, "Test disabled when tab modal prompts are not enabled."); +} else { + inittest(); +} + +var promptDone; + +function inittest() { + window.addEventListener("message", runtest, false); + window.open("bug619644_inner.html", "619644"); + + SimpleTest.waitForExplicitFinish(); +} + +function runtest(e) { + window.removeEventListener("message", runtest, false); + window.addEventListener("message", checktest, false); + + 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 000000000..bb4470259 --- /dev/null +++ b/toolkit/components/prompts/test/test_bug620145.html @@ -0,0 +1,105 @@ +<html> +<head> + <title>Test for Bug 620145</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" 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"> +var selectionTest = false; + +function openAlert() { + info("opening alert..."); + alert("hello!"); + info("...alert done."); +} + +add_task(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. + info("isTabModal? " + isTabModal); + selectionTest = isTabModal; + + 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", + }; + + 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. + + yield 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. + + yield promptDone; + checkSelection(); +}); + +function dispatchMouseEvent(target, type) +{ + var win = target.ownerDocument.defaultView; + 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.dispatchDOMEventViaPresShell(target, e, true); + ok(true, type + " sent to " + target.id); +} + +function checkSelection() +{ + if (!selectionTest) { + todo(false, "Selection test is disabled when tab modal prompts are not enabled."); + } else { + 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 000000000..413ed8fd5 --- /dev/null +++ b/toolkit/components/prompts/test/test_dom_prompts.html @@ -0,0 +1,208 @@ +<html> +<head> + <title>Test for DOM prompts</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" 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; + +add_task(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", + }; + + promptDone = handlePrompt(state, action); + + alert("This is the alert text."); + + yield promptDone; +}); + +// bug 861605 made the arguments to alert/confirm optional (prompt already was). +add_task(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", + }; + + promptDone = handlePrompt(state, action); + + try { + alert(); + ok(true, "alert() without arguments should not throw!"); + } catch (e) { + ok(false, "alert() without arguments should not throw!"); + } + + yield promptDone; +}); + + +add_task(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", + }; + + promptDone = handlePrompt(state, action); + + rv = confirm("This is the confirm text."); + is(rv, true, "check prompt return value"); + + yield promptDone; +}); + +// bug 861605 made the arguments to alert/confirm optional (prompt already was). +add_task(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", + }; + + 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"); + + yield promptDone; +}); + + +add_task(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", + }; + + promptDone = handlePrompt(state, action); + + rv = prompt("This is the Prompt text."); + is(rv, "", "check prompt return value"); + + yield promptDone; +}); + +// bug 861605 made the arguments to alert/confirm optional (prompt already was). +add_task(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", + }; + + 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"); + + yield 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 000000000..42e6be52c --- /dev/null +++ b/toolkit/components/prompts/test/test_modal_prompts.html @@ -0,0 +1,1184 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Modal Prompts Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</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;version=1.8"> + +function* runTests() { + const { NetUtil } = SpecialPowers.Cu.import('resource://gre/modules/NetUtil.jsm'); + let state, action; + ok(true, "Running tests (isTabModal=" + isTabModal + ", usePromptService=" + usePromptService + ")"); + + let prompter, promptArgs; + if (usePromptService) { + prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService2); + } else { + prompter = Cc["@mozilla.org/prompter;1"]. + getService(Ci.nsIPromptFactory). + getPrompt(window, Ci.nsIPrompt); + if (isTabModal) { + let bag = prompter.QueryInterface(Ci.nsIWritablePropertyBag2); + bag.setPropertyAsBool("allowTabModal", true); + } + } + + let checkVal = {}; + let textVal = {}; + let passVal = {}; + let flags; + let isOK, clickedButton; + + // ===== + 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."]; + if (usePromptService) + promptArgs.unshift(window); + prompter.alert.apply(null, promptArgs); + + yield 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, {}]; + if (usePromptService) + promptArgs.unshift(window); + prompter.alertCheck.apply(null, promptArgs); + + yield 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!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + prompter.alertCheck.apply(null, promptArgs); + is(checkVal.value, true, "checkbox was checked"); + + yield 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."]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.confirm.apply(null, promptArgs); + is(isOK, true, "checked expected retval"); + + yield 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."]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.confirm.apply(null, promptArgs); + is(isOK, false, "checked expected retval"); + + yield 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, {}]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.confirmCheck.apply(null, promptArgs); + is(isOK, true, "checked expected retval"); + + yield 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, {}]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.confirmCheck.apply(null, promptArgs); + is(isOK, false, "checked expected retval"); + + yield 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!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.confirmCheck.apply(null, promptArgs); + is(isOK, true, "checked expected retval"); + is(checkVal.value, true, "expected checkbox setting"); + + yield 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!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.confirmCheck.apply(null, promptArgs); + is(isOK, false, "checked expected retval"); + is(checkVal.value, true, "expected checkbox setting"); + + yield 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.", textVal, null, {}]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.prompt.apply(null, promptArgs); + is(isOK, true, "checked expected retval"); + is(textVal.value, "bacon", "checking expected text value"); + + yield 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.", textVal, null, {}]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.prompt.apply(null, promptArgs); + is(isOK, true, "checked expected retval"); + is(textVal.value, "kittens", "checking expected text value"); + + yield 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.", textVal, null, {}]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.prompt.apply(null, promptArgs); + is(isOK, false, "checked expected retval"); + is(textVal.value, "puppies", "checking expected text value"); + + yield 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.", textVal, null, {}]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.prompt.apply(null, promptArgs); + is(isOK, false, "checked expected retval"); + is(textVal.value, "puppies", "checking expected text value"); + + yield 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.", textVal, "Check me out!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.prompt.apply(null, promptArgs); + is(isOK, true, "checked expected retval"); + is(textVal.value, "tribbles", "checking expected text value"); + is(checkVal.value, true, "expected checkbox setting"); + + yield 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.", textVal, "Check me out!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.prompt.apply(null, promptArgs); + is(isOK, false, "checked expected retval"); + is(textVal.value, "tribbles", "checking expected text value"); + is(checkVal.value, false, "expected checkbox setting"); + + yield 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 : false, + textValue : "usr", + passValue : "ssh", + checkMsg : "Check me out!", + checked : false, + focused : "textField", + defButton : "button0", + }; + action = { + buttonClick: "ok", + setCheckbox: true, + textField: "newusr", + passField: "newssh", + }; + + promptDone = handlePrompt(state, action); + + textVal.value = "usr"; + passVal.value = "ssh"; + checkVal.value = false; + promptArgs = ["TestTitle", "This is the pUAP text.", textVal, passVal, "Check me out!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.promptUsernameAndPassword.apply(null, promptArgs); + is(isOK, true, "checked expected retval"); + is(textVal.value, "newusr", "checking expected text value"); + is(passVal.value, "newssh", "checking expected pass value"); + is(checkVal.value, true, "expected checkbox setting"); + + yield 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 : false, + textValue : "usr", + passValue : "ssh", + checkMsg : "Check me out!", + checked : false, + focused : "textField", + defButton : "button0", + }; + action = { + buttonClick : "cancel", + setCheckbox : true, + textField : "newusr", + passField : "newssh", + }; + + promptDone = handlePrompt(state, action); + + textVal.value = "usr"; + passVal.value = "ssh"; + checkVal.value = false; + promptArgs = ["TestTitle", "This is the pUAP text.", textVal, passVal, "Check me out!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.promptUsernameAndPassword.apply(null, promptArgs); + is(isOK, false, "checked expected retval"); + is(textVal.value, "usr", "checking expected text value"); + is(passVal.value, "ssh", "checking expected pass value"); + is(checkVal.value, false, "expected checkbox setting"); + + yield 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 : false, + textValue : "", + passValue : "ssh", + checkMsg : "Check me out!", + checked : false, + focused : "passField", + defButton : "button0", + }; + action = { + buttonClick : "ok", + setCheckbox : true, + passField : "newssh", + }; + + promptDone = handlePrompt(state, action); + + passVal.value = "ssh"; + checkVal.value = false; + promptArgs = ["TestTitle", "This is the promptPassword text.", passVal, "Check me out!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.promptPassword.apply(null, promptArgs); + is(isOK, true, "checked expected retval"); + is(passVal.value, "newssh", "checking expected pass value"); + is(checkVal.value, true, "expected checkbox setting"); + + yield 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 : false, + textValue : "", + passValue : "ssh", + checkMsg : "Check me out!", + checked : false, + focused : "passField", + defButton : "button0", + }; + action = { + buttonClick : "cancel", + setCheckbox : true, + passField : "newssh", + }; + + promptDone = handlePrompt(state, action); + + passVal.value = "ssh"; + checkVal.value = false; + promptArgs = ["TestTitle", "This is the promptPassword text.", passVal, "Check me out!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + isOK = prompter.promptPassword.apply(null, promptArgs); + is(isOK, false, "checked expected retval"); + is(passVal.value, "ssh", "checking expected pass value"); + is(checkVal.value, false, "expected checkbox setting"); + + yield 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, {}]; + if (usePromptService) + promptArgs.unshift(window); + clickedButton = prompter.confirmEx.apply(null, promptArgs); + is(clickedButton, 0, "checked expected button num click"); + + yield 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, {}]; + if (usePromptService) + promptArgs.unshift(window); + clickedButton = prompter.confirmEx.apply(null, promptArgs); + is(clickedButton, 1, "checked expected button num click"); + + yield 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!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + clickedButton = prompter.confirmEx.apply(null, promptArgs); + is(clickedButton, 0, "checked expected button num click"); + is(checkVal.value, true, "expected checkbox setting"); + + yield 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!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + clickedButton = prompter.confirmEx.apply(null, promptArgs); + is(clickedButton, 1, "checked expected button num click"); + is(checkVal.value, true, "expected checkbox setting"); + + yield 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!", checkVal]; + if (usePromptService) + promptArgs.unshift(window); + clickedButton = prompter.confirmEx.apply(null, promptArgs); + is(clickedButton, 2, "checked expected button num click"); + is(checkVal.value, true, "expected checkbox setting"); + + yield 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 (!isTabModal && !isE10S) { + promptDone = handlePrompt(state, action); + + promptArgs = ["TestTitle", "This is the alert text."]; + if (usePromptService) + promptArgs.unshift(null); + prompter.alert.apply(null, promptArgs); + + yield 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 after delay fires + defButton : "button0", + butt0Label : "OK", + butt1Label : "Cancel", + butt0Disabled: true, + }; + + // OS X doesn't initially focus the button, but rather the infoBody. + // The focus stays there even after the button-enable delay has fired. + if (isOSX) + state.focused = "infoBody"; + + action = { + buttonClick: "pollOK", + }; + if (!isTabModal) { + 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, {}]; + if (usePromptService) + promptArgs.unshift(window); + clickedButton = prompter.confirmEx.apply(null, promptArgs); + is(clickedButton, 0, "checked expected button num click"); + + yield 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 : "" + }; + + + // ===== + // (promptAuth is only accessible from the prompt service) + info("Starting test: promptAuth with empty realm"); + state = { + msg : 'http://example.com is requesting your username and password.', + title : "TestTitle", + iconClass : "authentication-icon question-icon", + titleHidden : true, + textHidden : false, + passHidden : false, + checkHidden : false, + textValue : "", + passValue : "", + checkMsg : "Check me out!", + checked : false, + focused : "textField", + defButton : "button0", + }; + action = { + buttonClick : "ok", + setCheckbox : true, + textField : "username", + passField : "password", + }; + if (usePromptService) { + promptDone = handlePrompt(state, action); + + checkVal.value = false; + isOK = prompter.promptAuth(window, channel, level, authinfo, "Check me out!", checkVal); + is(isOK, true, "checked expected retval"); + is(authinfo.username, "username", "checking filled username"); + is(authinfo.password, "password", "checking filled password"); + is(checkVal.value, true, "expected checkbox setting"); + + yield promptDone; + } + + + // ===== + // (promptAuth is only accessible from the prompt service) + info("Starting test: promptAuth with long realm"); + state = { + msg : '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', + title : "TestTitle", + iconClass : "authentication-icon question-icon", + titleHidden : true, + textHidden : false, + passHidden : false, + checkHidden : false, + textValue : "", + passValue : "", + checkMsg : "Check me out!", + checked : false, + focused : "textField", + defButton : "button0", + }; + action = { + buttonClick : "ok", + setCheckbox : true, + textField : "username", + passField : "password", + }; + if (usePromptService) { + promptDone = handlePrompt(state, action); + + checkVal.value = false; + var longString = ""; + for (var i = 0; i < 20; i++) + longString += "abcdefghi "; // 200 chars long + authinfo.realm = longString; + authinfo.username = ""; + authinfo.password = ""; + isOK = prompter.promptAuth(window, channel, level, authinfo, "Check me out!", checkVal); + is(isOK, true, "checked expected retval"); + is(authinfo.username, "username", "checking filled username"); + is(authinfo.password, "password", "checking filled password"); + is(checkVal.value, true, "expected checkbox setting"); + + yield promptDone; + } + + 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 : 'http://example.com is requesting your username and password. ' + + 'WARNING: Your password will not be sent to the website you are currently visiting!', + title : "TestTitle", + iconClass : "authentication-icon question-icon", + titleHidden : true, + textHidden : false, + passHidden : false, + checkHidden : false, + textValue : "", + passValue : "", + checkMsg : "Check me out!", + checked : false, + focused : "textField", + defButton : "button0", + }; + action = { + buttonClick : "ok", + setCheckbox : false, + textField : "username", + passField : "password", + }; + if (usePromptService) { + promptDone = handlePrompt(state, action); + checkVal.value = false; + isOK = prompter.promptAuth(window, channel, level, authinfo, "Check me out!", checkVal); + is(isOK, true, "checked expected retval"); + is(authinfo.username, "username", "checking filled username"); + is(authinfo.password, "password", "checking filled password"); + is(checkVal.value, false, "expected checkbox setting"); + + yield 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 : 'http://example.com is requesting your username and password. ' + + 'WARNING: Your password will not be sent to the website you are currently visiting!', + title : "TestTitle", + iconClass : "authentication-icon question-icon", + titleHidden : true, + textHidden : false, + passHidden : false, + checkHidden : false, + textValue : "", + passValue : "", + checkMsg : "Check me out!", + checked : false, + focused : "textField", + defButton : "button0", + }; + action = { + buttonClick : "ok", + setCheckbox : false, + textField : "username", + passField : "password", + }; + if (usePromptService) { + promptDone = handlePrompt(state, action); + + checkVal.value = false; + isOK = prompter.promptAuth(window, channel, level, authinfo, "Check me out!", checkVal); + is(isOK, true, "checked expected retval"); + is(authinfo.username, "username", "checking filled username"); + is(authinfo.password, "password", "checking filled password"); + is(checkVal.value, false, "expected checkbox setting"); + + yield promptDone; + } +} + +let usePromptService; + +/* + * Run the body of the 3 times: + * - 1st pass: with window-modal prompts, using nsIPromptService + * - 2nd pass: still window-modal, using nsIPrompt directly (via nsIPromptFactory) + * - 3rd pass: with tab-modal prompts. Can't opt into these via * nsIPromptService. + */ + +add_task(function* runPromptTests() { + info("Process type: " + SpecialPowers.Services.appinfo.processType); + + isTabModal = false; usePromptService = true; + info("Running tests with: isTabModal=" + isTabModal + ", usePromptService=" + usePromptService); + yield* runTests(); + + isTabModal = false; usePromptService = false; + info("Running tests with: isTabModal=" + isTabModal + ", usePromptService=" + usePromptService); + yield* runTests(); + + if (SpecialPowers.getBoolPref("prompts.tab_modal.enabled")) { + isTabModal = true; usePromptService = false; + info("Running tests with: isTabModal=" + isTabModal + ", usePromptService=" + usePromptService); + yield* 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 000000000..1e008d0f4 --- /dev/null +++ b/toolkit/components/prompts/test/test_modal_select.html @@ -0,0 +1,146 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Modal Prompts Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" src="prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</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;version=1.8"> + +let prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService2); + +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; +let state, action; + +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: action, isSelect: true}); + }); +} + + +// ===== +add_task(function* test_select_empty_list() { + info("Starting test: Select (0 items, ok)"); + state = { + msg : "This is the select text.", + title : "TestTitle", + items : [], + }; + action = { + buttonClick: "ok", + }; + promptDone = handlePrompt(state, action); + items = []; + selectVal.value = null; // outparam, just making sure. + isOK = prompter.select(window, "TestTitle", "This is the select text.", items.length, items, selectVal); + is(isOK, true, "checked expected retval"); + is(selectVal.value, -1, "checking selected index"); + + yield promptDone; +}); + +// ===== +add_task(function* test_select_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. + isOK = prompter.select(window, "TestTitle", "This is the select text.", items.length, items, selectVal); + is(isOK, true, "checked expected retval"); + is(selectVal.value, 0, "checking selected index"); + + yield promptDone; +}); + +// ===== +add_task(function* test_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. + isOK = prompter.select(window, "TestTitle", "This is the select text.", items.length, items, selectVal); + is(isOK, true, "checked expected retval"); + is(selectVal.value, 1, "checking selected index"); + + yield promptDone; +}); + +// ===== +add_task(function* test_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. + isOK = prompter.select(window, "TestTitle", "This is the select text.", items.length, items, selectVal); + is(isOK, false, "checked expected retval"); + is(selectVal.value, 0, "checking selected index"); + + yield promptDone; +}); + +</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 000000000..241ce430f --- /dev/null +++ b/toolkit/components/prompts/test/test_subresources_prompts.html @@ -0,0 +1,202 @@ +<html> +<head> + <title>Test subresources prompts (Bug 625187 and bug 1230462)</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <script type="text/javascript" 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 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"); + +add_task(function* runTest() +{ + // This test depends on tab modal prompts being enabled. + if (!isTabModal) { + todo(false, "Test disabled when tab modal prompts are not enabled."); + return; + } + + info("Ensuring iframe1 has loaded..."); + yield iframe1Loaded; + info("Ensuring iframe2 has loaded..."); + yield 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", + }; + + promptDone = handlePrompt(state, action); + + var button = document.querySelector("button"); + dispatchMouseEvent(button, "click"); + + yield 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"); + button = SpecialPowers.wrap(iframe.contentWindow).document.getElementById("btn1"); + dispatchMouseEvent(button, "click"); + + yield promptDone; + + + // mostly reusing same state/action + state.titleHidden = true; + state.msg = "hello world 2"; + promptDone = handlePrompt(state, action); + + iframe = document.getElementById("iframe_same_origin"); + button = iframe.contentWindow.document.getElementById("btn1"); + dispatchMouseEvent(button, "click"); + + yield promptDone; + + + // mostly reusing same state/action + state.msg = "hello world 3"; + promptDone = handlePrompt(state, action); + + button = iframe.contentWindow.document.getElementById("btn2"); + dispatchMouseEvent(button, "click"); + + yield promptDone; +}); + +add_task(function* runTestAuth() +{ + // Following tests chack prompt message for a cross-origin and not + // cross-origin subresources load + + // Force parent to not look for tab-modal prompts, as they're not + // used for auth prompts. + isTabModal = false; + + state = { + msg : "http://mochi.test:8888 is requesting your username " + + "and password. The site says: “mochitest”", + 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", + }; + + promptDone = handlePrompt(state, action); + + var iframe3Loaded = onloadPromiseFor("iframe_prompt"); + iframe_prompt.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1"; + yield promptDone; + yield iframe3Loaded; + checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"}, + iframe_prompt.contentDocument); + + // Cross-origin subresourse test. + + // Force parent to not look for tab-modal prompts, as they're not used for auth prompts. + isTabModal =false; + state = { + msg : "http://example.com is requesting your username and password. " + + "WARNING: Your password will not be sent to 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"; + yield promptDone; + yield iframe3Loaded; + checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"}, + SpecialPowers.wrap(iframe_prompt.contentWindow).document); +}); + +function dispatchMouseEvent(target, type) +{ + var win = SpecialPowers.unwrap(target.ownerDocument.defaultView); + var 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.dispatchDOMEventViaPresShell(SpecialPowers.unwrap(target), e, true); +} +</script> +</body> +</html> |