Bug 1005818 - Part 2: Only limited browser API are available to a widget. r=kanru

1. Add |ownerIsWidget| in nsIFrameLoader.idl
2. Add |GetReallyIsWidget| in nsIMozBrowserFrame.idl
3. Hide the methods of browser API of a widget
4. Hide security-sensitive mozbrowser events of a widget
This commit is contained in:
Junior Hsu 2014-08-19 15:14:22 +01:00
parent 7c8c44b873
commit 012e6ed0a7
12 changed files with 305 additions and 135 deletions

View File

@ -112,7 +112,7 @@ interface nsIContentViewManager : nsISupports
readonly attribute nsIContentView rootContentView;
};
[scriptable, builtinclass, uuid(d52ca6a8-8237-4ae0-91d7-7be4f1db24ef)]
[scriptable, builtinclass, uuid(55a772b8-855a-4c5f-b4a1-284b6b3bec28)]
interface nsIFrameLoader : nsISupports
{
/**
@ -291,6 +291,13 @@ interface nsIFrameLoader : nsISupports
* Especially, a widget frame is regarded as an app frame.
*/
readonly attribute boolean ownerIsBrowserOrAppFrame;
/**
* Find out whether the owner content really is a widget. If this attribute
* returns true, |ownerIsBrowserOrAppFrame| must return true.
*/
readonly attribute boolean ownerIsWidget;
};
%{C++

View File

@ -1474,6 +1474,22 @@ nsFrameLoader::GetOwnerIsBrowserOrAppFrame(bool* aResult)
return NS_OK;
}
bool
nsFrameLoader::OwnerIsWidget()
{
nsCOMPtr<nsIMozBrowserFrame> browserFrame = do_QueryInterface(mOwnerContent);
return browserFrame ? browserFrame->GetReallyIsWidget() : false;
}
// The xpcom getter version
NS_IMETHODIMP
nsFrameLoader::GetOwnerIsWidget(bool* aResult)
{
*aResult = OwnerIsWidget();
return NS_OK;
}
bool
nsFrameLoader::OwnerIsAppFrame()
{

View File

@ -333,6 +333,12 @@ private:
*/
bool OwnerIsBrowserOrAppFrame();
/**
* Is this a frameloader for a bona fide <iframe mozwidget>? (I.e., does the
* frame return true for nsIMozBrowserFrame::GetReallyIsWidget()?)
*/
bool OwnerIsWidget();
/**
* Is this a frameloader for a bona fide <iframe mozapp>? (I.e., does the
* frame return true for nsIMozBrowserFrame::GetReallyIsApp()?)

View File

@ -319,6 +319,44 @@ nsGenericHTMLFrameElement::GetReallyIsApp(bool *aOut)
return NS_OK;
}
namespace {
bool WidgetsEnabled()
{
static bool sMozWidgetsEnabled = false;
static bool sBoolVarCacheInitialized = false;
if (!sBoolVarCacheInitialized) {
sBoolVarCacheInitialized = true;
Preferences::AddBoolVarCache(&sMozWidgetsEnabled,
"dom.enable_widgets");
}
return sMozWidgetsEnabled;
}
} // anonymous namespace
/* [infallible] */ NS_IMETHODIMP
nsGenericHTMLFrameElement::GetReallyIsWidget(bool *aOut)
{
*aOut = false;
if (!WidgetsEnabled()) {
return NS_OK;
}
nsAutoString appManifestURL;
GetManifestURLByType(nsGkAtoms::mozapp, appManifestURL);
bool isApp = !appManifestURL.IsEmpty();
nsAutoString widgetManifestURL;
GetManifestURLByType(nsGkAtoms::mozwidget, widgetManifestURL);
bool isWidget = !widgetManifestURL.IsEmpty();
*aOut = isWidget && !isApp;
return NS_OK;
}
/* [infallible] */ NS_IMETHODIMP
nsGenericHTMLFrameElement::GetIsExpectingSystemMessage(bool *aOut)
{
@ -404,7 +442,7 @@ nsGenericHTMLFrameElement::GetAppManifestURL(nsAString& aOut)
GetManifestURLByType(nsGkAtoms::mozapp, appManifestURL);
if (Preferences::GetBool("dom.enable_widgets")) {
if (WidgetsEnabled()) {
GetManifestURLByType(nsGkAtoms::mozwidget, widgetManifestURL);
}

View File

@ -46,7 +46,7 @@ function handleRequest(request, response) {
// Get the app type.
var apptype = query.apptype;
if (apptype != 'hosted' && apptype != 'cached' && apptype != 'widget')
if (apptype != 'hosted' && apptype != 'cached' && apptype != 'widget' && apptype != 'invalidWidget')
throw "Invalid app type: " + apptype;
// Get the version from server state and handle the etag.
@ -83,7 +83,7 @@ function handleRequest(request, response) {
response.write(makeResource(gAppcacheTemplatePath, version, apptype));
return;
}
else if (apptype == 'widget')
else if (apptype == 'widget' || apptype == 'invalidWidget')
{
response.setHeader("Content-Type", "text/html", false);
response.write(makeResource(gWidgetTemplatePath, version, apptype));

View File

@ -0,0 +1,11 @@
{
"name": "Really Rapid Release (invalid widget)",
"description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.",
"launch_path": "/tests/dom/apps/tests/file_app.sjs?apptype=invalidWidget",
"icons": {
"128": "ICONTOKEN"
},
"widgetPages": [
"/tests/dom/apps/tests/file_app.sjs?apptype=widget"
]
}

View File

@ -1,68 +1,8 @@
<html>
<head>
<script>
function sendMessage(msg) {
alert(msg);
}
function ok(p, msg) {
if (p)
sendMessage("OK: " + msg);
else
sendMessage("KO: " + msg);
}
function is(a, b, msg) {
if (a == b)
sendMessage("OK: " + a + " == " + b + " - " + msg);
else
sendMessage("KO: " + a + " != " + b + " - " + msg);
}
function installed(p) {
if (p)
sendMessage("IS_INSTALLED");
else
sendMessage("NOT_INSTALLED");
}
function finish() {
sendMessage("VERSION: MyWebApp vVERSIONTOKEN");
sendMessage("DONE");
}
function cbError() {
ok(false, "Error callback invoked");
finish();
}
function go() {
ok(true, "Launched app");
var request = window.navigator.mozApps.getSelf();
request.onsuccess = function() {
var widget = request.result;
ok(widget,"Should be a widget");
checkWidget(widget);
}
request.onerror = cbError;
}
function checkWidget(widget) {
// If the widget is installed, |widget| will be non-null. If it is, verify its state.
installed(!!widget);
if (widget) {
var widgetName = "Really Rapid Release (APPTYPETOKEN)";
var manifest = SpecialPowers.wrap(widget.manifest);
is(manifest.name, widgetName, "Manifest name should be correct");
is(widget.origin, "http://test", "Widget origin should be correct");
is(widget.installOrigin, "http://mochi.test:8888", "Install origin should be correct");
}
finish();
}
</script>
</head>
<body onload="go();">
<body>
App Body. Version: VERSIONTOKEN
</body></html>
<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />
</body>
</html>

View File

@ -7,6 +7,7 @@ support-files =
file_cached_app.template.appcache
file_cached_app.template.webapp
file_hosted_app.template.webapp
file_invalidWidget_app.template.webapp
file_packaged_app.sjs
file_packaged_app.template.html
file_packaged_app.template.webapp

View File

@ -11,16 +11,17 @@
<script type="application/javascript;version=1.7">
var gWidgetManifestURL = 'http://test/tests/dom/apps/tests/file_app.sjs?apptype=widget&getmanifest=true';
var gInvalidWidgetManifestURL = 'http://test/tests/dom/apps/tests/file_app.sjs?apptype=invalidWidget&getmanifest=true';
var gApp;
function cbError() {
function onError() {
ok(false, "Error callback invoked");
finish();
}
function installApp() {
var request = navigator.mozApps.install(gWidgetManifestURL);
request.onerror = cbError;
function installApp(path) {
var request = navigator.mozApps.install(path);
request.onerror = onError;
request.onsuccess = function() {
gApp = request.result;
@ -31,40 +32,134 @@
function uninstallApp() {
// Uninstall the app.
var request = navigator.mozApps.mgmt.uninstall(gApp);
request.onerror = cbError;
request.onerror = onError;
request.onsuccess = function() {
// All done.
info("All done");
runTest();
}
}
function testApp() {
function testApp(isValidWidget) {
info("Test widget feature. IsValidWidget: " + isValidWidget);
var ifr = document.createElement('iframe');
ifr.setAttribute('mozbrowser', 'true');
ifr.setAttribute('mozwidget', gApp.manifestURL);
ifr.setAttribute('src', gApp.origin+gApp.manifest.launch_path);
var domParent = document.getElementById('container');
domParent.appendChild(ifr);
// Set us up to listen for messages from the app.
var listener = function(e) {
var message = e.detail.message;
if (/^OK/.exec(message)) {
ok(true, "Message from widget: " + message);
} else if (/KO/.exec(message)) {
ok(false, "Message from widget: " + message);
} else if (/DONE/.exec(message)) {
ok(true, "Message from widget complete");
ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
domParent.removeChild(ifr);
runTest();
var mm = SpecialPowers.getBrowserFrameMessageManager(ifr);
mm.addMessageListener('OK', function(msg) {
ok(isValidWidget, "Message from widget: " + SpecialPowers.wrap(msg).json);
});
mm.addMessageListener('KO', function(msg) {
ok(!isValidWidget, "Message from widget: " + SpecialPowers.wrap(msg).json);
});
mm.addMessageListener('DONE', function(msg) {
ok(true, "Message from widget complete: "+SpecialPowers.wrap(msg).json);
domParent.removeChild(ifr);
runTest();
});
ifr.addEventListener('mozbrowserloadend', function() {
ok(true, "receive mozbrowserloadend");
// Test limited browser API feature only for valid widget case
if (isValidWidget) {
testLimitedBrowserAPI(ifr);
}
SimpleTest.executeSoon(()=>loadFrameScript(mm));
}, false);
// Test limited browser API feature only for valid widget case
if (!isValidWidget) {
return;
}
// This event is triggered when the app calls "alert".
ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
domParent.appendChild(ifr);
[
'mozbrowsertitlechange',
'mozbrowseropenwindow',
'mozbrowserscroll',
'mozbrowserasyncscroll'
].forEach( function(topic) {
ifr.addEventListener(topic, function() {
ok(false, topic + " should be hidden");
}, false);
});
}
function testLimitedBrowserAPI(ifr) {
var securitySensitiveCalls = [
'sendMouseEvent',
'sendTouchEvent',
'goBack',
'goForward',
'reload',
'stop',
'download',
'purgeHistory',
'getScreenshot',
'zoom',
'getCanGoBack',
'getCanGoForward'
];
securitySensitiveCalls.forEach( function(call) {
is(typeof ifr[call], "undefined", call + " should be hidden for widget");
});
}
function loadFrameScript(mm) {
var script = 'data:,\
function ok(p, msg) { \
if (p) { \
sendAsyncMessage("OK", msg); \
} else { \
sendAsyncMessage("KO", msg); \
} \
} \
\
function is(a, b, msg) { \
if (a == b) { \
sendAsyncMessage("OK", a + " == " + b + " - " + msg); \
} else { \
sendAsyncMessage("KO", a + " != " + b + " - " + msg); \
} \
} \
\
function finish() { \
sendAsyncMessage("DONE",""); \
} \
\
function onError() { \
ok(false, "Error callback invoked"); \
finish(); \
} \
\
function checkWidget(widget) { \
/*For invalid widget case, ignore the following check*/\
if (widget) { \
var widgetName = "Really Rapid Release (APPTYPETOKEN)"; \
is(widget.origin, "http://test", "Widget origin should be correct"); \
is(widget.installOrigin, "http://mochi.test:8888", "Install origin should be correct"); \
} \
finish(); \
} \
\
var request = content.window.navigator.mozApps.getSelf(); \
request.onsuccess = function() { \
var widget = request.result; \
ok(widget,"Should be a widget"); \
checkWidget(widget); \
}; \
request.onerror = onError; \
content.window.open("about:blank"); /*test mozbrowseropenwindow*/ \
content.window.scrollTo(4000, 4000); /*test mozbrowser(async)scroll*/ \
';
mm.loadFrameScript(script, /* allowDelayedLoad = */ false);
}
var tests = [
@ -93,10 +188,19 @@
},
// Installing the app
installApp,
()=>installApp(gWidgetManifestURL),
// Run tests in app
testApp,
()=>testApp(true),
// Uninstall the app
uninstallApp,
// Installing the app for invalid widget case
()=>installApp(gInvalidWidgetManifestURL),
// Run tests in app for invalid widget case
()=>testApp(false),
// Uninstall the app
uninstallApp

View File

@ -164,6 +164,13 @@ BrowserElementParent::DispatchOpenWindowEvent(Element* aOpenerFrameElement,
return BrowserElementParent::OPEN_WINDOW_IGNORED;
}
// Do not dispatch a mozbrowseropenwindow event of a widget to its embedder
nsCOMPtr<nsIMozBrowserFrame> browserFrame =
do_QueryInterface(aOpenerFrameElement);
if (browserFrame && browserFrame->GetReallyIsWidget()) {
return BrowserElementParent::OPEN_WINDOW_CANCELLED;
}
nsEventStatus status;
bool dispatchSucceeded =
DispatchCustomDOMEvent(aOpenerFrameElement,
@ -349,6 +356,14 @@ BrowserElementParent::DispatchAsyncScrollEvent(TabParent* aTabParent,
const CSSRect& aContentRect,
const CSSSize& aContentSize)
{
// Do not dispatch a mozbrowserasyncscroll event of a widget to its embedder
nsCOMPtr<Element> frameElement = aTabParent->GetOwnerElement();
NS_ENSURE_TRUE(frameElement, false);
nsCOMPtr<nsIMozBrowserFrame> browserFrame = do_QueryInterface(frameElement);
if (browserFrame && browserFrame->GetReallyIsWidget()) {
return true;
}
nsRefPtr<DispatchAsyncScrollEventRunnable> runnable =
new DispatchAsyncScrollEventRunnable(aTabParent, aContentRect,
aContentSize);

View File

@ -116,24 +116,30 @@ function BrowserElementParent(frameLoader, hasRemoteFrame, isPendingFrame) {
// Define methods on the frame element.
defineNoReturnMethod('setVisible', this._setVisible);
defineDOMRequestMethod('getVisible', 'get-visible');
defineNoReturnMethod('sendMouseEvent', this._sendMouseEvent);
// 0 = disabled, 1 = enabled, 2 - auto detect
if (getIntPref(TOUCH_EVENTS_ENABLED_PREF, 0) != 0) {
defineNoReturnMethod('sendTouchEvent', this._sendTouchEvent);
// Not expose security sensitive browser API for widgets
if (!this._frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerIsWidget) {
defineNoReturnMethod('sendMouseEvent', this._sendMouseEvent);
// 0 = disabled, 1 = enabled, 2 - auto detect
if (getIntPref(TOUCH_EVENTS_ENABLED_PREF, 0) != 0) {
defineNoReturnMethod('sendTouchEvent', this._sendTouchEvent);
}
defineNoReturnMethod('goBack', this._goBack);
defineNoReturnMethod('goForward', this._goForward);
defineNoReturnMethod('reload', this._reload);
defineNoReturnMethod('stop', this._stop);
defineMethod('download', this._download);
defineDOMRequestMethod('purgeHistory', 'purge-history');
defineMethod('getScreenshot', this._getScreenshot);
defineNoReturnMethod('zoom', this._zoom);
defineDOMRequestMethod('getCanGoBack', 'get-can-go-back');
defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward');
}
defineNoReturnMethod('goBack', this._goBack);
defineNoReturnMethod('goForward', this._goForward);
defineNoReturnMethod('reload', this._reload);
defineNoReturnMethod('stop', this._stop);
defineNoReturnMethod('zoom', this._zoom);
defineMethod('download', this._download);
defineDOMRequestMethod('purgeHistory', 'purge-history');
defineMethod('getScreenshot', this._getScreenshot);
defineMethod('addNextPaintListener', this._addNextPaintListener);
defineMethod('removeNextPaintListener', this._removeNextPaintListener);
defineDOMRequestMethod('getCanGoBack', 'get-can-go-back');
defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward');
let principal = this._frameElement.ownerDocument.nodePrincipal;
let perm = Services.perms
@ -217,6 +223,9 @@ BrowserElementParent.prototype = {
_setupMessageListener: function() {
this._mm = this._frameLoader.messageManager;
let self = this;
let isWidget = this._frameLoader
.QueryInterface(Ci.nsIFrameLoader)
.ownerIsWidget;
// Messages we receive are handed to functions which take a (data) argument,
// where |data| is the message manager's data object.
@ -224,26 +233,14 @@ BrowserElementParent.prototype = {
// on data.msg_name
let mmCalls = {
"hello": this._recvHello,
"contextmenu": this._fireCtxMenuEvent,
"locationchange": this._fireEventFromMsg,
"loadstart": this._fireProfiledEventFromMsg,
"loadend": this._fireProfiledEventFromMsg,
"titlechange": this._fireProfiledEventFromMsg,
"iconchange": this._fireEventFromMsg,
"manifestchange": this._fireEventFromMsg,
"metachange": this._fireEventFromMsg,
"close": this._fireEventFromMsg,
"resize": this._fireEventFromMsg,
"activitydone": this._fireEventFromMsg,
"opensearch": this._fireEventFromMsg,
"securitychange": this._fireEventFromMsg,
"error": this._fireEventFromMsg,
"scroll": this._fireEventFromMsg,
"firstpaint": this._fireProfiledEventFromMsg,
"documentfirstpaint": this._fireProfiledEventFromMsg,
"nextpaint": this._recvNextPaint,
"keyevent": this._fireKeyEvent,
"showmodalprompt": this._handleShowModalPrompt,
"got-purge-history": this._gotDOMRequestResult,
"got-screenshot": this._gotDOMRequestResult,
"got-can-go-back": this._gotDOMRequestResult,
@ -257,9 +254,31 @@ BrowserElementParent.prototype = {
"selectionchange": this._handleSelectionChange
};
let mmSecuritySensitiveCalls = {
"showmodalprompt": this._handleShowModalPrompt,
"contextmenu": this._fireCtxMenuEvent,
"securitychange": this._fireEventFromMsg,
"locationchange": this._fireEventFromMsg,
"iconchange": this._fireEventFromMsg,
"titlechange": this._fireProfiledEventFromMsg,
"opensearch": this._fireEventFromMsg,
"manifestchange": this._fireEventFromMsg,
"metachange": this._fireEventFromMsg,
"resize": this._fireEventFromMsg,
"activitydone": this._fireEventFromMsg,
"scroll": this._fireEventFromMsg
};
this._mm.addMessageListener('browser-element-api:call', function(aMsg) {
if (self._isAlive() && (aMsg.data.msg_name in mmCalls)) {
if (!self._isAlive()) {
return;
}
if (aMsg.data.msg_name in mmCalls) {
return mmCalls[aMsg.data.msg_name].apply(self, arguments);
} else if (!isWidget && aMsg.data.msg_name in mmSecuritySensitiveCalls) {
return mmSecuritySensitiveCalls[aMsg.data.msg_name]
.apply(self, arguments);
}
});
},
@ -294,26 +313,29 @@ BrowserElementParent.prototype = {
}
};
if (authDetail.isOnlyPassword) {
// We don't handle password-only prompts, so just cancel it.
// 1. We don't handle password-only prompts.
// 2. We don't handle for widget case because of security concern.
if (authDetail.isOnlyPassword ||
this._frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerIsWidget) {
cancelCallback();
return;
} else { /* username and password */
let detail = {
host: authDetail.host,
realm: authDetail.realm
};
evt = this._createEvent('usernameandpasswordrequired', detail,
/* cancelable */ true);
Cu.exportFunction(function(username, password) {
if (callbackCalled)
return;
callbackCalled = true;
callback(true, username, password);
}, evt.detail, { defineAs: 'authenticate' });
}
/* username and password */
let detail = {
host: authDetail.host,
realm: authDetail.realm
};
evt = this._createEvent('usernameandpasswordrequired', detail,
/* cancelable */ true);
Cu.exportFunction(function(username, password) {
if (callbackCalled)
return;
callbackCalled = true;
callback(true, username, password);
}, evt.detail, { defineAs: 'authenticate' });
Cu.exportFunction(cancelCallback, evt.detail, { defineAs: 'cancel' });
this._frameElement.dispatchEvent(evt);

View File

@ -9,7 +9,7 @@
interface nsITabParent;
[scriptable, builtinclass, uuid(929AED00-3E15-49B7-8CA2-75003715B7E7)]
[scriptable, builtinclass, uuid(0c0a862c-1a47-43c0-ae9e-d51835e3e1a6)]
interface nsIMozBrowserFrame : nsIDOMMozBrowserFrame
{
/**
@ -33,6 +33,16 @@ interface nsIMozBrowserFrame : nsIDOMMozBrowserFrame
*/
[infallible] readonly attribute boolean reallyIsApp;
/**
* Gets whether this frame really is a widget frame.
*
* In order to really be a frame, this frame must really be a browser
* frame (this requirement will go away eventually), the frame's mozwidget
* attribute must point to the manifest of a valid app, and the src should
* be in the |widgetPages| specified by the manifest.
*/
[infallible] readonly attribute boolean reallyIsWidget;
/**
* This corresponds to the expecting-system-message attribute, which tells us
* whether we should expect that this frame will receive a system message once