michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et cindent: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "mozilla/dom/CallbackObject.h" michael@0: #include "mozilla/dom/BindingUtils.h" michael@0: #include "mozilla/dom/DOMError.h" michael@0: #include "mozilla/dom/DOMErrorBinding.h" michael@0: #include "jsfriendapi.h" michael@0: #include "nsIScriptGlobalObject.h" michael@0: #include "nsIXPConnect.h" michael@0: #include "nsIScriptContext.h" michael@0: #include "nsPIDOMWindow.h" michael@0: #include "nsJSUtils.h" michael@0: #include "nsCxPusher.h" michael@0: #include "nsIScriptSecurityManager.h" michael@0: #include "xpcprivate.h" michael@0: #include "WorkerPrivate.h" michael@0: #include "nsGlobalWindow.h" michael@0: #include "WorkerScope.h" michael@0: michael@0: namespace mozilla { michael@0: namespace dom { michael@0: michael@0: NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CallbackObject) michael@0: NS_INTERFACE_MAP_ENTRY(mozilla::dom::CallbackObject) michael@0: NS_INTERFACE_MAP_ENTRY(nsISupports) michael@0: NS_INTERFACE_MAP_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTING_ADDREF(CallbackObject) michael@0: NS_IMPL_CYCLE_COLLECTING_RELEASE(CallbackObject) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_CLASS(CallbackObject) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(CallbackObject) michael@0: tmp->DropJSObjects(); michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncumbentGlobal) michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_END michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CallbackObject) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncumbentGlobal) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(CallbackObject) michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCallback) michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mIncumbentJSGlobal) michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_END michael@0: michael@0: CallbackObject::CallSetup::CallSetup(CallbackObject* aCallback, michael@0: ErrorResult& aRv, michael@0: ExceptionHandling aExceptionHandling, michael@0: JSCompartment* aCompartment, michael@0: bool aIsJSImplementedWebIDL) michael@0: : mCx(nullptr) michael@0: , mCompartment(aCompartment) michael@0: , mErrorResult(aRv) michael@0: , mExceptionHandling(aExceptionHandling) michael@0: , mIsMainThread(NS_IsMainThread()) michael@0: { michael@0: if (mIsMainThread) { michael@0: nsContentUtils::EnterMicroTask(); michael@0: } michael@0: michael@0: // Compute the caller's subject principal (if necessary) early, before we michael@0: // do anything that might perturb the relevant state. michael@0: nsIPrincipal* webIDLCallerPrincipal = nullptr; michael@0: if (aIsJSImplementedWebIDL) { michael@0: webIDLCallerPrincipal = nsContentUtils::GetSubjectPrincipal(); michael@0: } michael@0: michael@0: // We need to produce a useful JSContext here. Ideally one that the callback michael@0: // is in some sense associated with, so that we can sort of treat it as a michael@0: // "script entry point". Though once we actually have script entry points, michael@0: // we'll need to do the script entry point bits once we have an actual michael@0: // callable. michael@0: michael@0: // First, find the real underlying callback. michael@0: JSObject* realCallback = js::UncheckedUnwrap(aCallback->CallbackPreserveColor()); michael@0: JSContext* cx = nullptr; michael@0: nsIGlobalObject* globalObject = nullptr; michael@0: michael@0: { michael@0: JS::AutoAssertNoGC nogc; michael@0: if (mIsMainThread) { michael@0: // Now get the global and JSContext for this callback. michael@0: nsGlobalWindow* win = xpc::WindowGlobalOrNull(realCallback); michael@0: if (win) { michael@0: // Make sure that if this is a window it's the current inner, since the michael@0: // nsIScriptContext and hence JSContext are associated with the outer michael@0: // window. Which means that if someone holds on to a function from a michael@0: // now-unloaded document we'd have the new document as the script entry michael@0: // point... michael@0: MOZ_ASSERT(win->IsInnerWindow()); michael@0: nsPIDOMWindow* outer = win->GetOuterWindow(); michael@0: if (!outer || win != outer->GetCurrentInnerWindow()) { michael@0: // Just bail out from here michael@0: return; michael@0: } michael@0: cx = win->GetContext() ? win->GetContext()->GetNativeContext() michael@0: // This happens - Removing it causes michael@0: // test_bug293235.xul to go orange. michael@0: : nsContentUtils::GetSafeJSContext(); michael@0: globalObject = win; michael@0: } else { michael@0: // No DOM Window. Store the global and use the SafeJSContext. michael@0: JSObject* glob = js::GetGlobalForObjectCrossCompartment(realCallback); michael@0: globalObject = xpc::GetNativeForGlobal(glob); michael@0: MOZ_ASSERT(globalObject); michael@0: cx = nsContentUtils::GetSafeJSContext(); michael@0: } michael@0: } else { michael@0: cx = workers::GetCurrentThreadJSContext(); michael@0: globalObject = workers::GetCurrentThreadWorkerPrivate()->GlobalScope(); michael@0: } michael@0: michael@0: // Bail out if there's no useful global. This seems to happen intermittently michael@0: // on gaia-ui tests, probably because nsInProcessTabChildGlobal is returning michael@0: // null in some kind of teardown state. michael@0: if (!globalObject->GetGlobalJSObject()) { michael@0: return; michael@0: } michael@0: michael@0: mAutoEntryScript.construct(globalObject, mIsMainThread, cx); michael@0: mAutoEntryScript.ref().SetWebIDLCallerPrincipal(webIDLCallerPrincipal); michael@0: nsIGlobalObject* incumbent = aCallback->IncumbentGlobalOrNull(); michael@0: if (incumbent) { michael@0: // The callback object traces its incumbent JS global, so in general it michael@0: // should be alive here. However, it's possible that we could run afoul michael@0: // of the same IPC global weirdness described above, wherein the michael@0: // nsIGlobalObject has severed its reference to the JS global. Let's just michael@0: // be safe here, so that nobody has to waste a day debugging gaia-ui tests. michael@0: if (!incumbent->GetGlobalJSObject()) { michael@0: return; michael@0: } michael@0: mAutoIncumbentScript.construct(incumbent); michael@0: } michael@0: michael@0: // Unmark the callable (by invoking Callback() and not the CallbackPreserveColor() michael@0: // variant), and stick it in a Rooted before it can go gray again. michael@0: // Nothing before us in this function can trigger a CC, so it's safe to wait michael@0: // until here it do the unmark. This allows us to order the following two michael@0: // operations _after_ the Push() above, which lets us take advantage of the michael@0: // JSAutoRequest embedded in the pusher. michael@0: // michael@0: // We can do this even though we're not in the right compartment yet, because michael@0: // Rooted<> does not care about compartments. michael@0: mRootedCallable.construct(cx, aCallback->Callback()); michael@0: } michael@0: michael@0: if (mIsMainThread) { michael@0: // Check that it's ok to run this callback at all. michael@0: // Make sure to use realCallback to get the global of the callback object, michael@0: // not the wrapper. michael@0: bool allowed = nsContentUtils::GetSecurityManager()-> michael@0: ScriptAllowed(js::GetGlobalForObjectCrossCompartment(realCallback)); michael@0: michael@0: if (!allowed) { michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Enter the compartment of our callback, so we can actually work with it. michael@0: // michael@0: // Note that if the callback is a wrapper, this will not be the same michael@0: // compartment that we ended up in with mAutoEntryScript above, because the michael@0: // entry point is based off of the unwrapped callback (realCallback). michael@0: mAc.construct(cx, mRootedCallable.ref()); michael@0: michael@0: // And now we're ready to go. michael@0: mCx = cx; michael@0: michael@0: // Make sure the JS engine doesn't report exceptions we want to re-throw michael@0: if ((mCompartment && mExceptionHandling == eRethrowContentExceptions) || michael@0: mExceptionHandling == eRethrowExceptions) { michael@0: mSavedJSContextOptions = JS::ContextOptionsRef(cx); michael@0: JS::ContextOptionsRef(cx).setDontReportUncaught(true); michael@0: } michael@0: } michael@0: michael@0: bool michael@0: CallbackObject::CallSetup::ShouldRethrowException(JS::Handle aException) michael@0: { michael@0: if (mExceptionHandling == eRethrowExceptions) { michael@0: return true; michael@0: } michael@0: michael@0: MOZ_ASSERT(mExceptionHandling == eRethrowContentExceptions); michael@0: michael@0: // For eRethrowContentExceptions we only want to throw an exception if the michael@0: // object that was thrown is a DOMError object in the caller compartment michael@0: // (which we stored in mCompartment). michael@0: michael@0: if (!aException.isObject()) { michael@0: return false; michael@0: } michael@0: michael@0: JS::Rooted obj(mCx, &aException.toObject()); michael@0: obj = js::UncheckedUnwrap(obj, /* stopAtOuter = */ false); michael@0: if (js::GetObjectCompartment(obj) != mCompartment) { michael@0: return false; michael@0: } michael@0: michael@0: DOMError* domError; michael@0: return NS_SUCCEEDED(UNWRAP_OBJECT(DOMError, obj, domError)); michael@0: } michael@0: michael@0: CallbackObject::CallSetup::~CallSetup() michael@0: { michael@0: // To get our nesting right we have to destroy our JSAutoCompartment first. michael@0: // In particular, we want to do this before we try reporting any exceptions, michael@0: // so we end up reporting them while in the compartment of our entry point, michael@0: // not whatever cross-compartment wrappper mCallback might be. michael@0: // Be careful: the JSAutoCompartment might not have been constructed at all! michael@0: mAc.destroyIfConstructed(); michael@0: michael@0: // Now, if we have a JSContext, report any pending errors on it, unless we michael@0: // were told to re-throw them. michael@0: if (mCx) { michael@0: bool dealtWithPendingException = false; michael@0: if ((mCompartment && mExceptionHandling == eRethrowContentExceptions) || michael@0: mExceptionHandling == eRethrowExceptions) { michael@0: // Restore the old context options michael@0: JS::ContextOptionsRef(mCx) = mSavedJSContextOptions; michael@0: mErrorResult.MightThrowJSException(); michael@0: if (JS_IsExceptionPending(mCx)) { michael@0: JS::Rooted exn(mCx); michael@0: if (JS_GetPendingException(mCx, &exn) && michael@0: ShouldRethrowException(exn)) { michael@0: mErrorResult.ThrowJSException(mCx, exn); michael@0: JS_ClearPendingException(mCx); michael@0: dealtWithPendingException = true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!dealtWithPendingException) { michael@0: // Either we're supposed to report our exceptions, or we're supposed to michael@0: // re-throw them but we failed to JS_GetPendingException. Either way, michael@0: // just report the pending exception, if any. michael@0: // michael@0: // We don't use nsJSUtils::ReportPendingException here because all it michael@0: // does at this point is JS_SaveFrameChain and enter a compartment around michael@0: // a JS_ReportPendingException call. But our mAutoEntryScript should michael@0: // already do a JS_SaveFrameChain and we are already in the compartment michael@0: // we want to be in, so all nsJSUtils::ReportPendingException would do is michael@0: // screw up our compartment, which is exactly what we do not want. michael@0: // michael@0: // XXXbz FIXME: bug 979525 means we don't always JS_SaveFrameChain here, michael@0: // so we need to go ahead and do that. michael@0: JS::Rooted oldGlobal(mCx, JS::CurrentGlobalOrNull(mCx)); michael@0: MOZ_ASSERT(oldGlobal, "How can we not have a global here??"); michael@0: bool saved = JS_SaveFrameChain(mCx); michael@0: // Make sure the JSAutoCompartment goes out of scope before the michael@0: // JS_RestoreFrameChain call! michael@0: { michael@0: JSAutoCompartment ac(mCx, oldGlobal); michael@0: MOZ_ASSERT(!JS::DescribeScriptedCaller(mCx), michael@0: "Our comment above about JS_SaveFrameChain having been " michael@0: "called is a lie?"); michael@0: JS_ReportPendingException(mCx); michael@0: } michael@0: if (saved) { michael@0: JS_RestoreFrameChain(mCx); michael@0: } michael@0: } michael@0: } michael@0: michael@0: mAutoIncumbentScript.destroyIfConstructed(); michael@0: mAutoEntryScript.destroyIfConstructed(); michael@0: michael@0: // It is important that this is the last thing we do, after leaving the michael@0: // compartment and undoing all our entry/incumbent script changes michael@0: if (mIsMainThread) { michael@0: nsContentUtils::LeaveMicroTask(); michael@0: } michael@0: } michael@0: michael@0: already_AddRefed michael@0: CallbackObjectHolderBase::ToXPCOMCallback(CallbackObject* aCallback, michael@0: const nsIID& aIID) const michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: if (!aCallback) { michael@0: return nullptr; michael@0: } michael@0: michael@0: AutoSafeJSContext cx; michael@0: michael@0: JS::Rooted callback(cx, aCallback->Callback()); michael@0: michael@0: JSAutoCompartment ac(cx, callback); michael@0: nsRefPtr wrappedJS; michael@0: nsresult rv = michael@0: nsXPCWrappedJS::GetNewOrUsed(callback, aIID, getter_AddRefs(wrappedJS)); michael@0: if (NS_FAILED(rv) || !wrappedJS) { michael@0: return nullptr; michael@0: } michael@0: michael@0: nsCOMPtr retval; michael@0: rv = wrappedJS->QueryInterface(aIID, getter_AddRefs(retval)); michael@0: if (NS_FAILED(rv)) { michael@0: return nullptr; michael@0: } michael@0: michael@0: return retval.forget(); michael@0: } michael@0: michael@0: } // namespace dom michael@0: } // namespace mozilla