/* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/. */ #include "ExtensionEventListener.h" #include "ExtensionAPIRequestForwarder.h" #include "ExtensionPort.h" #include "mozilla/dom/ClonedErrorHolder.h" #include "mozilla/dom/FunctionBinding.h" #include "mozilla/dom/Promise.h" #include "nsThreadManager.h" // NS_IsMainThread namespace mozilla { namespace extensions { namespace { class SendResponseCallback final : public nsISupports { public: NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(SendResponseCallback) NS_DECL_CYCLE_COLLECTING_ISUPPORTS static RefPtr Create( nsIGlobalObject* aGlobalObject, const RefPtr& aPromise, JS::Handle aValue, ErrorResult& aRv) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); RefPtr responseCallback = new SendResponseCallback(aPromise, aValue); auto cleanupCb = [responseCallback]() { responseCallback->Cleanup(); }; // Create a StrongWorkerRef to the worker thread, the cleanup callback // associated to the StongerWorkerRef will release the reference and resolve // the promise returned to the ExtensionEventListener caller with undefined // if the worker global is being destroyed. auto* workerPrivate = dom::GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); RefPtr workerRef = dom::StrongWorkerRef::Create( workerPrivate, "SendResponseCallback", cleanupCb); if (NS_WARN_IF(!workerRef)) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } responseCallback->mWorkerRef = workerRef; return responseCallback; } SendResponseCallback(const RefPtr& aPromise, JS::Handle aValue) : mPromise(aPromise), mValue(aValue) { MOZ_ASSERT(mPromise); mozilla::HoldJSObjects(this); // Create a promise monitor that invalidates the sendResponse // callback if the promise has been already resolved or rejected. mPromiseListener = new dom::DomPromiseListener( [self = RefPtr{this}](JSContext* aCx, JS::Handle aValue) { self->Cleanup(); }, [self = RefPtr{this}](nsresult aError) { self->Cleanup(); }); mPromise->AppendNativeHandler(mPromiseListener); } void Cleanup(bool aIsDestroying = false) { // Return earlier if the instance was already been cleaned up. if (!mPromiseListener) { return; } NS_WARNING("SendResponseCallback::Cleanup"); // Clear the promise listener's resolvers to release the // RefPtr captured by the ones initially set. mPromiseListener->Clear(); mPromiseListener = nullptr; if (mPromise) { mPromise->MaybeResolveWithUndefined(); } mPromise = nullptr; // Skipped if called from the destructor. if (!aIsDestroying && mValue.isObject()) { // Release the reference to the SendResponseCallback. js::SetFunctionNativeReserved(&mValue.toObject(), SLOT_SEND_RESPONSE_CALLBACK_INSTANCE, JS::PrivateValue(nullptr)); } if (mWorkerRef) { mWorkerRef = nullptr; } } static bool Call(JSContext* aCx, unsigned aArgc, JS::Value* aVp) { JS::CallArgs args = CallArgsFromVp(aArgc, aVp); JS::Rooted callee(aCx, &args.callee()); JS::Value v = js::GetFunctionNativeReserved( callee, SLOT_SEND_RESPONSE_CALLBACK_INSTANCE); SendResponseCallback* sendResponse = reinterpret_cast(v.toPrivate()); if (!sendResponse || !sendResponse->mPromise || !sendResponse->mPromise->PromiseObj()) { NS_WARNING("SendResponseCallback called after being invalidated"); return true; } sendResponse->mPromise->MaybeResolve(args.get(0)); sendResponse->Cleanup(); return true; } private: ~SendResponseCallback() { mozilla::DropJSObjects(this); this->Cleanup(true); }; RefPtr mPromise; RefPtr mPromiseListener; JS::Heap mValue; RefPtr mWorkerRef; }; NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SendResponseCallback) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTION_CLASS(SendResponseCallback) NS_IMPL_CYCLE_COLLECTING_ADDREF(SendResponseCallback) NS_IMPL_CYCLE_COLLECTING_RELEASE(SendResponseCallback) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SendResponseCallback) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(SendResponseCallback) NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mValue) NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SendResponseCallback) NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromiseListener); tmp->mValue.setUndefined(); // Resolve the promise with undefined (as "unhandled") before unlinking it. if (tmp->mPromise) { tmp->mPromise->MaybeResolveWithUndefined(); } NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise); NS_IMPL_CYCLE_COLLECTION_UNLINK_END } // anonymous namespace // ExtensionEventListener NS_IMPL_ISUPPORTS(ExtensionEventListener, mozIExtensionEventListener) // static already_AddRefed ExtensionEventListener::Create( nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser, dom::Function* aCallback, CleanupCallback&& aCleanupCallback, ErrorResult& aRv) { MOZ_ASSERT(dom::IsCurrentThreadRunningWorker()); RefPtr extCb = new ExtensionEventListener(aGlobal, aExtensionBrowser, aCallback); auto* workerPrivate = dom::GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); RefPtr workerRef = dom::StrongWorkerRef::Create( workerPrivate, "ExtensionEventListener", std::move(aCleanupCallback)); if (!workerRef) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } extCb->mWorkerRef = new dom::ThreadSafeWorkerRef(workerRef); return extCb.forget(); } // static UniquePtr ExtensionEventListener::SerializeCallArguments(const nsTArray& aArgs, JSContext* aCx, ErrorResult& aRv) { JS::Rooted jsval(aCx); if (NS_WARN_IF(!dom::ToJSValue(aCx, aArgs, &jsval))) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } UniquePtr argsHolder = MakeUnique( dom::StructuredCloneHolder::CloningSupported, dom::StructuredCloneHolder::TransferringNotSupported, JS::StructuredCloneScope::SameProcess); argsHolder->Write(aCx, jsval, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } return argsHolder; } NS_IMETHODIMP ExtensionEventListener::CallListener( const nsTArray& aArgs, ListenerCallOptions* aCallOptions, JSContext* aCx, dom::Promise** aPromiseResult) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_ARG_POINTER(aPromiseResult); // Process and validate call options. APIObjectType apiObjectType = APIObjectType::NONE; JS::Rooted apiObjectDescriptor(aCx); if (aCallOptions) { aCallOptions->GetApiObjectType(&apiObjectType); aCallOptions->GetApiObjectDescriptor(&apiObjectDescriptor); // Explicitly check that the APIObjectType is one of expected ones, // raise to the caller an explicit error if it is not. // // This is using a switch to also get a warning if a new value is added to // the APIObjectType enum and it is not yet handled. switch (apiObjectType) { case APIObjectType::NONE: if (NS_WARN_IF(!apiObjectDescriptor.isNullOrUndefined())) { JS_ReportErrorASCII( aCx, "Unexpected non-null apiObjectDescriptor on apiObjectType=NONE"); return NS_ERROR_UNEXPECTED; } break; case APIObjectType::RUNTIME_PORT: if (NS_WARN_IF(apiObjectDescriptor.isNullOrUndefined())) { JS_ReportErrorASCII(aCx, "Unexpected null apiObjectDescriptor on " "apiObjectType=RUNTIME_PORT"); return NS_ERROR_UNEXPECTED; } break; default: MOZ_CRASH("Unexpected APIObjectType"); return NS_ERROR_UNEXPECTED; } } // Create promise to be returned. IgnoredErrorResult rv; RefPtr retPromise; nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); if (NS_WARN_IF(!global)) { return NS_ERROR_FAILURE; } retPromise = dom::Promise::Create(global, rv); if (NS_WARN_IF(rv.Failed())) { return rv.StealNSResult(); } // Convert args into a non-const sequence. dom::Sequence args; if (!args.AppendElements(aArgs, fallible)) { return NS_ERROR_OUT_OF_MEMORY; } // Execute the listener call. MutexAutoLock lock(mMutex); if (NS_WARN_IF(!mWorkerRef)) { return NS_ERROR_ABORT; } if (apiObjectType != APIObjectType::NONE) { bool prependArgument = false; aCallOptions->GetApiObjectPrepended(&prependArgument); // Prepend or append the apiObjectDescriptor data to the call arguments, // the worker runnable will convert that into an API object // instance on the worker thread. if (prependArgument) { if (!args.InsertElementAt(0, std::move(apiObjectDescriptor), fallible)) { return NS_ERROR_OUT_OF_MEMORY; } } else { if (!args.AppendElement(std::move(apiObjectDescriptor), fallible)) { return NS_ERROR_OUT_OF_MEMORY; } } } UniquePtr argsHolder = SerializeCallArguments(args, aCx, rv); if (NS_WARN_IF(rv.Failed())) { return rv.StealNSResult(); } RefPtr runnable = new ExtensionListenerCallWorkerRunnable(this, std::move(argsHolder), aCallOptions, retPromise); runnable->Dispatch(); retPromise.forget(aPromiseResult); return NS_OK; } dom::WorkerPrivate* ExtensionEventListener::GetWorkerPrivate() const { MOZ_ASSERT(mWorkerRef); return mWorkerRef->Private(); } // ExtensionListenerCallWorkerRunnable void ExtensionListenerCallWorkerRunnable::DeserializeCallArguments( JSContext* aCx, dom::Sequence& aArgs, ErrorResult& aRv) { JS::Rooted jsvalue(aCx); mArgsHolder->Read(xpc::CurrentNativeGlobal(aCx), aCx, &jsvalue, aRv); if (NS_WARN_IF(aRv.Failed())) { return; } nsresult rv2 = ExtensionAPIRequestForwarder::JSArrayToSequence(aCx, jsvalue, aArgs); if (NS_FAILED(rv2)) { aRv.Throw(rv2); } } bool ExtensionListenerCallWorkerRunnable::WorkerRun( JSContext* aCx, dom::WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate); auto global = mListener->GetGlobalObject(); if (NS_WARN_IF(!global)) { return true; } RefPtr extensionBrowser = mListener->GetExtensionBrowser(); if (NS_WARN_IF(!extensionBrowser)) { return true; } auto fn = mListener->GetCallback(); if (NS_WARN_IF(!fn)) { return true; } IgnoredErrorResult rv; dom::Sequence argsSequence; dom::SequenceRooter arguments(aCx, &argsSequence); DeserializeCallArguments(aCx, argsSequence, rv); if (NS_WARN_IF(rv.Failed())) { return true; } RefPtr retPromise; RefPtr workerRef; retPromise = dom::Promise::Create(global, rv); if (retPromise) { workerRef = dom::StrongWorkerRef::Create( aWorkerPrivate, "ExtensionListenerCallWorkerRunnable", []() {}); } if (NS_WARN_IF(rv.Failed() || !workerRef)) { auto rejectMainThreadPromise = [error = rv.Failed() ? rv.StealNSResult() : NS_ERROR_UNEXPECTED, promiseResult = std::move(mPromiseResult)]() { // TODO(rpl): this seems to be currently rejecting an error object // without a stack trace, its a corner case but we may look into // improve this error. promiseResult->MaybeReject(error); }; nsCOMPtr runnable = NS_NewRunnableFunction(__func__, std::move(rejectMainThreadPromise)); NS_DispatchToMainThread(runnable); JS_ClearPendingException(aCx); return true; } ExtensionListenerCallPromiseResultHandler::Create( retPromise, this, new dom::ThreadSafeWorkerRef(workerRef)); // Translate the first parameter into the API object type (e.g. an // ExtensionPort), the content of the original argument value is expected to // be a dictionary that is valid as an internal descriptor for that API object // type. if (mAPIObjectType != APIObjectType::NONE) { IgnoredErrorResult rv; // The api object descriptor is expected to have been prepended to the // other arguments, assert here that the argsSequence does contain at least // one element. MOZ_ASSERT(!argsSequence.IsEmpty()); uint32_t apiObjectIdx = mAPIObjectPrepended ? 0 : argsSequence.Length() - 1; JS::Rooted apiObjectDescriptor( aCx, argsSequence.ElementAt(apiObjectIdx)); JS::Rooted apiObjectValue(aCx); // We only expect the object type to be RUNTIME_PORT at the moment, // until we will need to expect it to support other object types that // some specific API may need. MOZ_ASSERT(mAPIObjectType == APIObjectType::RUNTIME_PORT); RefPtr port = extensionBrowser->GetPort(apiObjectDescriptor, rv); if (NS_WARN_IF(rv.Failed())) { retPromise->MaybeReject(rv.StealNSResult()); return true; } if (NS_WARN_IF(!dom::ToJSValue(aCx, port, &apiObjectValue))) { retPromise->MaybeReject(NS_ERROR_UNEXPECTED); return true; } argsSequence.ReplaceElementAt(apiObjectIdx, apiObjectValue); } // Create callback argument and append it to the call arguments. JS::Rooted sendResponseObj(aCx); switch (mCallbackArgType) { case CallbackType::CALLBACK_NONE: break; case CallbackType::CALLBACK_SEND_RESPONSE: { JS::Rooted sendResponseFn( aCx, js::NewFunctionWithReserved(aCx, SendResponseCallback::Call, /* nargs */ 1, 0, "sendResponse")); sendResponseObj = JS_GetFunctionObject(sendResponseFn); JS::Rooted sendResponseValue( aCx, JS::ObjectValue(*sendResponseObj)); // Create a SendResponseCallback instance that keeps a reference // to the promise to resolve when the static SendReponseCallback::Call // is being called. // the SendReponseCallback instance from the resolved slot to resolve // the promise and invalidated the sendResponse callback (any new call // becomes a noop). RefPtr sendResponsePtr = SendResponseCallback::Create(global, retPromise, sendResponseValue, rv); if (NS_WARN_IF(rv.Failed())) { retPromise->MaybeReject(NS_ERROR_UNEXPECTED); return true; } // Store the SendResponseCallback instance in a private value set on the // function object reserved slot, where ehe SendResponseCallback::Call // static function will get it back to resolve the related promise // and then invalidate the sendResponse callback (any new call // becomes a noop). js::SetFunctionNativeReserved(sendResponseObj, SLOT_SEND_RESPONSE_CALLBACK_INSTANCE, JS::PrivateValue(sendResponsePtr)); if (NS_WARN_IF( !argsSequence.AppendElement(sendResponseValue, fallible))) { retPromise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); return true; } break; } default: MOZ_ASSERT_UNREACHABLE("Unexpected callbackType"); break; } // TODO: should `nsAutoMicroTask mt;` be used here? dom::AutoEntryScript aes(global, "WebExtensionAPIEvent"); JS::Rooted retval(aCx); ErrorResult erv; erv.MightThrowJSException(); MOZ_KnownLive(fn)->Call(argsSequence, &retval, erv, "WebExtensionAPIEvent", dom::Function::eRethrowExceptions); // Calling the callback may have thrown an exception. // TODO: add a ListenerCallOptions to optionally report the exception // instead of forwarding it to the caller. erv.WouldReportJSException(); if (erv.Failed()) { if (erv.IsUncatchableException()) { // TODO: include some more info? (e.g. api path). retPromise->MaybeRejectWithTimeoutError( "WebExtensions API Event listener threw uncatchable exception"); return true; } retPromise->MaybeReject(std::move(erv)); return true; } // Custom return value handling logic for events that do pass a // sendResponse callback parameter (see expected behavior // for the runtime.onMessage sendResponse parameter on MDN: // https://mzl.la/3dokpMi): // // - listener returns Boolean true => the extension listener is // expected to call sendResponse callback parameter asynchronosuly // - listener return a Promise object => the promise is the listener // response // - listener return any other value => the listener didn't handle the // event and the return value is ignored // if (mCallbackArgType == CallbackType::CALLBACK_SEND_RESPONSE) { if (retval.isBoolean() && retval.isTrue()) { // The listener returned `true` and so the promise relate to the // listener call will be resolved once the extension will call // the sendResponce function passed as a callback argument. return true; } // If the retval isn't true and it is not a Promise object, // the listener isn't handling the event, and we resolve the // promise with undefined (if the listener didn't reply already // by calling sendResponse synchronsouly). // undefined ( if (!ExtensionEventListener::IsPromise(aCx, retval)) { // Mark this listener call as cancelled, // ExtensionListenerCallPromiseResult will check to know that it should // release the main thread promise without resolving it. // // TODO: double-check if we should also cancel rejecting the promise // returned by mozIExtensionEventListener.callListener when the listener // call throws (by comparing it with the behavior on the current // privileged-based API implementation). mIsCallResultCancelled = true; retPromise->MaybeResolveWithUndefined(); // Invalidate the sendResponse function by setting the private // value where the SendResponseCallback instance was stored // to a nullptr. js::SetFunctionNativeReserved(sendResponseObj, SLOT_SEND_RESPONSE_CALLBACK_INSTANCE, JS::PrivateValue(nullptr)); return true; } } retPromise->MaybeResolve(retval); return true; } // ExtensionListenerCallPromiseResultHandler NS_IMPL_ISUPPORTS0(ExtensionListenerCallPromiseResultHandler) // static void ExtensionListenerCallPromiseResultHandler::Create( const RefPtr& aPromise, const RefPtr& aWorkerRunnable, dom::ThreadSafeWorkerRef* aWorkerRef) { MOZ_ASSERT(aPromise); MOZ_ASSERT(aWorkerRef); MOZ_ASSERT(aWorkerRef->Private()->IsOnCurrentThread()); RefPtr handler = new ExtensionListenerCallPromiseResultHandler(aWorkerRef, aWorkerRunnable); aPromise->AppendNativeHandler(handler); } void ExtensionListenerCallPromiseResultHandler::WorkerRunCallback( JSContext* aCx, JS::Handle aValue, PromiseCallbackType aCallbackType) { MOZ_ASSERT(mWorkerRef); mWorkerRef->Private()->AssertIsOnWorkerThread(); // The listener call was cancelled (e.g. when a runtime.onMessage listener // returned false), release resources associated with this promise handler // on the main thread without resolving the promise associated to the // extension event listener call. if (mWorkerRunnable->IsCallResultCancelled()) { auto releaseMainThreadPromise = [runnable = std::move(mWorkerRunnable), workerRef = std::move(mWorkerRef)]() {}; nsCOMPtr runnable = NS_NewRunnableFunction(__func__, std::move(releaseMainThreadPromise)); NS_DispatchToMainThread(runnable); return; } JS::Rooted retval(aCx, aValue); if (retval.isObject()) { // Try to serialize the result as an ClonedErrorHolder, // in case the value is an Error object. IgnoredErrorResult rv; JS::Rooted errObj(aCx, &retval.toObject()); RefPtr ceh = dom::ClonedErrorHolder::Create(aCx, errObj, rv); if (!rv.Failed() && ceh) { JS::Rooted obj(aCx); // Note: `ToJSValue` cannot be used because ClonedErrorHolder isn't // wrapped cached. Unused << NS_WARN_IF(!ceh->WrapObject(aCx, nullptr, &obj)); retval.setObject(*obj); } } UniquePtr resHolder = MakeUnique( dom::StructuredCloneHolder::CloningSupported, dom::StructuredCloneHolder::TransferringNotSupported, JS::StructuredCloneScope::SameProcess); IgnoredErrorResult erv; resHolder->Write(aCx, retval, erv); // Failed to serialize the result, dispatch a runnable to reject // the promise returned to the caller of the mozIExtensionCallback // callWithPromiseResult method. if (NS_WARN_IF(erv.Failed())) { auto rejectMainThreadPromise = [error = erv.StealNSResult(), runnable = std::move(mWorkerRunnable), resHolder = std::move(resHolder)]() { RefPtr promiseResult = std::move(runnable->mPromiseResult); promiseResult->MaybeReject(error); }; nsCOMPtr runnable = NS_NewRunnableFunction(__func__, std::move(rejectMainThreadPromise)); NS_DispatchToMainThread(runnable); JS_ClearPendingException(aCx); return; } auto resolveMainThreadPromise = [callbackType = aCallbackType, resHolder = std::move(resHolder), runnable = std::move(mWorkerRunnable), workerRef = std::move(mWorkerRef)]() { RefPtr promiseResult = std::move(runnable->mPromiseResult); auto* global = promiseResult->GetGlobalObject(); dom::AutoEntryScript aes(global, "ExtensionListenerCallWorkerRunnable::WorkerRun"); JSContext* cx = aes.cx(); JS::Rooted jsvalue(cx); IgnoredErrorResult rv; resHolder->Read(global, cx, &jsvalue, rv); if (NS_WARN_IF(rv.Failed())) { promiseResult->MaybeReject(rv.StealNSResult()); JS_ClearPendingException(cx); } else { switch (callbackType) { case PromiseCallbackType::Resolve: promiseResult->MaybeResolve(jsvalue); break; case PromiseCallbackType::Reject: promiseResult->MaybeReject(jsvalue); break; } } }; nsCOMPtr runnable = NS_NewRunnableFunction(__func__, std::move(resolveMainThreadPromise)); NS_DispatchToMainThread(runnable); } void ExtensionListenerCallPromiseResultHandler::ResolvedCallback( JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { WorkerRunCallback(aCx, aValue, PromiseCallbackType::Resolve); } void ExtensionListenerCallPromiseResultHandler::RejectedCallback( JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { WorkerRunCallback(aCx, aValue, PromiseCallbackType::Reject); } } // namespace extensions } // namespace mozilla