Bug 1174733 - Browser API: iframe.executeScript. r=kanru, r=bholley

This commit is contained in:
Paul Rouget 2015-07-16 03:56:00 -04:00
Родитель 3ff8c1f3fa
Коммит dfe7fcd341
13 изменённых файлов: 349 добавлений и 1 удалений

Просмотреть файл

@ -143,6 +143,12 @@ this.PermissionsTable = { geolocation: {
privileged: ALLOW_ACTION,
certified: ALLOW_ACTION
},
"browser:universalxss": {
app: DENY_ACTION,
trusted: DENY_ACTION,
privileged: ALLOW_ACTION,
certified: ALLOW_ACTION
},
bluetooth: {
app: DENY_ACTION,
trusted: DENY_ACTION,

Просмотреть файл

@ -238,6 +238,7 @@ BrowserElementChild.prototype = {
"find-all": this._recvFindAll.bind(this),
"find-next": this._recvFindNext.bind(this),
"clear-match": this._recvClearMatch.bind(this),
"execute-script": this._recvExecuteScript,
"get-audio-channel-volume": this._recvGetAudioChannelVolume,
"set-audio-channel-volume": this._recvSetAudioChannelVolume,
"get-audio-channel-muted": this._recvGetAudioChannelMuted,
@ -984,6 +985,90 @@ BrowserElementChild.prototype = {
takeScreenshotClosure, maxDelayMS);
},
_recvExecuteScript: function(data) {
debug("Received executeScript message: (" + data.json.id + ")");
let domRequestID = data.json.id;
let sendError = errorMsg => sendAsyncMsg("execute-script-done", {
errorMsg,
id: domRequestID
});
let sendSuccess = successRv => sendAsyncMsg("execute-script-done", {
successRv,
id: domRequestID
});
let isJSON = obj => {
try {
JSON.stringify(obj);
} catch(e) {
return false;
}
return true;
}
let expectedOrigin = data.json.args.options.origin;
let expectedUrl = data.json.args.options.url;
if (expectedOrigin) {
if (expectedOrigin != content.location.origin) {
sendError("Origin mismatches");
return;
}
}
if (expectedUrl) {
let expectedURI
try {
expectedURI = Services.io.newURI(expectedUrl, null, null);
} catch(e) {
sendError("Malformed URL");
return;
}
let currentURI = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
if (!currentURI.equalsExceptRef(expectedURI)) {
sendError("URL mismatches");
return;
}
}
let sandbox = new Cu.Sandbox([content], {
sandboxPrototype: content,
sandboxName: "browser-api-execute-script",
allowWaivers: false,
sameZoneAs: content
});
try {
let sandboxRv = Cu.evalInSandbox(data.json.args.script, sandbox, "1.8");
if (sandboxRv instanceof Promise) {
sandboxRv.then(rv => {
if (isJSON(rv)) {
sendSuccess(rv);
} else {
sendError("Value returned (resolve) by promise is not a valid JSON object");
}
}, error => {
if (isJSON(error)) {
sendError(error);
} else {
sendError("Value returned (reject) by promise is not a valid JSON object");
}
});
} else {
if (isJSON(sandboxRv)) {
sendSuccess(sandboxRv);
} else {
sendError("Script last expression must be a promise or a JSON object");
}
}
} catch(e) {
sendError(e.toString());
}
},
_recvGetContentDimensions: function(data) {
debug("Received getContentDimensions message: (" + data.json.id + ")");
sendAsyncMsg('got-contentdimensions', {

Просмотреть файл

@ -207,6 +207,7 @@ BrowserElementParent.prototype = {
"scrollviewchange": this._handleScrollViewChange,
"caretstatechanged": this._handleCaretStateChanged,
"findchange": this._handleFindChange,
"execute-script-done": this._gotDOMRequestResult,
"got-audio-channel-volume": this._gotDOMRequestResult,
"got-set-audio-channel-volume": this._gotDOMRequestResult,
"got-audio-channel-muted": this._gotDOMRequestResult,
@ -692,6 +693,19 @@ BrowserElementParent.prototype = {
this._sendAsyncMsg('stop');
}),
executeScript: function(script, options) {
if (!this._isAlive()) {
throw Components.Exception("Dead content process",
Cr.NS_ERROR_DOM_INVALID_STATE_ERR);
}
// Enforcing options.url or options.origin
if (!options.url && !options.origin) {
throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
}
return this._sendDOMRequest('execute-script', {script, options});
},
/*
* The valid range of zoom scale is defined in preference "zoom.maxPercent" and "zoom.minPercent".
*/

Просмотреть файл

@ -0,0 +1,119 @@
/* Any copyright is dedicated to the public domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Bug 1174733 - Browser API: iframe.executeScript
'use strict';
SimpleTest.waitForExplicitFinish();
browserElementTestHelpers.setEnabledPref(true);
function runTest() {
const origin = 'http://example.org';
const url = 'http://example.org/tests/dom/browser-element/mochitest/file_browserElement_ExecuteScript.html';
// Test if all key=>value pairs in o1 are present in o2.
const c = (o1, o2) => Object.keys(o1).every(k => o1[k] == o2[k]);
let scriptId = 0;
const bail = () => {
ok(false, `scriptId: ${scriptId++}`);
}
SpecialPowers.pushPermissions([
{type: 'browser', allow: 1, context: document},
{type: 'browser:universalxss', allow: 1, context: document}
], function() {
let iframe = document.createElement('iframe');
iframe.setAttribute('mozbrowser', 'true');
iframe.addEventListener('mozbrowserloadend', function onload() {
iframe.removeEventListener('mozbrowserloadend', onload);
onReady(iframe);
});
iframe.src = url;
document.body.appendChild(iframe);
});
function onReady(iframe) {
iframe.executeScript('4 + 4', {url}).then(rv => {
is(rv, 8, `scriptId: ${scriptId++}`);
return iframe.executeScript('(() => {return {a:42}})()', {url})
}, bail).then(rv => {
ok(c(rv, {a:42}), `scriptId: ${scriptId++}`);
return iframe.executeScript('(() => {return {a:42}})()', {origin})
}, bail).then(rv => {
ok(c(rv, {a:42}), `scriptId: ${scriptId++}`);
return iframe.executeScript('(() => {return {a:42}})()', {origin, url})
}, bail).then(rv => {
ok(c(rv, {a:42}), `scriptId: ${scriptId++}`);
return iframe.executeScript(`
new Promise((resolve, reject) => {
resolve(document.body.textContent.trim());
});
`, {url})
}, bail).then(rv => {
is(rv, 'foo', `scriptId: ${scriptId++}`);
return iframe.executeScript(`
new Promise((resolve, reject) => {
resolve({a:43,b:34});
});
`, {url})
}, bail).then(rv => {
ok(c(rv, {a:43,b:34}), `scriptId: ${scriptId++}`);
return iframe.executeScript(`
syntax error
`, {url});
}, bail).then(bail, (error) => {
is(error.name, 'SyntaxError: illegal character', `scriptId: ${scriptId++}`);
return iframe.executeScript(`
window
`, {url});
}).then(bail, (error) => {
is(error.name, 'Script last expression must be a promise or a JSON object', `scriptId: ${scriptId++}`);
return iframe.executeScript(`
new Promise((resolve, reject) => {
reject('BOOM');
});
`, {url});
}).then(bail, (error) => {
is(error.name, 'BOOM', `scriptId: ${scriptId++}`);
return iframe.executeScript(`
new Promise((resolve, reject) => {
resolve(window);
});
`, {url});
}).then(bail, (error) => {
is(error.name, 'Value returned (resolve) by promise is not a valid JSON object', `scriptId: ${scriptId++}`);
return iframe.executeScript('window.btoa("a")', {url})
}, bail).then(rv => {
ok(c(rv, 'YQ=='), `scriptId: ${scriptId++}`);
return iframe.executeScript('window.wrappedJSObject.btoa("a")', {url})
}, bail).then(bail, (error) => {
is(error.name, 'TypeError: window.wrappedJSObject is undefined', `scriptId: ${scriptId++}`);
return iframe.executeScript('42', {})
}).then(bail, error => {
is(error.name, 'InvalidAccessError', `scriptId: ${scriptId++}`);
return iframe.executeScript('42');
}).then(bail, error => {
is(error.name, 'InvalidAccessError', `scriptId: ${scriptId++}`);
return iframe.executeScript('43', { url: 'http://foo.com' });
}).then(bail, (error) => {
is(error.name, 'URL mismatches', `scriptId: ${scriptId++}`);
return iframe.executeScript('43', { url: '_' });
}, bail).then(bail, (error) => {
is(error.name, 'Malformed URL', `scriptId: ${scriptId++}`);
return iframe.executeScript('43', { origin: 'http://foo.com' });
}, bail).then(bail, (error) => {
is(error.name, 'Origin mismatches', `scriptId: ${scriptId++}`);
return iframe.executeScript('43', { origin: 'https://example.org' });
}, bail).then(bail, (error) => {
is(error.name, 'Origin mismatches', `scriptId: ${scriptId++}`);
SimpleTest.finish();
});
}
}
addEventListener('testready', runTest);

Просмотреть файл

@ -0,0 +1,8 @@
<html>
<head>
<script>
window.btoa = () => "fake btoa";
</script>
</head>
<body>foo</body>
</html>

Просмотреть файл

@ -6,7 +6,9 @@
skip-if = os == "android" || (toolkit == "cocoa" && debug) || buildapp == 'mulet' || (buildapp == 'b2g' && (toolkit != 'gonk' || debug)) || e10s
support-files =
browserElement_OpenMixedProcess.js
file_browserElement_ExecuteScript.html
file_browserElement_OpenMixedProcess.html
browserElement_ExecuteScript.js
browserElement_Find.js
browserElement_OpenTab.js
@ -42,6 +44,7 @@ skip-if = (toolkit == 'gonk' && !debug)
disabled = bug 1022281
[test_browserElement_oop_ErrorSecurity.html]
skip-if = (toolkit == 'gonk' && !debug)
[test_browserElement_oop_ExecuteScript.html]
[test_browserElement_oop_Find.html]
[test_browserElement_oop_FirstPaint.html]
[test_browserElement_oop_ForwardName.html]

Просмотреть файл

@ -28,6 +28,7 @@ support-files =
browserElement_DocumentFirstPaint.js
browserElement_Download.js
browserElement_ErrorSecurity.js
browserElement_ExecuteScript.js
browserElement_ExposableURI.js
browserElement_Find.js
browserElement_FirstPaint.js
@ -88,6 +89,7 @@ support-files =
file_browserElement_CloseFromOpener.html
file_browserElement_CookiesNotThirdParty.html
file_browserElement_DisallowEmbedAppsInOOP.html
file_browserElement_ExecuteScript.html
file_browserElement_ForwardName.html
file_browserElement_FrameWrongURI.html
file_browserElement_LoadEvents.html
@ -163,6 +165,7 @@ skip-if = os == "android" || toolkit == 'gonk' # embed-apps doesn't work in the
[test_browserElement_inproc_DocumentFirstPaint.html]
[test_browserElement_inproc_Download.html]
disabled = bug 1022281
[test_browserElement_inproc_ExecuteScript.html]
[test_browserElement_inproc_ExposableURI.html]
[test_browserElement_inproc_Find.html]
[test_browserElement_inproc_FirstPaint.html]

Просмотреть файл

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1174733
-->
<head>
<title>Test for Bug 1163961</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="browserElementTestHelpers.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=1174733">Mozilla Bug 1174733</a>
<script type="application/javascript;version=1.7" src="browserElement_ExecuteScript.js">
</script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,18 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1174733
-->
<head>
<title>Test for Bug 1163961</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="browserElementTestHelpers.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=1174733">Mozilla Bug 1174733</a>
<script type="application/javascript;version=1.7" src="browserElement_ExecuteScript.js">
</script>
</body>
</html>

Просмотреть файл

@ -26,7 +26,7 @@ interface nsIBrowserElementNextPaintListener : nsISupports
* Interface to the BrowserElementParent implementation. All methods
* but setFrameLoader throw when the remote process is dead.
*/
[scriptable, uuid(daa264b2-54df-4fc7-89b7-c9d02167c5d4)]
[scriptable, uuid(26a832d1-9d71-43ef-9d46-9d7c8ec33f00)]
interface nsIBrowserElementAPI : nsISupports
{
const long FIND_CASE_SENSITIVE = 0;
@ -91,4 +91,6 @@ interface nsIBrowserElementAPI : nsISupports
nsIDOMDOMRequest isAudioChannelActive(in uint32_t audioChannel);
void setNFCFocus(in boolean isFocus);
nsIDOMDOMRequest executeScript(in DOMString script, in jsval options);
};

Просмотреть файл

@ -678,4 +678,59 @@ nsBrowserElement::SetNFCFocus(bool aIsFocus,
}
}
already_AddRefed<DOMRequest>
nsBrowserElement::ExecuteScript(const nsAString& aScript,
const BrowserElementExecuteScriptOptions& aOptions,
ErrorResult& aRv)
{
NS_ENSURE_TRUE(IsBrowserElementOrThrow(aRv), nullptr);
NS_ENSURE_TRUE(IsNotWidgetOrThrow(aRv), nullptr);
nsRefPtr<nsFrameLoader> frameLoader = GetFrameLoader();
if (!frameLoader) {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return nullptr;
}
nsCOMPtr<nsIDOMElement> ownerElement;
nsresult rv = frameLoader->GetOwnerElement(getter_AddRefs(ownerElement));
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return nullptr;
}
nsCOMPtr<nsINode> node = do_QueryInterface(ownerElement);
nsCOMPtr<nsIPrincipal> principal = node->NodePrincipal();
if (!nsContentUtils::IsExactSitePermAllow(principal, "browser:universalxss")) {
aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR);
return nullptr;
}
nsCOMPtr<nsIDOMDOMRequest> req;
nsCOMPtr<nsIXPConnectWrappedJS> wrappedObj = do_QueryInterface(mBrowserElementAPI);
MOZ_ASSERT(wrappedObj, "Failed to get wrapped JS from XPCOM component.");
AutoJSAPI jsapi;
jsapi.Init(wrappedObj->GetJSObject());
JSContext* cx = jsapi.cx();
JS::Rooted<JS::Value> options(cx);
if (!ToJSValue(cx, aOptions, &options)) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return nullptr;
}
rv = mBrowserElementAPI->ExecuteScript(aScript, options, getter_AddRefs(req));
if (NS_FAILED(rv)) {
if (rv == NS_ERROR_INVALID_ARG) {
aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR);
} else {
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
}
return nullptr;
}
return req.forget().downcast<DOMRequest>();
}
} // namespace mozilla

Просмотреть файл

@ -19,6 +19,7 @@ namespace mozilla {
namespace dom {
struct BrowserElementDownloadOptions;
struct BrowserElementExecuteScriptOptions;
class BrowserElementNextPaintEventCallback;
class DOMRequest;
enum class BrowserFindCaseSensitivity: uint32_t;
@ -100,6 +101,10 @@ public:
already_AddRefed<dom::DOMRequest> SetInputMethodActive(bool isActive,
ErrorResult& aRv);
already_AddRefed<dom::DOMRequest> ExecuteScript(const nsAString& aScript,
const dom::BrowserElementExecuteScriptOptions& aOptions,
ErrorResult& aRv);
void SetNFCFocus(bool isFocus,
ErrorResult& aRv);

Просмотреть файл

@ -14,6 +14,11 @@ dictionary BrowserElementDownloadOptions {
DOMString? referrer;
};
dictionary BrowserElementExecuteScriptOptions {
DOMString? url;
DOMString? origin;
};
[NoInterfaceObject]
interface BrowserElement {
};
@ -165,4 +170,11 @@ interface BrowserElementPrivileged {
CheckPermissions="browser"]
void clearMatch();
// Additional |browser:universalxss| permission is required for executeScript API
[Throws,
Pref="dom.mozBrowserFramesEnabled",
CheckPermissions="browser"]
DOMRequest executeScript(DOMString script,
optional BrowserElementExecuteScriptOptions options);
};