mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-03-09 04:25:38 +00:00
Bug 911444 part 4. Expose a print method on PDF viewer windows. r=bholley
The method is exposed only if the consumer has the same principal as the PDF would have if it were not getting the PDF viewer treatment. The method just calls the print() method in the PDF viewer window. It's enough to expose this on nsOuterWindowProxy, not RemoteOuterWindowProxy, because PDF documents end up in the process they would have been in based on their pre-PDF-viewer principal, since we do process determination in the parent process but only run the pdfjs stream converter in the content process, once we have decided which one to use. Differential Revision: https://phabricator.services.mozilla.com/D63711 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
4750163d3b
commit
f6cdd2b50e
@ -108,6 +108,12 @@ bool RemoteOuterWindowProxy::getOwnPropertyDescriptor(
|
||||
return ok;
|
||||
}
|
||||
|
||||
// We don't need the "print" hack that nsOuterWindowProxy has, because pdf
|
||||
// documents are placed in a process based on their principal before the PDF
|
||||
// viewer changes principals around, so are always same-process with things
|
||||
// that are same-origin with their original principal and won't reach this
|
||||
// code in the cases when "print" should be accessible.
|
||||
|
||||
if (JSID_IS_STRING(aId)) {
|
||||
nsAutoJSString str;
|
||||
if (!str.init(aCx, JSID_TO_STRING(aId))) {
|
||||
|
@ -22,7 +22,9 @@
|
||||
#include "nsISecureBrowserUI.h"
|
||||
#include "nsIWebProgressListener.h"
|
||||
#include "mozilla/AntiTrackingCommon.h"
|
||||
#include "mozilla/dom/BindingUtils.h"
|
||||
#include "mozilla/dom/BrowserChild.h"
|
||||
#include "mozilla/dom/BrowsingContextBinding.h"
|
||||
#include "mozilla/dom/ContentFrameMessageManager.h"
|
||||
#include "mozilla/dom/EventTarget.h"
|
||||
#include "mozilla/dom/LocalStorage.h"
|
||||
@ -37,6 +39,7 @@
|
||||
#include "mozilla/dom/Timeout.h"
|
||||
#include "mozilla/dom/TimeoutHandler.h"
|
||||
#include "mozilla/dom/TimeoutManager.h"
|
||||
#include "mozilla/dom/WindowContext.h"
|
||||
#include "mozilla/dom/WindowProxyHolder.h"
|
||||
#include "mozilla/IntegerPrintfMacros.h"
|
||||
#if defined(MOZ_WIDGET_ANDROID)
|
||||
@ -68,6 +71,7 @@
|
||||
// Helper Classes
|
||||
#include "nsJSUtils.h"
|
||||
#include "jsapi.h"
|
||||
#include "jsfriendapi.h"
|
||||
#include "js/PropertySpec.h"
|
||||
#include "js/Wrapper.h"
|
||||
#include "nsCharSeparatedTokenizer.h"
|
||||
@ -514,6 +518,27 @@ class nsOuterWindowProxy : public MaybeCrossOriginObject<js::Wrapper> {
|
||||
using MaybeCrossOriginObjectMixins::EnsureHolder;
|
||||
bool EnsureHolder(JSContext* cx, JS::Handle<JSObject*> proxy,
|
||||
JS::MutableHandle<JSObject*> holder) const override;
|
||||
|
||||
// Helper method for creating a special "print" method that allows printing
|
||||
// our PDF-viewer documents even if you're not same-origin with them.
|
||||
//
|
||||
// aProxy must be our nsOuterWindowProxy. It will not be same-compartment
|
||||
// with aCx, since we only use this on the different-origin codepath!
|
||||
//
|
||||
// Can return true without filling in aDesc, which corresponds to not exposing
|
||||
// a "print" method.
|
||||
static bool MaybeGetPDFJSPrintMethod(
|
||||
JSContext* cx, JS::Handle<JSObject*> proxy,
|
||||
JS::MutableHandle<JS::PropertyDescriptor> desc);
|
||||
|
||||
// The actual "print" method we use for the PDFJS case.
|
||||
static bool PDFJSPrintMethod(JSContext* cx, unsigned argc, JS::Value* vp);
|
||||
|
||||
// Helper method to get the pre-PDF-viewer-messing-with-it principal from an
|
||||
// inner window. Will return null if this is not a PDF-viewer inner or if the
|
||||
// principal could not be found for some reason.
|
||||
static already_AddRefed<nsIPrincipal> GetNoPDFJSPrincipal(
|
||||
nsGlobalWindowInner* inner);
|
||||
};
|
||||
|
||||
const char* nsOuterWindowProxy::className(JSContext* cx,
|
||||
@ -636,6 +661,19 @@ bool nsOuterWindowProxy::getOwnPropertyDescriptor(
|
||||
return true;
|
||||
}
|
||||
|
||||
// Non-spec step for the PDF viewer's window.print(). This comes before we
|
||||
// check for named subframes, because in the same-origin case print() would
|
||||
// shadow those.
|
||||
if (id == GetJSIDByIndex(cx, XPCJSContext::IDX_PRINT)) {
|
||||
if (!MaybeGetPDFJSPrintMethod(cx, proxy, desc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (desc.object()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6 -- check for named subframes.
|
||||
if (JSID_IS_STRING(id)) {
|
||||
nsAutoJSString name;
|
||||
@ -773,6 +811,22 @@ bool nsOuterWindowProxy::ownPropertyKeys(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the "print" property if needed.
|
||||
nsGlobalWindowOuter* outer = GetOuterWindow(proxy);
|
||||
nsGlobalWindowInner* inner =
|
||||
nsGlobalWindowInner::Cast(outer->GetCurrentInnerWindow());
|
||||
if (inner) {
|
||||
nsCOMPtr<nsIPrincipal> targetPrincipal = GetNoPDFJSPrincipal(inner);
|
||||
if (targetPrincipal &&
|
||||
nsContentUtils::SubjectPrincipal(cx)->Equals(targetPrincipal)) {
|
||||
JS::RootedVector<jsid> printProp(cx);
|
||||
if (!printProp.append(GetJSIDByIndex(cx, XPCJSContext::IDX_PRINT)) ||
|
||||
!js::AppendUnique(cx, props, printProp)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return xpc::AppendCrossOriginWhitelistedPropNames(cx, props);
|
||||
}
|
||||
|
||||
@ -1042,6 +1096,175 @@ size_t nsOuterWindowProxy::objectMoved(JSObject* obj, JSObject* old) const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
enum { PDFJS_SLOT_CALLEE = 0 };
|
||||
|
||||
// static
|
||||
bool nsOuterWindowProxy::MaybeGetPDFJSPrintMethod(
|
||||
JSContext* cx, JS::Handle<JSObject*> proxy,
|
||||
JS::MutableHandle<JS::PropertyDescriptor> desc) {
|
||||
MOZ_ASSERT(proxy);
|
||||
|
||||
nsGlobalWindowOuter* outer = GetOuterWindow(proxy);
|
||||
nsGlobalWindowInner* inner =
|
||||
nsGlobalWindowInner::Cast(outer->GetCurrentInnerWindow());
|
||||
if (!inner) {
|
||||
// No print method to expose.
|
||||
return true;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIPrincipal> targetPrincipal = GetNoPDFJSPrincipal(inner);
|
||||
if (!targetPrincipal) {
|
||||
// Nothing special to be done.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!nsContentUtils::SubjectPrincipal(cx)->Equals(targetPrincipal)) {
|
||||
// Not our origin's PDF document.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the function we plan to actually call.
|
||||
JS::Rooted<JSObject*> innerObj(cx, inner->GetGlobalJSObject());
|
||||
if (!innerObj) {
|
||||
// Really should not happen, but ok, let's just return.
|
||||
return true;
|
||||
}
|
||||
|
||||
JS::Rooted<JS::Value> targetFunc(cx);
|
||||
{
|
||||
JSAutoRealm ar(cx, innerObj);
|
||||
if (!JS_GetProperty(cx, innerObj, "print", &targetFunc)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetFunc.isObject()) {
|
||||
// Who knows what's going on. Just return.
|
||||
return true;
|
||||
}
|
||||
|
||||
// The Realm of cx is the realm our caller is in and the realm we
|
||||
// should create our function in. Note that we can't use the
|
||||
// standard XPConnect function forwarder machinery because our
|
||||
// "this" is cross-origin, so we have to do thus by hand.
|
||||
|
||||
// Make sure targetFunc is wrapped into the right compartment.
|
||||
if (!MaybeWrapValue(cx, &targetFunc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JSFunction* fun =
|
||||
js::NewFunctionWithReserved(cx, PDFJSPrintMethod, 0, 0, "print");
|
||||
if (!fun) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JS::Rooted<JSObject*> funObj(cx, JS_GetFunctionObject(fun));
|
||||
js::SetFunctionNativeReserved(funObj, PDFJS_SLOT_CALLEE, targetFunc);
|
||||
|
||||
JS::Rooted<JS::Value> funVal(cx, JS::ObjectValue(*funObj));
|
||||
// JSPROP_ENUMERATE because that's what it would have been in the same-origin
|
||||
// case without the PDF viewer messing with things.
|
||||
desc.setDataDescriptor(funVal, JSPROP_ENUMERATE);
|
||||
desc.object().set(proxy);
|
||||
return true;
|
||||
}
|
||||
|
||||
// static
|
||||
bool nsOuterWindowProxy::PDFJSPrintMethod(JSContext* cx, unsigned argc,
|
||||
JS::Value* vp) {
|
||||
JS::CallArgs args = CallArgsFromVp(argc, vp);
|
||||
|
||||
JS::Rooted<JSObject*> realCallee(
|
||||
cx, &js::GetFunctionNativeReserved(&args.callee(), PDFJS_SLOT_CALLEE)
|
||||
.toObject());
|
||||
// Unchecked unwrap, because we want to extract the thing we really had
|
||||
// before.
|
||||
realCallee = js::UncheckedUnwrap(realCallee);
|
||||
|
||||
JS::Rooted<JS::Value> thisv(cx, args.thisv());
|
||||
if (thisv.isNullOrUndefined()) {
|
||||
// Replace it with the global of our stashed callee, simulating the
|
||||
// global-assuming behavior of DOM methods.
|
||||
JS::Rooted<JSObject*> global(cx, JS::GetNonCCWObjectGlobal(realCallee));
|
||||
if (!MaybeWrapObject(cx, &global)) {
|
||||
return false;
|
||||
}
|
||||
thisv.setObject(*global);
|
||||
} else if (!thisv.isObject()) {
|
||||
return ThrowInvalidThis(cx, args, false, prototypes::id::Window);
|
||||
}
|
||||
|
||||
// We want to do an UncheckedUnwrap here, because we're going to directly
|
||||
// examine the principal of the inner window, if we have an inner window.
|
||||
JS::Rooted<JSObject*> unwrappedObj(cx,
|
||||
js::UncheckedUnwrap(&thisv.toObject()));
|
||||
nsGlobalWindowInner* inner = nullptr;
|
||||
{
|
||||
// Do the unwrap in the Realm of the object we're looking at.
|
||||
JSAutoRealm ar(cx, unwrappedObj);
|
||||
UNWRAP_MAYBE_CROSS_ORIGIN_OBJECT(Window, &unwrappedObj, inner, cx);
|
||||
}
|
||||
if (!inner) {
|
||||
return ThrowInvalidThis(cx, args, false, prototypes::id::Window);
|
||||
}
|
||||
|
||||
nsIPrincipal* callerPrincipal = nsContentUtils::SubjectPrincipal(cx);
|
||||
if (!callerPrincipal->SubsumesConsideringDomain(inner->GetPrincipal())) {
|
||||
// Check whether it's a PDF viewer from our origin.
|
||||
nsCOMPtr<nsIPrincipal> pdfPrincipal = GetNoPDFJSPrincipal(inner);
|
||||
if (!pdfPrincipal || !callerPrincipal->Equals(pdfPrincipal)) {
|
||||
// Security error.
|
||||
return ThrowInvalidThis(cx, args, true, prototypes::id::Window);
|
||||
}
|
||||
}
|
||||
|
||||
// Go ahead and enter the Realm of our real callee to call it. We'll pass it
|
||||
// our "thisv", just in case someone grabs a "print" method off one PDF
|
||||
// document and .call()s it on another one.
|
||||
{
|
||||
JSAutoRealm ar(cx, realCallee);
|
||||
if (!MaybeWrapValue(cx, &thisv)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't bother passing through the args; they will get ignored anyway.
|
||||
|
||||
if (!JS::Call(cx, thisv, realCallee, JS::HandleValueArray::empty(),
|
||||
args.rval())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the return value (not that there should be any!) into the right
|
||||
// compartment.
|
||||
return MaybeWrapValue(cx, args.rval());
|
||||
}
|
||||
|
||||
// static
|
||||
already_AddRefed<nsIPrincipal> nsOuterWindowProxy::GetNoPDFJSPrincipal(
|
||||
nsGlobalWindowInner* inner) {
|
||||
if (!nsContentUtils::IsPDFJS(inner->GetPrincipal())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Document* doc = inner->GetExtantDoc();
|
||||
if (!doc) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIPropertyBag2> propBag(do_QueryInterface(doc->GetChannel()));
|
||||
if (!propBag) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIPrincipal> principal;
|
||||
propBag->GetPropertyAsInterface(NS_LITERAL_STRING("noPDFJSPrincipal"),
|
||||
NS_GET_IID(nsIPrincipal),
|
||||
getter_AddRefs(principal));
|
||||
return principal.forget();
|
||||
}
|
||||
|
||||
const nsOuterWindowProxy nsOuterWindowProxy::singleton;
|
||||
|
||||
class nsChromeOuterWindowProxy : public nsOuterWindowProxy {
|
||||
|
@ -768,6 +768,8 @@ tags = audiochannel
|
||||
[test_open_null_features.html]
|
||||
[test_openDialogChromeOnly.html]
|
||||
tags = openwindow
|
||||
[test_pdf_print.html]
|
||||
skip-if = toolkit == 'android' # We don't ship pdf.js on Android
|
||||
[test_plugin_freezing.html]
|
||||
skip-if = (os == 'win' && processor == 'aarch64')
|
||||
reason = Plugins are not supported on Windows/AArch64
|
||||
|
62
dom/base/test/test_pdf_print.html
Normal file
62
dom/base/test/test_pdf_print.html
Normal file
@ -0,0 +1,62 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><!-- TODO: insert title here --></title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
<script>
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
const blob = new Blob(["x"], { type: "application/pdf" });
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
const blobFrame = document.createElement("iframe");
|
||||
blobFrame.src = blobURL;
|
||||
document.getElementById("content").appendChild(blobFrame);
|
||||
|
||||
const dataURL = "data:application/pdf,";
|
||||
const dataFrame = document.createElement("iframe");
|
||||
dataFrame.src = dataURL;
|
||||
document.getElementById("content").appendChild(dataFrame);
|
||||
|
||||
addLoadEvent(function() {
|
||||
// blob:// URLs inherit their origin, so the window inside blobFrame
|
||||
// should be same-orgin with us except for the PDF viewer messing with
|
||||
// origins.
|
||||
const printFunc = blobFrame.contentWindow.print;
|
||||
is(typeof printFunc, "function", "Should have a 'print' function");
|
||||
ok(Object.getOwnPropertyNames(blobFrame.contentWindow).includes("print"),
|
||||
"Should see 'print' property in property names");
|
||||
|
||||
try {
|
||||
// data: URLs get nonce origins, so the window inside dataFrame is not
|
||||
// same-origin with us in any way.
|
||||
dataFrame.contentWindow.print;
|
||||
ok(false, "Should throw on cross-origin .print access");
|
||||
} catch (e) {
|
||||
ok(/Permission denied/.test(e.message), "Should have a security error");
|
||||
}
|
||||
ok(!Object.getOwnPropertyNames(dataFrame.contentWindow).includes("print"),
|
||||
"Should not see 'print' property in property names");
|
||||
|
||||
try {
|
||||
printFunc.call(dataFrame.contentWindow);
|
||||
ok(false, "Should throw on cross-origin call");
|
||||
} catch (e) {
|
||||
ok(/Permission to call/.test(e.message),
|
||||
"Should have a security error for call");
|
||||
}
|
||||
|
||||
// It'd be nice to test that calling the function works right, but if it
|
||||
// does it'll put up the print dialog, which is not helpful in an
|
||||
// automated test.
|
||||
SimpleTest.finish();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<pre id="test"></pre>
|
||||
</body>
|
||||
</html>
|
@ -161,8 +161,8 @@ static bool ThrowInvalidThis(JSContext* aCx, const JS::CallArgs& aArgs,
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ThrowInvalidThis(JSContext* aCx, const JS::CallArgs& aArgs,
|
||||
bool aSecurityError, prototypes::ID aProtoId) {
|
||||
bool ThrowInvalidThis(JSContext* aCx, const JS::CallArgs& aArgs,
|
||||
bool aSecurityError, prototypes::ID aProtoId) {
|
||||
return ThrowInvalidThis(aCx, aArgs, aSecurityError,
|
||||
NamesOfInterfacesWithProtos(aProtoId));
|
||||
}
|
||||
|
@ -1783,6 +1783,10 @@ bool ThrowingConstructor(JSContext* cx, unsigned argc, JS::Value* vp);
|
||||
|
||||
bool ThrowConstructorWithoutNew(JSContext* cx, const char* name);
|
||||
|
||||
// Helper for throwing an "invalid this" exception.
|
||||
bool ThrowInvalidThis(JSContext* aCx, const JS::CallArgs& aArgs,
|
||||
bool aSecurityError, prototypes::ID aProtoId);
|
||||
|
||||
bool GetPropertyOnPrototype(JSContext* cx, JS::Handle<JSObject*> proxy,
|
||||
JS::Handle<JS::Value> receiver, JS::Handle<jsid> id,
|
||||
bool* found, JS::MutableHandle<JS::Value> vp);
|
||||
|
@ -126,6 +126,7 @@ const char* const XPCJSRuntime::mStrings[] = {
|
||||
"classId", // IDX_CLASS_ID
|
||||
"interfaceId", // IDX_INTERFACE_ID
|
||||
"initializer", // IDX_INITIALIZER
|
||||
"print", // IDX_PRINT
|
||||
};
|
||||
|
||||
/***************************************************************************/
|
||||
|
@ -402,6 +402,7 @@ class XPCJSContext final : public mozilla::CycleCollectedJSContext,
|
||||
IDX_CLASS_ID,
|
||||
IDX_INTERFACE_ID,
|
||||
IDX_INITIALIZER,
|
||||
IDX_PRINT,
|
||||
IDX_TOTAL_COUNT // just a count of the above
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user