Bug 741587 - Part 1: Make alert/prompt/confirm work with <iframe mozbrowser>. r=smaug

--HG--
extra : rebase_source : f418da542453f8b0f5853a8da48bea1c8c95969e
This commit is contained in:
Justin Lebar 2012-06-07 10:43:23 -04:00
parent 3f38f83c87
commit b9c544e22e
9 changed files with 439 additions and 22 deletions

View File

@ -487,3 +487,9 @@ pref("ui.click_hold_context_menus.delay", 1000);
pref("device.storage.enabled", true);
pref("media.plugins.enabled", true);
// Disable printing (particularly, window.print())
pref("dom.disable_window_print", true);
// Disable window.showModalDialog
pref("dom.disable_window_showModalDialog", true);

View File

@ -874,6 +874,9 @@ xpicleanup@BIN_SUFFIX@
components/amContentHandler.js
components/amWebInstallListener.js
components/browser.xpt
components/BrowserElementParent.js
components/BrowserElementParent.manifest
components/BrowserElementPromptService.jsm
components/components.manifest
components/contentAreaDropListener.js
components/contentSecurityPolicy.js

View File

@ -7,7 +7,10 @@
let Cu = Components.utils;
let Ci = Components.interfaces;
let Cc = Components.classes;
let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/BrowserElementPromptService.jsm");
// Event whitelisted for bubbling.
let whitelistedEvents = [
@ -45,6 +48,9 @@ var global = this;
function BrowserElementChild() {
this._init();
// Maps outer window id --> weak ref to window. Used by modal dialog code.
this._windowIDDict = {};
};
BrowserElementChild.prototype = {
@ -52,6 +58,8 @@ BrowserElementChild.prototype = {
debug("Starting up.");
sendAsyncMsg("hello");
BrowserElementPromptService.mapWindowToBrowserElementChild(content, this);
docShell.QueryInterface(Ci.nsIWebProgress)
.addProgressListener(this._progressListener,
Ci.nsIWebProgress.NOTIFY_LOCATION |
@ -87,11 +95,14 @@ BrowserElementChild.prototype = {
/* useCapture = */ true,
/* wantsUntrusted = */ false);
addMessageListener("browser-element-api:get-screenshot",
this._recvGetScreenshot.bind(this));
var self = this;
function addMsgListener(msg, handler) {
addMessageListener('browser-element-api:' + msg, handler.bind(self));
}
addMessageListener("browser-element-api:set-visible",
this._recvSetVisible.bind(this));
addMsgListener("get-screenshot", this._recvGetScreenshot);
addMsgListener("set-visible", this._recvSetVisible);
addMsgListener("unblock-modal-prompt", this._recvStopWaiting);
let els = Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService);
@ -109,6 +120,136 @@ BrowserElementChild.prototype = {
/* useCapture = */ true);
},
_tryGetInnerWindowID: function(win) {
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
try {
return utils.currentInnerWindowID;
}
catch(e) {
return null;
}
},
/**
* Show a modal prompt. Called by BrowserElementPromptService.
*/
showModalPrompt: function(win, args) {
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
args.windowID = { outer: utils.outerWindowID,
inner: this._tryGetInnerWindowID(win) };
sendAsyncMsg('showmodalprompt', args);
let returnValue = this._waitForResult(win);
if (args.promptType == 'prompt' ||
args.promptType == 'confirm') {
return returnValue;
}
},
/**
* Spin in a nested event loop until we receive a unblock-modal-prompt message for
* this window.
*/
_waitForResult: function(win) {
debug("_waitForResult(" + win + ")");
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let outerWindowID = utils.outerWindowID;
let innerWindowID = this._tryGetInnerWindowID(win);
if (innerWindowID === null) {
// I have no idea what waiting for a result means when there's no inner
// window, so let's just bail.
debug("_waitForResult: No inner window. Bailing.");
return;
}
this._windowIDDict[outerWindowID] = Cu.getWeakReference(win);
debug("Entering modal state (outerWindowID=" + outerWindowID + ", " +
"innerWindowID=" + innerWindowID + ")");
// In theory, we're supposed to pass |modalStateWin| back to
// leaveModalStateWithWindow. But in practice, the window is always null,
// because it's the window associated with this script context, which
// doesn't have a window. But we'll play along anyway in case this
// changes.
var modalStateWin = utils.enterModalStateWithWindow();
// We'll decrement win.modalDepth when we receive a unblock-modal-prompt message
// for the window.
if (!win.modalDepth) {
win.modalDepth = 0;
}
win.modalDepth++;
let origModalDepth = win.modalDepth;
let thread = Services.tm.currentThread;
debug("Nested event loop - begin");
while (win.modalDepth == origModalDepth) {
// Bail out of the loop if the inner window changed; that means the
// window navigated.
if (this._tryGetInnerWindowID(win) !== innerWindowID) {
debug("_waitForResult: Inner window ID changed " +
"while in nested event loop.");
break;
}
thread.processNextEvent(/* mayWait = */ true);
}
debug("Nested event loop - finish");
// If we exited the loop because the inner window changed, then bail on the
// modal prompt.
if (innerWindowID !== this._tryGetInnerWindowID(win)) {
throw Components.Exception("Modal state aborted by navigation",
Cr.NS_ERROR_NOT_AVAILABLE);
}
let returnValue = win.modalReturnValue;
delete win.modalReturnValue;
utils.leaveModalStateWithWindow(modalStateWin);
debug("Leaving modal state (outerID=" + outerWindowID + ", " +
"innerID=" + innerWindowID + ")");
return returnValue;
},
_recvStopWaiting: function(msg) {
let outerID = msg.json.windowID.outer;
let innerID = msg.json.windowID.inner;
let returnValue = msg.json.returnValue;
debug("recvStopWaiting(outer=" + outerID + ", inner=" + innerID +
", returnValue=" + returnValue + ")");
if (!this._windowIDDict[outerID]) {
debug("recvStopWaiting: No record of outer window ID " + outerID);
return;
}
let win = this._windowIDDict[outerID].get();
delete this._windowIDDict[outerID];
if (!win) {
debug("recvStopWaiting, but window is gone\n");
return;
}
if (innerID !== this._tryGetInnerWindowID(win)) {
debug("recvStopWaiting, but inner ID has changed\n");
return;
}
debug("recvStopWaiting " + win);
win.modalReturnValue = returnValue;
win.modalDepth--;
},
_titleChangedHandler: function(e) {
debug("Got titlechanged: (" + e.target.title + ")");
var win = e.target.defaultView;
@ -179,8 +320,7 @@ BrowserElementChild.prototype = {
// to keep a strong ref to it ourselves.
_progressListener: {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
Ci.nsISupports]),
Ci.nsISupportsWeakReference]),
_seenLoadStart: false,
onLocationChange: function(webProgress, request, location, flags) {

View File

@ -18,6 +18,14 @@ function debug(msg) {
//dump("BrowserElementParent - " + msg + "\n");
}
function sendAsyncMsg(frameElement, msg, data) {
let mm = frameElement.QueryInterface(Ci.nsIFrameLoaderOwner)
.frameLoader
.messageManager;
mm.sendAsyncMessage('browser-element-api:' + msg, data);
}
/**
* The BrowserElementParent implements one half of <iframe mozbrowser>.
* (The other half is, unsurprisingly, BrowserElementChild.)
@ -38,8 +46,6 @@ BrowserElementParent.prototype = {
* changed.
*/
_init: function() {
debug("_init");
if (this._initialized) {
return;
}
@ -52,6 +58,7 @@ BrowserElementParent.prototype = {
return;
}
debug("_init");
this._initialized = true;
this._screenshotListeners = {};
@ -107,6 +114,7 @@ BrowserElementParent.prototype = {
addMessageListener("iconchange", this._fireEventFromMsg);
addMessageListener("get-mozapp-manifest-url", this._sendMozAppManifestURL);
addMessageListener("keyevent", this._fireKeyEvent);
addMessageListener("showmodalprompt", this._handleShowModalPrompt);
mm.addMessageListener('browser-element-api:got-screenshot',
this._recvGotScreenshot.bind(this));
@ -129,24 +137,83 @@ BrowserElementParent.prototype = {
* |data|.
*/
_fireEventFromMsg: function(frameElement, data) {
let name = data.name;
let name = data.name.substring('browser-element-api:'.length);
let detail = data.json;
debug('fireEventFromMsg: ' + name + ' ' + detail);
let evtName = name.substring('browser-element-api:'.length);
debug('fireEventFromMsg: ' + name + ', ' + detail);
let evt = this._createEvent(frameElement, name, detail,
/* cancelable = */ false);
frameElement.dispatchEvent(evt);
},
_handleShowModalPrompt: function(frameElement, data) {
// Fire a showmodalprmopt event on the iframe. When this method is called,
// the child is spinning in a nested event loop waiting for an
// unblock-modal-prompt message.
//
// If the embedder calls preventDefault() on the showmodalprompt event,
// we'll block the child until event.detail.unblock() is called.
//
// Otherwise, if preventDefault() is not called, we'll send the
// unblock-modal-prompt message to the child as soon as the event is done
// dispatching.
let detail = data.json;
debug('handleShowPrompt ' + JSON.stringify(detail));
// Strip off the windowID property from the object we send along in the
// event.
let windowID = detail.windowID;
delete detail.windowID;
debug("Event will have detail: " + JSON.stringify(detail));
let evt = this._createEvent(frameElement, 'showmodalprompt', detail,
/* cancelable = */ true);
let unblockMsgSent = false;
function sendUnblockMsg() {
if (unblockMsgSent) {
return;
}
unblockMsgSent = true;
// We don't need to sanitize evt.detail.returnValue (e.g. converting the
// return value of confirm() to a boolean); Gecko does that for us.
let data = { windowID: windowID,
returnValue: evt.detail.returnValue };
sendAsyncMsg(frameElement, 'unblock-modal-prompt', data);
}
XPCNativeWrapper.unwrap(evt.detail).unblock = function() {
sendUnblockMsg();
};
frameElement.dispatchEvent(evt);
if (!evt.defaultPrevented) {
// Unblock the inner frame immediately. Otherwise we'll unblock upon
// evt.detail.unblock().
sendUnblockMsg();
}
},
_createEvent: function(frameElement, evtName, detail, cancelable) {
let win = frameElement.ownerDocument.defaultView;
let evt;
// This will have to change if we ever want to send a CustomEvent with null
// detail. For now, it's OK.
if (detail !== undefined && detail !== null) {
evt = new win.CustomEvent('mozbrowser' + evtName, {detail: detail});
evt = new win.CustomEvent('mozbrowser' + evtName,
{bubbles: true, cancelable: cancelable,
detail: detail});
}
else {
evt = new win.Event('mozbrowser' + evtName);
evt = new win.Event('mozbrowser' + evtName,
{bubbles: true, cancelable: cancelable});
}
frameElement.dispatchEvent(evt);
return evt;
},
_sendMozAppManifestURL: function(frameElement, data) {

View File

@ -0,0 +1,189 @@
/* 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/. */
/* vim: set ft=javascript : */
"use strict";
let Cu = Components.utils;
let Ci = Components.interfaces;
let Cc = Components.classes;
let Cr = Components.results;
let Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
let EXPORTED_SYMBOLS = ["BrowserElementPromptService"];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
function debug(msg) {
//dump("BrowserElementPromptService - " + msg + "\n");
}
function BrowserElementPrompt(win, browserElementChild) {
this._win = win;
this._browserElementChild = browserElementChild;
}
BrowserElementPrompt.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
alert: function(title, text) {
this._browserElementChild.showModalPrompt(
this._win, {promptType: "alert", title: title, message: text});
},
alertCheck: function(title, text, checkMsg, checkState) {
// Treat this like a normal alert() call, ignoring the checkState. The
// front-end can do its own suppression of the alert() if it wants.
this.alert(title, text);
},
confirm: function(title, text) {
return this._browserElementChild.showModalPrompt(
this._win, {promptType: "confirm", title: title, message: text});
},
confirmCheck: function(title, text, checkMsg, checkState) {
return this.confirm(title, text);
},
confirmEx: function(title, text, buttonFlags, button0Title, button1Title, button2Title, checkMsg, checkState) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
prompt: function(title, text, value, checkMsg, checkState) {
let rv = this._browserElementChild.showModalPrompt(
this._win,
{ promptType: "prompt",
title: title,
message: text,
initialValue: value.value,
returnValue: null });
value.value = rv;
// nsIPrompt::Prompt returns true if the user pressed "OK" at the prompt,
// and false if the user pressed "Cancel".
//
// BrowserElementChild returns null for "Cancel" and returns the string the
// user entered otherwise.
return rv !== null;
},
promptUsernameAndPassword: function(title, text, username, password, checkMsg, checkState) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
promptPassword: function(title, text, password, checkMsg, checkState) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
select: function(title, text, aCount, aSelectList, aOutSelection) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
};
function BrowserElementPromptFactory(toWrap) {
this._wrapped = toWrap;
}
BrowserElementPromptFactory.prototype = {
classID: Components.ID("{24f3d0cf-e417-4b85-9017-c9ecf8bb1299}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptFactory]),
getPrompt: function(win, iid) {
// Try to find a browserelementchild for the window.
if (iid.number != Ci.nsIPrompt.number) {
debug("Falling back to wrapped prompt service because " +
"we don't recognize the requested IID (" + iid + ", " +
"nsIPrompt=" + Ci.nsIPrompt);
return this._wrapped.getPrompt(win, iid);
}
let browserElementChild =
BrowserElementPromptService.getBrowserElementChildForWindow(win);
if (!browserElementChild) {
debug("Falling back to wrapped prompt service because " +
"we can't find a browserElementChild for " +
win + ", " + win.location);
return this._wrapped.getPrompt(win, iid);
}
debug("Returning wrapped getPrompt for " + win);
return new BrowserElementPrompt(win, browserElementChild)
.QueryInterface(iid);
}
};
let BrowserElementPromptService = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
_init: function() {
var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
os.addObserver(this, "outer-window-destroyed", /* ownsWeak = */ true);
// Wrap the existing @mozilla.org/prompter;1 implementation.
var contractID = "@mozilla.org/prompter;1";
var oldCID = Cm.contractIDToCID(contractID);
var newCID = BrowserElementPromptFactory.prototype.classID;
var oldFactory = Cm.getClassObject(Cc[contractID], Ci.nsIFactory);
if (oldCID == newCID) {
debug("WARNING: Wrapped prompt factory is already installed!");
return;
}
Cm.unregisterFactory(oldCID, oldFactory);
var oldInstance = oldFactory.createInstance(null, Ci.nsIPromptFactory);
var newInstance = new BrowserElementPromptFactory(oldInstance);
var newFactory = {
createInstance: function(outer, iid) {
if (outer != null) {
throw Cr.NS_ERROR_NO_AGGREGATION;
}
return newInstance.QueryInterface(iid);
}
};
Cm.registerFactory(newCID,
"BrowserElementPromptService's prompter;1 wrapper",
contractID, newFactory);
debug("Done installing new prompt factory.");
},
_getOuterWindowID: function(win) {
return win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
},
_browserElementChildMap: {},
mapWindowToBrowserElementChild: function(win, browserElementChild) {
this._browserElementChildMap[this._getOuterWindowID(win)] = browserElementChild;
},
getBrowserElementChildForWindow: function(win) {
return this._browserElementChildMap[this._getOuterWindowID(win)];
},
_observeOuterWindowDestroyed: function(outerWindowID) {
let id = outerWindowID.QueryInterface(Ci.nsISupportsPRUint64).data;
debug("observeOuterWindowDestroyed " + id);
delete this._browserElementChildMap[outerWindowID.data];
},
observe: function(subject, topic, data) {
switch(topic) {
case "outer-window-destroyed":
this._observeOuterWindowDestroyed(subject);
break;
default:
debug("Observed unexpected topic " + topic);
}
},
};
BrowserElementPromptService._init();

View File

@ -32,6 +32,7 @@ EXTRA_JS_MODULES = ConsoleAPIStorage.jsm \
EXTRA_JS_MODULES += \
DOMRequestHelper.jsm \
IndexedDBHelper.jsm \
BrowserElementPromptService.jsm \
$(NULL)
XPIDLSRCS = \

View File

@ -1793,7 +1793,6 @@ nsDOMWindowUtils::LeaveModalStateWithWindow(nsIDOMWindow *aWindow)
nsCOMPtr<nsPIDOMWindow> window = do_QueryReferent(mWindow);
NS_ENSURE_STATE(window);
NS_ENSURE_ARG_POINTER(aWindow);
window->LeaveModalState(aWindow);
return NS_OK;
}

View File

@ -2473,7 +2473,7 @@ nsGlobalWindow::PreHandleEvent(nsEventChainPreVisitor& aVisitor)
bool
nsGlobalWindow::DialogOpenAttempted()
{
nsGlobalWindow *topWindow = GetTop();
nsGlobalWindow *topWindow = GetScriptableTop();
if (!topWindow) {
NS_ERROR("DialogOpenAttempted() called without a top window?");
@ -2507,7 +2507,7 @@ nsGlobalWindow::DialogOpenAttempted()
bool
nsGlobalWindow::AreDialogsBlocked()
{
nsGlobalWindow *topWindow = GetTop();
nsGlobalWindow *topWindow = GetScriptableTop();
if (!topWindow) {
NS_ASSERTION(!mDocShell, "AreDialogsBlocked() called without a top window?");
@ -2558,7 +2558,7 @@ nsGlobalWindow::ConfirmDialogAllowed()
void
nsGlobalWindow::PreventFurtherDialogs()
{
nsGlobalWindow *topWindow = GetTop();
nsGlobalWindow *topWindow = GetScriptableTop();
if (!topWindow) {
NS_ERROR("PreventFurtherDialogs() called without a top window?");
@ -6543,7 +6543,9 @@ nsGlobalWindow::ReallyCloseWindow()
nsIDOMWindow *
nsGlobalWindow::EnterModalState()
{
nsGlobalWindow* topWin = GetTop();
// GetScriptableTop, not GetTop, so that EnterModalState works properly with
// <iframe mozbrowser>.
nsGlobalWindow* topWin = GetScriptableTop();
if (!topWin) {
NS_ERROR("Uh, EnterModalState() called w/o a reachable top window?");
@ -6672,7 +6674,7 @@ private:
void
nsGlobalWindow::LeaveModalState(nsIDOMWindow *aCallerWin)
{
nsGlobalWindow *topWin = GetTop();
nsGlobalWindow* topWin = GetScriptableTop();
if (!topWin) {
NS_ERROR("Uh, LeaveModalState() called w/o a reachable top window?");
@ -6714,7 +6716,7 @@ nsGlobalWindow::LeaveModalState(nsIDOMWindow *aCallerWin)
bool
nsGlobalWindow::IsInModalState()
{
nsGlobalWindow *topWin = GetTop();
nsGlobalWindow *topWin = GetScriptableTop();
if (!topWin) {
NS_ERROR("Uh, IsInModalState() called w/o a reachable top window?");

View File

@ -379,7 +379,17 @@ public:
nsCOMPtr<nsIDOMWindow> top;
GetTop(getter_AddRefs(top));
if (top)
return static_cast<nsGlobalWindow *>(static_cast<nsIDOMWindow *>(top.get()));
return static_cast<nsGlobalWindow *>(top.get());
return nsnull;
}
inline nsGlobalWindow* GetScriptableTop()
{
nsCOMPtr<nsIDOMWindow> top;
GetScriptableTop(getter_AddRefs(top));
if (top) {
return static_cast<nsGlobalWindow *>(top.get());
}
return nsnull;
}