diff options
Diffstat (limited to 'dom/ipc/jsactor')
21 files changed, 3121 insertions, 0 deletions
diff --git a/dom/ipc/jsactor/JSActor.cpp b/dom/ipc/jsactor/JSActor.cpp new file mode 100644 index 0000000000..8ac0442a35 --- /dev/null +++ b/dom/ipc/jsactor/JSActor.cpp @@ -0,0 +1,503 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/JSActor.h" +#include "mozilla/dom/JSActorBinding.h" + +#include "chrome/common/ipc_channel.h" +#include "mozilla/Attributes.h" +#include "mozilla/FunctionRef.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/ClonedErrorHolder.h" +#include "mozilla/dom/ClonedErrorHolderBinding.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/DOMExceptionBinding.h" +#include "mozilla/dom/JSActorManager.h" +#include "mozilla/dom/MessageManagerBinding.h" +#include "mozilla/dom/PWindowGlobal.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ipc/StructuredCloneData.h" +#include "mozilla/ProfilerMarkers.h" +#include "js/Promise.h" +#include "xpcprivate.h" +#include "nsFrameMessageManager.h" +#include "nsICrashReporter.h" + +namespace mozilla::dom { + +struct JSActorMessageMarker { + static constexpr Span<const char> MarkerTypeName() { + return MakeStringSpan("JSActorMessage"); + } + static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, + const ProfilerString8View& aActorName, + const ProfilerString16View& aMessageName) { + aWriter.StringProperty("actor", aActorName); + aWriter.StringProperty("name", NS_ConvertUTF16toUTF8(aMessageName)); + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyLabelFormatSearchable( + "actor", "Actor Name", MS::Format::String, MS::Searchable::Searchable); + schema.AddKeyLabelFormatSearchable( + "name", "Message Name", MS::Format::String, MS::Searchable::Searchable); + schema.SetTooltipLabel("JSActor - {marker.name}"); + schema.SetTableLabel( + "{marker.name} - [{marker.data.actor}] {marker.data.name}"); + return schema; + } +}; + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSActor) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(JSActor) +NS_IMPL_CYCLE_COLLECTING_RELEASE(JSActor) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(JSActor) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(JSActor) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWrappedJS) + tmp->mPendingQueries.Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(JSActor) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWrappedJS) + for (const auto& query : tmp->mPendingQueries.Values()) { + CycleCollectionNoteChild(cb, query.mPromise.get(), "Pending Query Promise"); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +JSActor::JSActor(nsISupports* aGlobal) { + mGlobal = do_QueryInterface(aGlobal); + if (!mGlobal) { + mGlobal = xpc::NativeGlobal(xpc::PrivilegedJunkScope()); + } +} + +void JSActor::StartDestroy() { mCanSend = false; } + +void JSActor::AfterDestroy() { + mCanSend = false; + + // Take our queries out, in case somehow rejecting promises can trigger + // additions or removals. + const nsTHashMap<nsUint64HashKey, PendingQuery> pendingQueries = + std::move(mPendingQueries); + for (const auto& entry : pendingQueries.Values()) { + nsPrintfCString message( + "Actor '%s' destroyed before query '%s' was resolved", mName.get(), + NS_LossyConvertUTF16toASCII(entry.mMessageName).get()); + entry.mPromise->MaybeRejectWithAbortError(message); + } + + InvokeCallback(CallbackFunction::DidDestroy); + ClearManager(); +} + +void JSActor::InvokeCallback(CallbackFunction callback) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + AutoEntryScript aes(GetParentObject(), "JSActor destroy callback"); + JSContext* cx = aes.cx(); + MozJSActorCallbacks callbacksHolder; + JS::Rooted<JS::Value> val(cx, JS::ObjectOrNullValue(GetWrapper())); + if (NS_WARN_IF(!callbacksHolder.Init(cx, val))) { + return; + } + + // Destroy callback is optional. + if (callback == CallbackFunction::DidDestroy) { + if (callbacksHolder.mDidDestroy.WasPassed()) { + callbacksHolder.mDidDestroy.Value()->Call(this); + } + } else { + if (callbacksHolder.mActorCreated.WasPassed()) { + callbacksHolder.mActorCreated.Value()->Call(this); + } + } +} + +nsresult JSActor::QueryInterfaceActor(const nsIID& aIID, void** aPtr) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + if (!GetWrapperPreserveColor()) { + // If we have no preserved wrapper, we won't implement any interfaces. + return NS_NOINTERFACE; + } + + if (!mWrappedJS) { + AutoEntryScript aes(GetParentObject(), "JSActor query interface"); + JSContext* cx = aes.cx(); + + JS::Rooted<JSObject*> self(cx, GetWrapper()); + JSAutoRealm ar(cx, self); + + RefPtr<nsXPCWrappedJS> wrappedJS; + nsresult rv = nsXPCWrappedJS::GetNewOrUsed( + cx, self, NS_GET_IID(nsISupports), getter_AddRefs(wrappedJS)); + NS_ENSURE_SUCCESS(rv, rv); + + mWrappedJS = do_QueryInterface(wrappedJS); + MOZ_ASSERT(mWrappedJS); + } + + return mWrappedJS->QueryInterface(aIID, aPtr); +} + +void JSActor::SetName(const nsACString& aName) { + MOZ_ASSERT(mName.IsEmpty(), "Cannot set name twice!"); + mName = aName; +} + +void JSActor::ThrowStateErrorForGetter(const char* aName, + ErrorResult& aRv) const { + if (mName.IsEmpty()) { + aRv.ThrowInvalidStateError(nsPrintfCString( + "Cannot access property '%s' before actor is initialized", aName)); + } else { + aRv.ThrowInvalidStateError(nsPrintfCString( + "Cannot access property '%s' after actor '%s' has been destroyed", + aName, mName.get())); + } +} + +static Maybe<ipc::StructuredCloneData> TryClone(JSContext* aCx, + JS::Handle<JS::Value> aValue) { + Maybe<ipc::StructuredCloneData> data{std::in_place}; + + // Try to directly serialize the passed-in data, and return it to our caller. + IgnoredErrorResult rv; + data->Write(aCx, aValue, rv); + if (rv.Failed()) { + // Serialization failed, return `Nothing()` instead. + JS_ClearPendingException(aCx); + data.reset(); + } + return data; +} + +static Maybe<ipc::StructuredCloneData> CloneJSStack( + JSContext* aCx, JS::Handle<JSObject*> aStack) { + JS::Rooted<JS::Value> stackVal(aCx, JS::ObjectOrNullValue(aStack)); + return TryClone(aCx, stackVal); +} + +static Maybe<ipc::StructuredCloneData> CaptureJSStack(JSContext* aCx) { + JS::Rooted<JSObject*> stack(aCx, nullptr); + if (JS::IsAsyncStackCaptureEnabledForRealm(aCx) && + !JS::CaptureCurrentStack(aCx, &stack)) { + JS_ClearPendingException(aCx); + } + + return CloneJSStack(aCx, stack); +} + +void JSActor::SendAsyncMessage(JSContext* aCx, const nsAString& aMessageName, + JS::Handle<JS::Value> aObj, ErrorResult& aRv) { + profiler_add_marker("SendAsyncMessage", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMessageName); + Maybe<ipc::StructuredCloneData> data{std::in_place}; + if (!nsFrameMessageManager::GetParamsForMessage( + aCx, aObj, JS::UndefinedHandleValue, *data)) { + aRv.ThrowDataCloneError(nsPrintfCString( + "Failed to serialize message '%s::%s'", + NS_LossyConvertUTF16toASCII(aMessageName).get(), mName.get())); + return; + } + + JSActorMessageMeta meta; + meta.actorName() = mName; + meta.messageName() = aMessageName; + meta.kind() = JSActorMessageKind::Message; + + SendRawMessage(meta, std::move(data), CaptureJSStack(aCx), aRv); +} + +already_AddRefed<Promise> JSActor::SendQuery(JSContext* aCx, + const nsAString& aMessageName, + JS::Handle<JS::Value> aObj, + ErrorResult& aRv) { + profiler_add_marker("SendQuery", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMessageName); + Maybe<ipc::StructuredCloneData> data{std::in_place}; + if (!nsFrameMessageManager::GetParamsForMessage( + aCx, aObj, JS::UndefinedHandleValue, *data)) { + aRv.ThrowDataCloneError(nsPrintfCString( + "Failed to serialize message '%s::%s'", + NS_LossyConvertUTF16toASCII(aMessageName).get(), mName.get())); + return nullptr; + } + + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + aRv.ThrowUnknownError("Unable to get current native global"); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + JSActorMessageMeta meta; + meta.actorName() = mName; + meta.messageName() = aMessageName; + meta.queryId() = mNextQueryId++; + meta.kind() = JSActorMessageKind::Query; + + mPendingQueries.InsertOrUpdate(meta.queryId(), + PendingQuery{promise, meta.messageName()}); + + SendRawMessage(meta, std::move(data), CaptureJSStack(aCx), aRv); + return promise.forget(); +} + +void JSActor::CallReceiveMessage(JSContext* aCx, + const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) { + // The argument which we want to pass to IPC. + RootedDictionary<ReceiveMessageArgument> argument(aCx); + argument.mTarget = this; + argument.mName = aMetadata.messageName(); + argument.mData = aData; + argument.mJson = aData; + argument.mSync = false; + + if (GetWrapperPreserveColor()) { + // Invoke the actual callback. + JS::Rooted<JSObject*> global(aCx, JS::GetNonCCWObjectGlobal(GetWrapper())); + RefPtr<MessageListener> messageListener = + new MessageListener(GetWrapper(), global, nullptr, nullptr); + messageListener->ReceiveMessage(argument, aRetVal, aRv, + "JSActor receive message", + MessageListener::eRethrowExceptions); + } else { + aRv.ThrowTypeError<MSG_NOT_CALLABLE>("Property 'receiveMessage'"); + } +} + +void JSActor::ReceiveMessage(JSContext* aCx, + const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, ErrorResult& aRv) { + MOZ_ASSERT(aMetadata.kind() == JSActorMessageKind::Message); + profiler_add_marker("ReceiveMessage", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMetadata.messageName()); + + JS::Rooted<JS::Value> retval(aCx); + CallReceiveMessage(aCx, aMetadata, aData, &retval, aRv); +} + +void JSActor::ReceiveQuery(JSContext* aCx, const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, ErrorResult& aRv) { + MOZ_ASSERT(aMetadata.kind() == JSActorMessageKind::Query); + profiler_add_marker("ReceiveQuery", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMetadata.messageName()); + + // This promise will be resolved or rejected once the listener has been + // called. Our listener on this promise will then send the reply. + RefPtr<Promise> promise = Promise::Create(GetParentObject(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + RefPtr<QueryHandler> handler = new QueryHandler(this, aMetadata, promise); + promise->AppendNativeHandler(handler); + + ErrorResult error; + JS::Rooted<JS::Value> retval(aCx); + CallReceiveMessage(aCx, aMetadata, aData, &retval, error); + + // If we have a promise, resolve or reject it respectively. + if (error.Failed()) { + if (error.IsUncatchableException()) { + promise->MaybeRejectWithTimeoutError( + "Message handler threw uncatchable exception"); + } else { + promise->MaybeReject(std::move(error)); + } + } else { + promise->MaybeResolve(retval); + } + error.SuppressException(); +} + +void JSActor::ReceiveQueryReply(JSContext* aCx, + const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, ErrorResult& aRv) { + if (NS_WARN_IF(aMetadata.actorName() != mName)) { + aRv.ThrowUnknownError("Mismatched actor name for query reply"); + return; + } + + Maybe<PendingQuery> query = mPendingQueries.Extract(aMetadata.queryId()); + if (NS_WARN_IF(!query)) { + aRv.ThrowUnknownError("Received reply for non-pending query"); + return; + } + + profiler_add_marker("ReceiveQueryReply", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMetadata.messageName()); + + Promise* promise = query->mPromise; + JSAutoRealm ar(aCx, promise->PromiseObj()); + JS::RootedValue data(aCx, aData); + if (NS_WARN_IF(!JS_WrapValue(aCx, &data))) { + aRv.NoteJSContextException(aCx); + return; + } + + if (aMetadata.kind() == JSActorMessageKind::QueryResolve) { + promise->MaybeResolve(data); + } else { + promise->MaybeReject(data); + } +} + +void JSActor::SendRawMessageInProcess(const JSActorMessageMeta& aMeta, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, + OtherSideCallback&& aGetOtherSide) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "JSActor Async Message", + [aMeta, data{std::move(aData)}, stack{std::move(aStack)}, + getOtherSide{std::move(aGetOtherSide)}]() mutable { + if (RefPtr<JSActorManager> otherSide = getOtherSide()) { + otherSide->ReceiveRawMessage(aMeta, std::move(data), + std::move(stack)); + } + })); +} + +// Native handler for our generated promise which is used to handle Queries and +// send the reply when their promises have been resolved. +JSActor::QueryHandler::QueryHandler(JSActor* aActor, + const JSActorMessageMeta& aMetadata, + Promise* aPromise) + : mActor(aActor), + mPromise(aPromise), + mMessageName(aMetadata.messageName()), + mQueryId(aMetadata.queryId()) {} + +void JSActor::QueryHandler::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + if (!mActor) { + // Make sure that this rejection is reported. See comment below. + if (!JS::CallOriginalPromiseReject(aCx, aValue)) { + JS_ClearPendingException(aCx); + } + return; + } + + JS::Rooted<JS::Value> value(aCx, aValue); + if (value.isObject()) { + JS::Rooted<JSObject*> error(aCx, &value.toObject()); + if (RefPtr<ClonedErrorHolder> ceh = + ClonedErrorHolder::Create(aCx, error, IgnoreErrors())) { + JS::RootedObject obj(aCx); + // Note: We can't use `ToJSValue` here because ClonedErrorHolder isn't + // wrapper cached. + if (ceh->WrapObject(aCx, nullptr, &obj)) { + value.setObject(*obj); + } else { + JS_ClearPendingException(aCx); + } + } else { + JS_ClearPendingException(aCx); + } + } + + Maybe<ipc::StructuredCloneData> data = TryClone(aCx, value); + if (!data) { + // Failed to clone the rejection value. Make sure that this + // rejection is reported, despite being "handled". This is done by + // creating a new promise in the rejected state, and throwing it + // away. This will be reported as an unhandled rejected promise. + if (!JS::CallOriginalPromiseReject(aCx, aValue)) { + JS_ClearPendingException(aCx); + } + } + + SendReply(aCx, JSActorMessageKind::QueryReject, std::move(data)); +} + +void JSActor::QueryHandler::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + if (!mActor) { + return; + } + + Maybe<ipc::StructuredCloneData> data{std::in_place}; + data->InitScope(JS::StructuredCloneScope::DifferentProcess); + + IgnoredErrorResult error; + data->Write(aCx, aValue, error); + if (NS_WARN_IF(error.Failed())) { + JS_ClearPendingException(aCx); + + nsAutoCString msg; + msg.Append(mActor->Name()); + msg.Append(':'); + msg.Append(NS_LossyConvertUTF16toASCII(mMessageName)); + msg.AppendLiteral(": message reply cannot be cloned."); + + auto exc = MakeRefPtr<Exception>(msg, NS_ERROR_FAILURE, "DataCloneError"_ns, + nullptr, nullptr); + + JS::Rooted<JS::Value> val(aCx); + if (ToJSValue(aCx, exc, &val)) { + RejectedCallback(aCx, val, aRv); + } else { + JS_ClearPendingException(aCx); + } + return; + } + + SendReply(aCx, JSActorMessageKind::QueryResolve, std::move(data)); +} + +void JSActor::QueryHandler::SendReply(JSContext* aCx, JSActorMessageKind aKind, + Maybe<ipc::StructuredCloneData>&& aData) { + MOZ_ASSERT(mActor); + profiler_add_marker("SendQueryReply", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mActor->Name(), mMessageName); + + JSActorMessageMeta meta; + meta.actorName() = mActor->Name(); + meta.messageName() = mMessageName; + meta.queryId() = mQueryId; + meta.kind() = aKind; + + JS::Rooted<JSObject*> promise(aCx, mPromise->PromiseObj()); + JS::Rooted<JSObject*> stack(aCx, JS::GetPromiseResolutionSite(promise)); + + mActor->SendRawMessage(meta, std::move(aData), CloneJSStack(aCx, stack), + IgnoreErrors()); + mActor = nullptr; + mPromise = nullptr; +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSActor::QueryHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(JSActor::QueryHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(JSActor::QueryHandler) + +NS_IMPL_CYCLE_COLLECTION(JSActor::QueryHandler, mActor, mPromise) + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSActor.h b/dom/ipc/jsactor/JSActor.h new file mode 100644 index 0000000000..54af93d1ce --- /dev/null +++ b/dom/ipc/jsactor/JSActor.h @@ -0,0 +1,173 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef mozilla_dom_JSActor_h +#define mozilla_dom_JSActor_h + +#include "js/TypeDecls.h" +#include "ipc/EnumSerializer.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashMap.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; +class nsQueryJSActor; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +namespace ipc { +class StructuredCloneData; +} + +class JSActorManager; +class JSActorMessageMeta; +class QueryPromiseHandler; + +enum class JSActorMessageKind { + Message, + Query, + QueryResolve, + QueryReject, + EndGuard_, +}; + +// Common base class for JSWindowActor{Parent,Child}. +class JSActor : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(JSActor) + + explicit JSActor(nsISupports* aGlobal = nullptr); + + const nsCString& Name() const { return mName; } + void GetName(nsCString& aName) { aName = Name(); } + + void SendAsyncMessage(JSContext* aCx, const nsAString& aMessageName, + JS::Handle<JS::Value> aObj, ErrorResult& aRv); + + already_AddRefed<Promise> SendQuery(JSContext* aCx, + const nsAString& aMessageName, + JS::Handle<JS::Value> aObj, + ErrorResult& aRv); + + nsIGlobalObject* GetParentObject() const { return mGlobal; }; + + protected: + // Send the message described by the structured clone data |aData|, and the + // message metadata |aMetadata|. The underlying transport should call the + // |ReceiveMessage| method on the other side asynchronously. + virtual void SendRawMessage(const JSActorMessageMeta& aMetadata, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, + ErrorResult& aRv) = 0; + + // Helper method to send an in-process raw message. + using OtherSideCallback = std::function<already_AddRefed<JSActorManager>()>; + static void SendRawMessageInProcess(const JSActorMessageMeta& aMeta, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, + OtherSideCallback&& aGetOtherSide); + + virtual ~JSActor() = default; + + void SetName(const nsACString& aName); + + bool CanSend() const { return mCanSend; } + + void ThrowStateErrorForGetter(const char* aName, ErrorResult& aRv) const; + + void StartDestroy(); + void AfterDestroy(); + + enum class CallbackFunction { DidDestroy, ActorCreated }; + void InvokeCallback(CallbackFunction callback); + + virtual void ClearManager() = 0; + + private: + friend class JSActorManager; + friend class ::nsQueryJSActor; // for QueryInterfaceActor + + nsresult QueryInterfaceActor(const nsIID& aIID, void** aPtr); + + // Called by JSActorManager when they receive raw message data destined for + // this actor. + void ReceiveMessage(JSContext* aCx, const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, ErrorResult& aRv); + void ReceiveQuery(JSContext* aCx, const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, ErrorResult& aRv); + void ReceiveQueryReply(JSContext* aCx, const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, ErrorResult& aRv); + + // Call the actual `ReceiveMessage` method, and get the return value. + void CallReceiveMessage(JSContext* aCx, const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv); + + // Helper object used while processing query messages to send the final reply + // message. + class QueryHandler final : public PromiseNativeHandler { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(QueryHandler) + + QueryHandler(JSActor* aActor, const JSActorMessageMeta& aMetadata, + Promise* aPromise); + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + private: + ~QueryHandler() = default; + + void SendReply(JSContext* aCx, JSActorMessageKind aKind, + Maybe<ipc::StructuredCloneData>&& aData); + + RefPtr<JSActor> mActor; + RefPtr<Promise> mPromise; + nsString mMessageName; + uint64_t mQueryId; + }; + + // A query which hasn't been resolved yet, along with metadata about what + // query the promise is for. + struct PendingQuery { + RefPtr<Promise> mPromise; + nsString mMessageName; + }; + + nsCOMPtr<nsIGlobalObject> mGlobal; + nsCOMPtr<nsISupports> mWrappedJS; + nsCString mName; + nsTHashMap<nsUint64HashKey, PendingQuery> mPendingQueries; + uint64_t mNextQueryId = 0; + bool mCanSend = true; +}; + +} // namespace dom +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::JSActorMessageKind> + : public ContiguousEnumSerializer< + mozilla::dom::JSActorMessageKind, + mozilla::dom::JSActorMessageKind::Message, + mozilla::dom::JSActorMessageKind::EndGuard_> {}; + +} // namespace IPC + +#endif // !defined(mozilla_dom_JSActor_h) diff --git a/dom/ipc/jsactor/JSActorManager.cpp b/dom/ipc/jsactor/JSActorManager.cpp new file mode 100644 index 0000000000..237c00f3a4 --- /dev/null +++ b/dom/ipc/jsactor/JSActorManager.cpp @@ -0,0 +1,250 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/JSActorManager.h" + +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/JSActorService.h" +#include "mozilla/dom/PWindowGlobal.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/CycleCollectedJSRuntime.h" +#include "mozilla/ScopeExit.h" +#include "mozJSModuleLoader.h" +#include "jsapi.h" +#include "js/CallAndConstruct.h" // JS::Construct +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "nsContentUtils.h" + +namespace mozilla::dom { + +already_AddRefed<JSActor> JSActorManager::GetActor(JSContext* aCx, + const nsACString& aName, + ErrorResult& aRv) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + // If our connection has been closed, return an error. + mozilla::ipc::IProtocol* nativeActor = AsNativeActor(); + if (!nativeActor->CanSend()) { + aRv.ThrowInvalidStateError(nsPrintfCString( + "Cannot get actor '%s'. Native '%s' actor is destroyed.", + PromiseFlatCString(aName).get(), nativeActor->GetProtocolName())); + return nullptr; + } + + // Check if this actor has already been created, and return it if it has. + if (RefPtr<JSActor> actor = mJSActors.Get(aName)) { + return actor.forget(); + } + + RefPtr<JSActorService> actorSvc = JSActorService::GetSingleton(); + if (!actorSvc) { + aRv.ThrowInvalidStateError("JSActorService hasn't been initialized"); + return nullptr; + } + + // Check if this actor satisfies the requirements of the protocol + // corresponding to `aName`, and get the module which implements it. + RefPtr<JSActorProtocol> protocol = + MatchingJSActorProtocol(actorSvc, aName, aRv); + if (!protocol) { + return nullptr; + } + + bool isParent = nativeActor->GetSide() == mozilla::ipc::ParentSide; + auto& side = isParent ? protocol->Parent() : protocol->Child(); + + // We're about to construct the actor, so make sure we're in the JSM realm + // while importing etc. + JSAutoRealm ar(aCx, xpc::PrivilegedJunkScope()); + + // Load the module using mozJSModuleLoader. + RefPtr loader = mozJSModuleLoader::Get(); + MOZ_ASSERT(loader); + + // If a module URI was provided, use it to construct an instance of the actor. + JS::Rooted<JSObject*> actorObj(aCx); + if (side.mModuleURI || side.mESModuleURI) { + JS::Rooted<JSObject*> exports(aCx); + if (side.mModuleURI) { + JS::Rooted<JSObject*> global(aCx); + aRv = loader->Import(aCx, side.mModuleURI.ref(), &global, &exports); + if (aRv.Failed()) { + return nullptr; + } + } else { + aRv = loader->ImportESModule(aCx, side.mESModuleURI.ref(), &exports); + if (aRv.Failed()) { + return nullptr; + } + } + MOZ_ASSERT(exports, "null exports!"); + + // Load the specific property from our module. + JS::Rooted<JS::Value> ctor(aCx); + nsAutoCString ctorName(aName); + ctorName.Append(isParent ? "Parent"_ns : "Child"_ns); + if (!JS_GetProperty(aCx, exports, ctorName.get(), &ctor)) { + aRv.NoteJSContextException(aCx); + return nullptr; + } + + if (NS_WARN_IF(!ctor.isObject())) { + aRv.ThrowNotFoundError(nsPrintfCString( + "Could not find actor constructor '%s'", ctorName.get())); + return nullptr; + } + + // Invoke the constructor loaded from the module. + if (!JS::Construct(aCx, ctor, JS::HandleValueArray::empty(), &actorObj)) { + aRv.NoteJSContextException(aCx); + return nullptr; + } + } + + // Initialize our newly-constructed actor, and return it. + RefPtr<JSActor> actor = InitJSActor(actorObj, aName, aRv); + if (aRv.Failed()) { + return nullptr; + } + mJSActors.InsertOrUpdate(aName, RefPtr{actor}); + return actor.forget(); +} + +already_AddRefed<JSActor> JSActorManager::GetExistingActor( + const nsACString& aName) { + if (!AsNativeActor()->CanSend()) { + return nullptr; + } + return mJSActors.Get(aName); +} + +#define CHILD_DIAGNOSTIC_ASSERT(test, msg) \ + do { \ + if (XRE_IsParentProcess()) { \ + MOZ_ASSERT(test, msg); \ + } else { \ + MOZ_DIAGNOSTIC_ASSERT(test, msg); \ + } \ + } while (0) + +void JSActorManager::ReceiveRawMessage( + const JSActorMessageMeta& aMetadata, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + CrashReporter::AutoAnnotateCrashReport autoActorName( + CrashReporter::Annotation::JSActorName, aMetadata.actorName()); + CrashReporter::AutoAnnotateCrashReport autoMessageName( + CrashReporter::Annotation::JSActorMessage, + NS_LossyConvertUTF16toASCII(aMetadata.messageName())); + + // We're going to be running JS. Enter the privileged junk realm so we can set + // up our JS state correctly. + AutoEntryScript aes(xpc::PrivilegedJunkScope(), "JSActor message handler"); + JSContext* cx = aes.cx(); + + // Ensure any errors reported to `error` are set on the scope, so they're + // reported. + ErrorResult error; + auto autoSetException = + MakeScopeExit([&] { Unused << error.MaybeSetPendingException(cx); }); + + // If an async stack was provided, set up our async stack state. + JS::Rooted<JSObject*> stack(cx); + Maybe<JS::AutoSetAsyncStackForNewCalls> stackSetter; + { + JS::Rooted<JS::Value> stackVal(cx); + if (aStack) { + aStack->Read(cx, &stackVal, error); + if (error.Failed()) { + error.SuppressException(); + JS_ClearPendingException(cx); + stackVal.setUndefined(); + } + } + + if (stackVal.isObject()) { + stack = &stackVal.toObject(); + if (!js::IsSavedFrame(stack)) { + CHILD_DIAGNOSTIC_ASSERT(false, "Stack must be a SavedFrame object"); + error.ThrowDataError("Actor async stack must be a SavedFrame object"); + return; + } + stackSetter.emplace(cx, stack, "JSActor query"); + } + } + + RefPtr<JSActor> actor = GetActor(cx, aMetadata.actorName(), error); + if (error.Failed()) { + return; + } + + JS::Rooted<JS::Value> data(cx); + if (aData) { + aData->Read(cx, &data, error); + if (error.Failed()) { + CHILD_DIAGNOSTIC_ASSERT(CycleCollectedJSRuntime::Get()->OOMReported(), + "Should not receive non-decodable data"); + return; + } + } + + switch (aMetadata.kind()) { + case JSActorMessageKind::QueryResolve: + case JSActorMessageKind::QueryReject: + actor->ReceiveQueryReply(cx, aMetadata, data, error); + break; + + case JSActorMessageKind::Message: + actor->ReceiveMessage(cx, aMetadata, data, error); + break; + + case JSActorMessageKind::Query: + actor->ReceiveQuery(cx, aMetadata, data, error); + break; + + default: + MOZ_ASSERT_UNREACHABLE(); + } +} + +void JSActorManager::JSActorWillDestroy() { + for (const auto& entry : mJSActors.Values()) { + entry->StartDestroy(); + } +} + +void JSActorManager::JSActorDidDestroy() { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + CrashReporter::AutoAnnotateCrashReport autoMessageName( + CrashReporter::Annotation::JSActorMessage, "<DidDestroy>"_ns); + + // Swap the table with `mJSActors` so that we don't invalidate it while + // iterating. + const nsRefPtrHashtable<nsCStringHashKey, JSActor> actors = + std::move(mJSActors); + for (const auto& entry : actors.Values()) { + CrashReporter::AutoAnnotateCrashReport autoActorName( + CrashReporter::Annotation::JSActorName, entry->Name()); + // Do not risk to run script very late in shutdown + if (!AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownFinal)) { + entry->AfterDestroy(); + } + } +} + +void JSActorManager::JSActorUnregister(const nsACString& aName) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + RefPtr<JSActor> actor; + if (mJSActors.Remove(aName, getter_AddRefs(actor))) { + actor->AfterDestroy(); + } +} + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSActorManager.h b/dom/ipc/jsactor/JSActorManager.h new file mode 100644 index 0000000000..86efa9d7af --- /dev/null +++ b/dom/ipc/jsactor/JSActorManager.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_JSActorManager_h +#define mozilla_dom_JSActorManager_h + +#include "js/TypeDecls.h" +#include "mozilla/dom/JSActor.h" +#include "nsRefPtrHashtable.h" +#include "nsString.h" + +namespace mozilla { +class ErrorResult; + +namespace ipc { +class IProtocol; +} + +namespace dom { + +class JSActorProtocol; +class JSActorService; + +class JSActorManager : public nsISupports { + public: + /** + * Get or create an actor by its name. + * + * Will set an error on |aRv| if the actor fails to be constructed. + */ + already_AddRefed<JSActor> GetActor(JSContext* aCx, const nsACString& aName, + ErrorResult& aRv); + + /** + * Look up an existing actor by its name, returning nullptr if it doesn't + * already exist. Will not attempt to create the actor. + */ + already_AddRefed<JSActor> GetExistingActor(const nsACString& aName); + + /** + * Handle receiving a raw message from the other side. + */ + void ReceiveRawMessage(const JSActorMessageMeta& aMetadata, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack); + + protected: + /** + * The actor is about to be destroyed so prevent it from sending any + * more messages. + */ + void JSActorWillDestroy(); + + /** + * Lifecycle method which will fire the `didDestroy` methods on relevant + * actors. + */ + void JSActorDidDestroy(); + + /** + * Return the protocol with the given name, if it is supported by the current + * actor. + */ + virtual already_AddRefed<JSActorProtocol> MatchingJSActorProtocol( + JSActorService* aActorSvc, const nsACString& aName, ErrorResult& aRv) = 0; + + /** + * Initialize a JSActor instance given the constructed JS object. + * `aMaybeActor` may be `nullptr`, which should construct the default empty + * actor. + */ + virtual already_AddRefed<JSActor> InitJSActor( + JS::Handle<JSObject*> aMaybeActor, const nsACString& aName, + ErrorResult& aRv) = 0; + + /** + * Return this native actor. This should be the same object which is + * implementing `JSActorManager`. + */ + virtual mozilla::ipc::IProtocol* AsNativeActor() = 0; + + private: + friend class JSActorService; + + /** + * Note that a particular actor name has been unregistered, and fire the + * `didDestroy` method on the actor, if it's been initialized. + */ + void JSActorUnregister(const nsACString& aName); + + nsRefPtrHashtable<nsCStringHashKey, JSActor> mJSActors; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_JSActorManager_h diff --git a/dom/ipc/jsactor/JSActorProtocolUtils.h b/dom/ipc/jsactor/JSActorProtocolUtils.h new file mode 100644 index 0000000000..b1fb4d461c --- /dev/null +++ b/dom/ipc/jsactor/JSActorProtocolUtils.h @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_JSActorProtocolUtils_h +#define mozilla_dom_JSActorProtocolUtils_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/ErrorResult.h" // ErrorResult + +namespace mozilla { + +namespace dom { + +class JSActorProtocolUtils { + public: + template <typename ProtoT, typename ActorInfoT> + static void FromIPCShared(ProtoT& aProto, const ActorInfoT& aInfo) { + aProto->mRemoteTypes = aInfo.remoteTypes().Clone(); + + if (aInfo.isESModule()) { + aProto->mChild.mESModuleURI = aInfo.url(); + } else { + aProto->mChild.mModuleURI = aInfo.url(); + } + + aProto->mChild.mObservers = aInfo.observers().Clone(); + } + + template <typename ProtoT, typename ActorInfoT> + static void ToIPCShared(ActorInfoT& aInfo, const ProtoT& aProto) { + aInfo.name() = aProto->mName; + + aInfo.remoteTypes() = aProto->mRemoteTypes.Clone(); + + if (aProto->mChild.mModuleURI) { + aInfo.url() = aProto->mChild.mModuleURI; + aInfo.isESModule() = false; + } else { + aInfo.url() = aProto->mChild.mESModuleURI; + aInfo.isESModule() = true; + } + + aInfo.observers() = aProto->mChild.mObservers.Clone(); + } + + template <typename ProtoT, typename ActorOptionsT> + static bool FromWebIDLOptionsShared(ProtoT& aProto, + const ActorOptionsT& aOptions, + ErrorResult& aRv) { + if (aOptions.mRemoteTypes.WasPassed()) { + MOZ_ASSERT(aOptions.mRemoteTypes.Value().Length()); + aProto->mRemoteTypes = aOptions.mRemoteTypes.Value(); + } + + if (aOptions.mParent.WasPassed()) { + const auto& parentOptions = aOptions.mParent.Value(); + + if (parentOptions.mModuleURI.WasPassed()) { + if (parentOptions.mEsModuleURI.WasPassed()) { + aRv.ThrowNotSupportedError( + "moduleURI and esModuleURI are mutually exclusive."); + return false; + } + + aProto->mParent.mModuleURI.emplace(parentOptions.mModuleURI.Value()); + } else if (parentOptions.mEsModuleURI.WasPassed()) { + aProto->mParent.mESModuleURI.emplace( + parentOptions.mEsModuleURI.Value()); + } else { + aRv.ThrowNotSupportedError( + "Either moduleURI or esModuleURI is required."); + return false; + } + } + if (aOptions.mChild.WasPassed()) { + const auto& childOptions = aOptions.mChild.Value(); + + if (childOptions.mModuleURI.WasPassed()) { + if (childOptions.mEsModuleURI.WasPassed()) { + aRv.ThrowNotSupportedError( + "moduleURI and esModuleURI are exclusive."); + return false; + } + + aProto->mChild.mModuleURI.emplace(childOptions.mModuleURI.Value()); + } else if (childOptions.mEsModuleURI.WasPassed()) { + aProto->mChild.mESModuleURI.emplace(childOptions.mEsModuleURI.Value()); + } else { + aRv.ThrowNotSupportedError( + "Either moduleURI or esModuleURI is required."); + return false; + } + } + + if (!aOptions.mChild.WasPassed() && !aOptions.mParent.WasPassed()) { + aRv.ThrowNotSupportedError( + "No point registering an actor with neither child nor parent " + "specifications."); + return false; + } + + if (aOptions.mChild.WasPassed() && + aOptions.mChild.Value().mObservers.WasPassed()) { + aProto->mChild.mObservers = aOptions.mChild.Value().mObservers.Value(); + } + + return true; + } +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_JSActorProtocolUtils_h diff --git a/dom/ipc/jsactor/JSActorService.cpp b/dom/ipc/jsactor/JSActorService.cpp new file mode 100644 index 0000000000..3fde76a206 --- /dev/null +++ b/dom/ipc/jsactor/JSActorService.cpp @@ -0,0 +1,322 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* 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 "mozilla/dom/JSActorService.h" +#include "mozilla/dom/ChromeUtilsBinding.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/EventListenerBinding.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTargetBinding.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/InProcessChild.h" +#include "mozilla/dom/InProcessParent.h" +#include "mozilla/dom/JSActorManager.h" +#include "mozilla/dom/JSProcessActorBinding.h" +#include "mozilla/dom/JSProcessActorChild.h" +#include "mozilla/dom/JSProcessActorProtocol.h" +#include "mozilla/dom/JSWindowActorBinding.h" +#include "mozilla/dom/JSWindowActorChild.h" +#include "mozilla/dom/JSWindowActorProtocol.h" +#include "mozilla/dom/MessageManagerBinding.h" +#include "mozilla/dom/PContent.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/ArrayAlgorithm.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Logging.h" +#include "nsIObserverService.h" + +namespace mozilla::dom { +namespace { +StaticRefPtr<JSActorService> gJSActorService; +} + +JSActorService::JSActorService() { MOZ_ASSERT(NS_IsMainThread()); } + +JSActorService::~JSActorService() { MOZ_ASSERT(NS_IsMainThread()); } + +/* static */ +already_AddRefed<JSActorService> JSActorService::GetSingleton() { + MOZ_ASSERT(NS_IsMainThread()); + if (!gJSActorService) { + gJSActorService = new JSActorService(); + ClearOnShutdown(&gJSActorService); + } + + RefPtr<JSActorService> service = gJSActorService.get(); + return service.forget(); +} + +void JSActorService::RegisterWindowActor(const nsACString& aName, + const WindowActorOptions& aOptions, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(XRE_IsParentProcess()); + + const auto proto = mWindowActorDescriptors.WithEntryHandle( + aName, [&](auto&& entry) -> RefPtr<JSWindowActorProtocol> { + if (entry) { + aRv.ThrowNotSupportedError( + nsPrintfCString("'%s' actor is already registered.", + PromiseFlatCString(aName).get())); + return nullptr; + } + + // Insert a new entry for the protocol. + RefPtr<JSWindowActorProtocol> protocol = + JSWindowActorProtocol::FromWebIDLOptions(aName, aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + entry.Insert(protocol); + + return protocol; + }); + + if (!proto) { + MOZ_ASSERT(aRv.Failed()); + return; + } + + // Send information about the newly added entry to every existing content + // process. + AutoTArray<JSWindowActorInfo, 1> windowInfos{proto->ToIPC()}; + nsTArray<JSProcessActorInfo> contentInfos{}; + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + Unused << cp->SendInitJSActorInfos(contentInfos, windowInfos); + } + + // Register event listeners for any existing chrome targets. + for (EventTarget* target : mChromeEventTargets) { + proto->RegisterListenersFor(target); + } + + // Add observers to the protocol. + proto->AddObservers(); +} + +void JSActorService::UnregisterWindowActor(const nsACString& aName) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + CrashReporter::AutoAnnotateCrashReport autoActorName( + CrashReporter::Annotation::JSActorName, aName); + CrashReporter::AutoAnnotateCrashReport autoMessageName( + CrashReporter::Annotation::JSActorMessage, "<Unregister>"_ns); + + nsAutoCString name(aName); + RefPtr<JSWindowActorProtocol> proto; + if (mWindowActorDescriptors.Remove(name, getter_AddRefs(proto))) { + // Remove listeners for this actor from each of our chrome targets. + for (EventTarget* target : mChromeEventTargets) { + proto->UnregisterListenersFor(target); + } + + // Remove observers for this actor from observer serivce. + proto->RemoveObservers(); + + // Tell every content process to also unregister, and accumulate the set of + // potential managers, to have the actor disabled. + nsTArray<RefPtr<JSActorManager>> managers; + if (XRE_IsParentProcess()) { + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + Unused << cp->SendUnregisterJSWindowActor(name); + for (const auto& bp : cp->ManagedPBrowserParent()) { + for (const auto& wgp : bp->ManagedPWindowGlobalParent()) { + managers.AppendElement(static_cast<WindowGlobalParent*>(wgp)); + } + } + } + + for (const auto& wgp : + InProcessParent::Singleton()->ManagedPWindowGlobalParent()) { + managers.AppendElement(static_cast<WindowGlobalParent*>(wgp)); + } + for (const auto& wgc : + InProcessChild::Singleton()->ManagedPWindowGlobalChild()) { + managers.AppendElement(static_cast<WindowGlobalChild*>(wgc)); + } + } else { + for (const auto& bc : + ContentChild::GetSingleton()->ManagedPBrowserChild()) { + for (const auto& wgc : bc->ManagedPWindowGlobalChild()) { + managers.AppendElement(static_cast<WindowGlobalChild*>(wgc)); + } + } + } + + for (auto& mgr : managers) { + mgr->JSActorUnregister(name); + } + } +} + +void JSActorService::LoadJSActorInfos(nsTArray<JSProcessActorInfo>& aProcess, + nsTArray<JSWindowActorInfo>& aWindow) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(XRE_IsContentProcess()); + + for (auto& info : aProcess) { + // Create our JSProcessActorProtocol, register it in + // mProcessActorDescriptors. + auto name = info.name(); + RefPtr<JSProcessActorProtocol> proto = + JSProcessActorProtocol::FromIPC(std::move(info)); + mProcessActorDescriptors.InsertOrUpdate(std::move(name), RefPtr{proto}); + + // Add observers for each actor. + proto->AddObservers(); + } + + for (auto& info : aWindow) { + auto name = info.name(); + RefPtr<JSWindowActorProtocol> proto = + JSWindowActorProtocol::FromIPC(std::move(info)); + mWindowActorDescriptors.InsertOrUpdate(std::move(name), RefPtr{proto}); + + // Register listeners for each chrome target. + for (EventTarget* target : mChromeEventTargets) { + proto->RegisterListenersFor(target); + } + + // Add observers for each actor. + proto->AddObservers(); + } +} + +void JSActorService::GetJSWindowActorInfos( + nsTArray<JSWindowActorInfo>& aInfos) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(XRE_IsParentProcess()); + + for (const auto& data : mWindowActorDescriptors.Values()) { + aInfos.AppendElement(data->ToIPC()); + } +} + +void JSActorService::RegisterChromeEventTarget(EventTarget* aTarget) { + MOZ_ASSERT(!mChromeEventTargets.Contains(aTarget)); + mChromeEventTargets.AppendElement(aTarget); + + // Register event listeners on the newly added Window Root. + for (const auto& data : mWindowActorDescriptors.Values()) { + data->RegisterListenersFor(aTarget); + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + obs->NotifyObservers(aTarget, "chrome-event-target-created", nullptr); +} + +/* static */ +void JSActorService::UnregisterChromeEventTarget(EventTarget* aTarget) { + if (gJSActorService) { + // NOTE: No need to unregister listeners here, as the target is going away. + gJSActorService->mChromeEventTargets.RemoveElement(aTarget); + } +} + +void JSActorService::RegisterProcessActor(const nsACString& aName, + const ProcessActorOptions& aOptions, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(XRE_IsParentProcess()); + + const auto proto = mProcessActorDescriptors.WithEntryHandle( + aName, [&](auto&& entry) -> RefPtr<JSProcessActorProtocol> { + if (entry) { + aRv.ThrowNotSupportedError( + nsPrintfCString("'%s' actor is already registered.", + PromiseFlatCString(aName).get())); + return nullptr; + } + + // Insert a new entry for the protocol. + RefPtr<JSProcessActorProtocol> protocol = + JSProcessActorProtocol::FromWebIDLOptions(aName, aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + entry.Insert(protocol); + + return protocol; + }); + + if (!proto) { + MOZ_ASSERT(aRv.Failed()); + return; + } + + // Send information about the newly added entry to every existing content + // process. + AutoTArray<JSProcessActorInfo, 1> contentInfos{proto->ToIPC()}; + nsTArray<JSWindowActorInfo> windowInfos{}; + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + Unused << cp->SendInitJSActorInfos(contentInfos, windowInfos); + } + + // Add observers to the protocol. + proto->AddObservers(); +} + +void JSActorService::UnregisterProcessActor(const nsACString& aName) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + CrashReporter::AutoAnnotateCrashReport autoActorName( + CrashReporter::Annotation::JSActorName, aName); + CrashReporter::AutoAnnotateCrashReport autoMessageName( + CrashReporter::Annotation::JSActorMessage, "<Unregister>"_ns); + + nsAutoCString name(aName); + RefPtr<JSProcessActorProtocol> proto; + if (mProcessActorDescriptors.Remove(name, getter_AddRefs(proto))) { + // Remove observers for this actor from observer serivce. + proto->RemoveObservers(); + + // Tell every content process to also unregister, and accumulate the set of + // potential managers, to have the actor disabled. + nsTArray<RefPtr<JSActorManager>> managers; + if (XRE_IsParentProcess()) { + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + Unused << cp->SendUnregisterJSProcessActor(name); + managers.AppendElement(cp); + } + managers.AppendElement(InProcessChild::Singleton()); + managers.AppendElement(InProcessParent::Singleton()); + } else { + managers.AppendElement(ContentChild::GetSingleton()); + } + + for (auto& mgr : managers) { + mgr->JSActorUnregister(name); + } + } +} + +void JSActorService::GetJSProcessActorInfos( + nsTArray<JSProcessActorInfo>& aInfos) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(XRE_IsParentProcess()); + + for (const auto& data : mProcessActorDescriptors.Values()) { + aInfos.AppendElement(data->ToIPC()); + } +} + +already_AddRefed<JSProcessActorProtocol> +JSActorService::GetJSProcessActorProtocol(const nsACString& aName) { + return mProcessActorDescriptors.Get(aName); +} + +already_AddRefed<JSWindowActorProtocol> +JSActorService::GetJSWindowActorProtocol(const nsACString& aName) { + return mWindowActorDescriptors.Get(aName); +} + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSActorService.h b/dom/ipc/jsactor/JSActorService.h new file mode 100644 index 0000000000..e3296e1934 --- /dev/null +++ b/dom/ipc/jsactor/JSActorService.h @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_JSActorService_h +#define mozilla_dom_JSActorService_h + +#include "mozilla/dom/BrowsingContext.h" +#include "nsIURI.h" +#include "nsRefPtrHashtable.h" +#include "nsString.h" +#include "nsTArray.h" +#include "mozilla/dom/JSActor.h" +#include "nsIObserver.h" +#include "nsIDOMEventListener.h" +#include "mozilla/EventListenerManager.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +struct ProcessActorOptions; +struct WindowActorOptions; +class JSProcessActorInfo; +class JSWindowActorInfo; +class EventTarget; +class JSWindowActorProtocol; +class JSProcessActorProtocol; + +class JSActorService final { + public: + NS_INLINE_DECL_REFCOUNTING(JSActorService) + + static already_AddRefed<JSActorService> GetSingleton(); + + // Register or unregister a chrome event target. + void RegisterChromeEventTarget(EventTarget* aTarget); + + // NOTE: This method is static, as it may be called during shutdown. + static void UnregisterChromeEventTarget(EventTarget* aTarget); + + // Register child's Actor for content process. + void LoadJSActorInfos(nsTArray<JSProcessActorInfo>& aProcess, + nsTArray<JSWindowActorInfo>& aWindow); + + // --- Window Actor + + void RegisterWindowActor(const nsACString& aName, + const WindowActorOptions& aOptions, + ErrorResult& aRv); + + void UnregisterWindowActor(const nsACString& aName); + + // Get the named of Window Actor and the child's WindowActorOptions + // from mDescriptors to JSWindowActorInfos. + void GetJSWindowActorInfos(nsTArray<JSWindowActorInfo>& aInfos); + + already_AddRefed<JSWindowActorProtocol> GetJSWindowActorProtocol( + const nsACString& aName); + + // -- Content Actor + + void RegisterProcessActor(const nsACString& aName, + const ProcessActorOptions& aOptions, + ErrorResult& aRv); + + void UnregisterProcessActor(const nsACString& aName); + + // Get the named of Content Actor and the child's ProcessActorOptions + // from mDescriptors to JSProcessActorInfos. + void GetJSProcessActorInfos(nsTArray<JSProcessActorInfo>& aInfos); + + already_AddRefed<JSProcessActorProtocol> GetJSProcessActorProtocol( + const nsACString& aName); + + private: + JSActorService(); + ~JSActorService(); + + nsTArray<EventTarget*> mChromeEventTargets; + + // -- Window Actor + nsRefPtrHashtable<nsCStringHashKey, JSWindowActorProtocol> + mWindowActorDescriptors; + + // -- Process Actor + nsRefPtrHashtable<nsCStringHashKey, JSProcessActorProtocol> + mProcessActorDescriptors; +}; + +/** + * Base clsas for both `JSWindowActorProtocol` and `JSProcessActorProtocol` + * which can be used by generic code. + */ +class JSActorProtocol : public nsISupports { + public: + struct Sided { + Maybe<nsCString> mModuleURI; + Maybe<nsCString> mESModuleURI; + }; + + virtual const Sided& Parent() const = 0; + virtual const Sided& Child() const = 0; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_JSActorService_h diff --git a/dom/ipc/jsactor/JSProcessActorChild.cpp b/dom/ipc/jsactor/JSProcessActorChild.cpp new file mode 100644 index 0000000000..a9379838cf --- /dev/null +++ b/dom/ipc/jsactor/JSProcessActorChild.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/ContentChild.h" +#include "mozilla/dom/JSProcessActorBinding.h" +#include "mozilla/dom/JSProcessActorChild.h" +#include "mozilla/dom/InProcessChild.h" +#include "mozilla/dom/InProcessParent.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(JSProcessActorChild, JSActor, mManager) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSProcessActorChild) +NS_INTERFACE_MAP_END_INHERITING(JSActor) + +NS_IMPL_ADDREF_INHERITED(JSProcessActorChild, JSActor) +NS_IMPL_RELEASE_INHERITED(JSProcessActorChild, JSActor) + +JSObject* JSProcessActorChild::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return JSProcessActorChild_Binding::Wrap(aCx, this, aGivenProto); +} + +void JSProcessActorChild::SendRawMessage( + const JSActorMessageMeta& aMeta, Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, ErrorResult& aRv) { + if (NS_WARN_IF(!CanSend() || !mManager || !mManager->GetCanSend())) { + aRv.ThrowInvalidStateError("JSProcessActorChild cannot send at the moment"); + return; + } + + // If the parent side is in the same process, we have a PInProcess manager, + // and can dispatch the message directly to the event loop. + ContentChild* contentChild = mManager->AsContentChild(); + if (!contentChild) { + SendRawMessageInProcess(aMeta, std::move(aData), std::move(aStack), []() { + return do_AddRef(InProcessParent::Singleton()); + }); + return; + } + + // Cross-process case - send data over ContentChild to other side. + Maybe<ClonedMessageData> msgData; + if (aData) { + msgData.emplace(); + if (NS_WARN_IF(!aData->BuildClonedMessageData(*msgData))) { + aRv.ThrowDataCloneError( + nsPrintfCString("JSProcessActorChild serialization error: cannot " + "clone, in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; + } + } + + Maybe<ClonedMessageData> stackData; + if (aStack) { + stackData.emplace(); + if (!aStack->BuildClonedMessageData(*stackData)) { + stackData.reset(); + } + } + + if (NS_WARN_IF(!contentChild->SendRawMessage(aMeta, msgData, stackData))) { + aRv.ThrowOperationError( + nsPrintfCString("JSProcessActorChild send error in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; + } +} + +void JSProcessActorChild::Init(const nsACString& aName, + nsIDOMProcessChild* aManager) { + MOZ_ASSERT(!mManager, "Cannot Init() a JSProcessActorChild twice!"); + SetName(aName); + mManager = aManager; + + InvokeCallback(CallbackFunction::ActorCreated); +} + +void JSProcessActorChild::ClearManager() { mManager = nullptr; } + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSProcessActorChild.h b/dom/ipc/jsactor/JSProcessActorChild.h new file mode 100644 index 0000000000..a8be65211c --- /dev/null +++ b/dom/ipc/jsactor/JSProcessActorChild.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_JSProcessActorChild_h +#define mozilla_dom_JSProcessActorChild_h + +#include "mozilla/dom/JSActor.h" +#include "nsIDOMProcessChild.h" + +namespace mozilla::dom { + +// Placeholder implementation. +class JSProcessActorChild final : public JSActor { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(JSProcessActorChild, JSActor) + + explicit JSProcessActorChild(nsISupports* aGlobal = nullptr) + : JSActor(aGlobal) {} + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<JSProcessActorChild> Constructor( + GlobalObject& aGlobal) { + return MakeAndAddRef<JSProcessActorChild>(aGlobal.GetAsSupports()); + } + + nsIDOMProcessChild* Manager() const { return mManager; } + + void Init(const nsACString& aName, nsIDOMProcessChild* aManager); + void ClearManager() override; + + protected: + // Send the message described by the structured clone data |aData|, and the + // message metadata |aMetadata|. The underlying transport should call the + // |ReceiveMessage| method on the other side asynchronously. + virtual void SendRawMessage(const JSActorMessageMeta& aMetadata, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, + ErrorResult& aRv) override; + + private: + ~JSProcessActorChild() { MOZ_ASSERT(!mManager); } + + nsCOMPtr<nsIDOMProcessChild> mManager; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_JSProcessActorChild_h diff --git a/dom/ipc/jsactor/JSProcessActorParent.cpp b/dom/ipc/jsactor/JSProcessActorParent.cpp new file mode 100644 index 0000000000..eec8ad17c7 --- /dev/null +++ b/dom/ipc/jsactor/JSProcessActorParent.cpp @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/JSProcessActorBinding.h" +#include "mozilla/dom/JSProcessActorParent.h" +#include "mozilla/dom/InProcessChild.h" +#include "mozilla/dom/InProcessParent.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(JSProcessActorParent, JSActor, mManager) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSProcessActorParent) +NS_INTERFACE_MAP_END_INHERITING(JSActor) + +NS_IMPL_ADDREF_INHERITED(JSProcessActorParent, JSActor) +NS_IMPL_RELEASE_INHERITED(JSProcessActorParent, JSActor) + +JSObject* JSProcessActorParent::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return JSProcessActorParent_Binding::Wrap(aCx, this, aGivenProto); +} + +void JSProcessActorParent::Init(const nsACString& aName, + nsIDOMProcessParent* aManager) { + MOZ_ASSERT(!mManager, "Cannot Init() a JSProcessActorParent twice!"); + SetName(aName); + mManager = aManager; + + InvokeCallback(CallbackFunction::ActorCreated); +} + +JSProcessActorParent::~JSProcessActorParent() { MOZ_ASSERT(!mManager); } + +void JSProcessActorParent::SendRawMessage( + const JSActorMessageMeta& aMeta, Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, ErrorResult& aRv) { + if (NS_WARN_IF(!CanSend() || !mManager || !mManager->GetCanSend())) { + aRv.ThrowInvalidStateError( + nsPrintfCString("Actor '%s' cannot send message '%s' during shutdown.", + PromiseFlatCString(aMeta.actorName()).get(), + NS_ConvertUTF16toUTF8(aMeta.messageName()).get())); + return; + } + + // If the parent side is in the same process, we have a PInProcess manager, + // and can dispatch the message directly to the event loop. + ContentParent* contentParent = mManager->AsContentParent(); + if (!contentParent) { + SendRawMessageInProcess(aMeta, std::move(aData), std::move(aStack), []() { + return do_AddRef(InProcessChild::Singleton()); + }); + return; + } + + // Cross-process case - send data over ContentParent to other side. + Maybe<ClonedMessageData> msgData; + if (aData) { + msgData.emplace(); + if (NS_WARN_IF(!aData->BuildClonedMessageData(*msgData))) { + aRv.ThrowDataCloneError( + nsPrintfCString("Actor '%s' cannot send message '%s': cannot clone.", + PromiseFlatCString(aMeta.actorName()).get(), + NS_ConvertUTF16toUTF8(aMeta.messageName()).get())); + return; + } + } + + Maybe<ClonedMessageData> stackData; + if (aStack) { + stackData.emplace(); + if (!aStack->BuildClonedMessageData(*stackData)) { + stackData.reset(); + } + } + + if (NS_WARN_IF(!contentParent->SendRawMessage(aMeta, msgData, stackData))) { + aRv.ThrowOperationError( + nsPrintfCString("JSProcessActorParent send error in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; + } +} + +void JSProcessActorParent::ClearManager() { mManager = nullptr; } + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSProcessActorParent.h b/dom/ipc/jsactor/JSProcessActorParent.h new file mode 100644 index 0000000000..7b2a1535f9 --- /dev/null +++ b/dom/ipc/jsactor/JSProcessActorParent.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_JSProcessActorParent_h +#define mozilla_dom_JSProcessActorParent_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/JSActor.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIDOMProcessParent.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class JSProcessActorParent final : public JSActor { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(JSProcessActorParent, JSActor) + + explicit JSProcessActorParent(nsISupports* aGlobal = nullptr) + : JSActor(aGlobal) {} + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<JSProcessActorParent> Constructor( + GlobalObject& aGlobal) { + return MakeAndAddRef<JSProcessActorParent>(aGlobal.GetAsSupports()); + } + + nsIDOMProcessParent* Manager() const { return mManager; } + + void Init(const nsACString& aName, nsIDOMProcessParent* aManager); + void ClearManager() override; + + protected: + // Send the message described by the structured clone data |aData|, and the + // message metadata |aMetadata|. The underlying transport should call the + // |ReceiveMessage| method on the other side asynchronously. + virtual void SendRawMessage(const JSActorMessageMeta& aMetadata, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, + ErrorResult& aRv) override; + + private: + ~JSProcessActorParent(); + + nsCOMPtr<nsIDOMProcessParent> mManager; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_JSProcessActorParent_h diff --git a/dom/ipc/jsactor/JSProcessActorProtocol.cpp b/dom/ipc/jsactor/JSProcessActorProtocol.cpp new file mode 100644 index 0000000000..367db5305b --- /dev/null +++ b/dom/ipc/jsactor/JSProcessActorProtocol.cpp @@ -0,0 +1,147 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* 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 "mozilla/dom/JSProcessActorProtocol.h" +#include "mozilla/dom/InProcessChild.h" +#include "mozilla/dom/JSProcessActorBinding.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/JSActorBinding.h" +#include "mozilla/dom/PContent.h" + +#include "nsContentUtils.h" +#include "JSActorProtocolUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(JSProcessActorProtocol) +NS_IMPL_CYCLE_COLLECTING_RELEASE(JSProcessActorProtocol) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSProcessActorProtocol) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(JSProcessActorProtocol) + +/* static */ already_AddRefed<JSProcessActorProtocol> +JSProcessActorProtocol::FromIPC(const JSProcessActorInfo& aInfo) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess()); + + RefPtr<JSProcessActorProtocol> proto = + new JSProcessActorProtocol(aInfo.name()); + JSActorProtocolUtils::FromIPCShared(proto, aInfo); + + // Content processes aren't the parent process, so this flag is irrelevant and + // not propagated. + proto->mIncludeParent = false; + + return proto.forget(); +} + +JSProcessActorInfo JSProcessActorProtocol::ToIPC() { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + JSProcessActorInfo info; + JSActorProtocolUtils::ToIPCShared(info, this); + + return info; +} + +already_AddRefed<JSProcessActorProtocol> +JSProcessActorProtocol::FromWebIDLOptions(const nsACString& aName, + const ProcessActorOptions& aOptions, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + RefPtr<JSProcessActorProtocol> proto = new JSProcessActorProtocol(aName); + if (!JSActorProtocolUtils::FromWebIDLOptionsShared(proto, aOptions, aRv)) { + return nullptr; + } + + proto->mIncludeParent = aOptions.mIncludeParent; + + return proto.forget(); +} + +NS_IMETHODIMP JSProcessActorProtocol::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + RefPtr<JSActorManager> manager; + if (XRE_IsParentProcess()) { + manager = InProcessChild::Singleton(); + } else { + manager = ContentChild::GetSingleton(); + } + + // Ensure our actor is present. + AutoJSAPI jsapi; + jsapi.Init(); + RefPtr<JSActor> actor = manager->GetActor(jsapi.cx(), mName, IgnoreErrors()); + if (!actor || NS_WARN_IF(!actor->GetWrapperPreserveColor())) { + return NS_OK; + } + + // Build a observer callback. + JS::Rooted<JSObject*> global(jsapi.cx(), + JS::GetNonCCWObjectGlobal(actor->GetWrapper())); + RefPtr<MozObserverCallback> observerCallback = + new MozObserverCallback(actor->GetWrapper(), global, nullptr, nullptr); + observerCallback->Observe(aSubject, nsDependentCString(aTopic), + aData ? nsDependentString(aData) : VoidString()); + return NS_OK; +} + +void JSProcessActorProtocol::AddObservers() { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + for (auto& topic : mChild.mObservers) { + // This makes the observer service hold an owning reference to the + // JSProcessActorProtocol. The JSWindowActorProtocol objects will be living + // for the full lifetime of the content process, thus the extra strong + // referencec doesn't have a negative impact. + os->AddObserver(this, topic.get(), false); + } +} + +void JSProcessActorProtocol::RemoveObservers() { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + for (auto& topic : mChild.mObservers) { + os->RemoveObserver(this, topic.get()); + } +} + +bool JSProcessActorProtocol::RemoteTypePrefixMatches( + const nsDependentCSubstring& aRemoteType) { + for (auto& remoteType : mRemoteTypes) { + if (StringBeginsWith(aRemoteType, remoteType)) { + return true; + } + } + return false; +} + +bool JSProcessActorProtocol::Matches(const nsACString& aRemoteType, + ErrorResult& aRv) { + if (!mIncludeParent && aRemoteType.IsEmpty()) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Process protocol '%s' doesn't match the parent process", mName.get())); + return false; + } + + if (!mRemoteTypes.IsEmpty() && + !RemoteTypePrefixMatches(RemoteTypePrefix(aRemoteType))) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Process protocol '%s' doesn't support remote type '%s'", mName.get(), + PromiseFlatCString(aRemoteType).get())); + return false; + } + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSProcessActorProtocol.h b/dom/ipc/jsactor/JSProcessActorProtocol.h new file mode 100644 index 0000000000..eab8876c35 --- /dev/null +++ b/dom/ipc/jsactor/JSProcessActorProtocol.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_JSProcessActorProtocol_h +#define mozilla_dom_JSProcessActorProtocol_h + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/JSActorService.h" +#include "nsIURI.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIObserver.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +struct ProcessActorOptions; +class JSProcessActorInfo; +class EventTarget; +class JSActorProtocolUtils; + +/** + * Object corresponding to a single process actor protocol + * + * This object also can act as a carrier for methods and other state related to + * a single protocol managed by the JSActorService. + */ +class JSProcessActorProtocol final : public JSActorProtocol, + public nsIObserver { + public: + NS_DECL_NSIOBSERVER + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(JSProcessActorProtocol, nsIObserver) + + static already_AddRefed<JSProcessActorProtocol> FromIPC( + const JSProcessActorInfo& aInfo); + JSProcessActorInfo ToIPC(); + + static already_AddRefed<JSProcessActorProtocol> FromWebIDLOptions( + const nsACString& aName, const ProcessActorOptions& aOptions, + ErrorResult& aRv); + + struct ParentSide : public Sided {}; + + struct ChildSide : public Sided { + nsTArray<nsCString> mObservers; + }; + + const ParentSide& Parent() const override { return mParent; } + const ChildSide& Child() const override { return mChild; } + + void AddObservers(); + void RemoveObservers(); + bool Matches(const nsACString& aRemoteType, ErrorResult& aRv); + + private: + explicit JSProcessActorProtocol(const nsACString& aName) : mName(aName) {} + bool RemoteTypePrefixMatches(const nsDependentCSubstring& aRemoteType); + ~JSProcessActorProtocol() = default; + + nsCString mName; + nsTArray<nsCString> mRemoteTypes; + bool mIncludeParent = false; + + friend class JSActorProtocolUtils; + + ParentSide mParent; + ChildSide mChild; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_JSProcessActorProtocol_h diff --git a/dom/ipc/jsactor/JSWindowActorChild.cpp b/dom/ipc/jsactor/JSWindowActorChild.cpp new file mode 100644 index 0000000000..4d028a0789 --- /dev/null +++ b/dom/ipc/jsactor/JSWindowActorChild.cpp @@ -0,0 +1,160 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/JSWindowActorBinding.h" +#include "mozilla/dom/JSWindowActorChild.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "mozilla/dom/MessageManagerBinding.h" +#include "mozilla/dom/BrowsingContext.h" +#include "nsGlobalWindowInner.h" + +namespace mozilla::dom { + +JSWindowActorChild::~JSWindowActorChild() { MOZ_ASSERT(!mManager); } + +JSObject* JSWindowActorChild::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return JSWindowActorChild_Binding::Wrap(aCx, this, aGivenProto); +} + +WindowGlobalChild* JSWindowActorChild::GetManager() const { return mManager; } + +WindowContext* JSWindowActorChild::GetWindowContext() const { + return mManager ? mManager->WindowContext() : nullptr; +} + +void JSWindowActorChild::Init(const nsACString& aName, + WindowGlobalChild* aManager) { + MOZ_ASSERT(!mManager, "Cannot Init() a JSWindowActorChild twice!"); + SetName(aName); + mManager = aManager; + + InvokeCallback(CallbackFunction::ActorCreated); +} + +#ifdef DEBUG +# define DEBUG_WARN_MESSAGE_UNSENT(aMeta, aWarning) \ + NS_DebugBreak( \ + NS_DEBUG_WARNING, \ + nsPrintfCString( \ + "JSWindowActorChild::SendRawMessage (%s, %s) not sent: %s", \ + (aMeta).actorName().get(), \ + NS_LossyConvertUTF16toASCII((aMeta).messageName()).get(), \ + (aWarning)) \ + .get(), \ + nullptr, __FILE__, __LINE__) +#else +# define DEBUG_WARN_MESSAGE_UNSENT(aMeta, aWarning) +#endif + +void JSWindowActorChild::SendRawMessage( + const JSActorMessageMeta& aMeta, Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, ErrorResult& aRv) { + if (!CanSend() || !mManager || !mManager->CanSend()) { + DEBUG_WARN_MESSAGE_UNSENT( + aMeta, "!CanSend() || !mManager || !mManager->CanSend()"); + aRv.ThrowInvalidStateError("JSWindowActorChild cannot send at the moment"); + return; + } + + if (mManager->IsInProcess()) { + SendRawMessageInProcess( + aMeta, std::move(aData), std::move(aStack), + [manager{mManager}]() { return manager->GetParentActor(); }); + return; + } + + // Cross-process case - send data over WindowGlobalChild to other side. + Maybe<ClonedMessageData> msgData; + if (aData) { + msgData.emplace(); + if (!aData->BuildClonedMessageData(*msgData)) { + DEBUG_WARN_MESSAGE_UNSENT(aMeta, + "!aData->BuildClonedMessageData(*msgData)"); + aRv.ThrowDataCloneError( + nsPrintfCString("JSWindowActorChild serialization error: cannot " + "clone, in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; + } + } + + Maybe<ClonedMessageData> stackData; + if (aStack) { + stackData.emplace(); + if (!aStack->BuildClonedMessageData(*stackData)) { + stackData.reset(); + } + } + + if (!mManager->SendRawMessage(aMeta, msgData, stackData)) { + DEBUG_WARN_MESSAGE_UNSENT( + aMeta, "!mManager->SendRawMessage(aMeta, msgData, stackData)"); + aRv.ThrowOperationError( + nsPrintfCString("JSWindowActorChild send error in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; + } +} + +Document* JSWindowActorChild::GetDocument(ErrorResult& aRv) { + if (!mManager) { + ThrowStateErrorForGetter("document", aRv); + return nullptr; + } + + nsGlobalWindowInner* window = mManager->GetWindowGlobal(); + return window ? window->GetDocument() : nullptr; +} + +BrowsingContext* JSWindowActorChild::GetBrowsingContext(ErrorResult& aRv) { + if (!mManager) { + ThrowStateErrorForGetter("browsingContext", aRv); + return nullptr; + } + + return mManager->BrowsingContext(); +} + +nsIDocShell* JSWindowActorChild::GetDocShell(ErrorResult& aRv) { + if (!mManager) { + ThrowStateErrorForGetter("docShell", aRv); + return nullptr; + } + + return mManager->BrowsingContext()->GetDocShell(); +} + +Nullable<WindowProxyHolder> JSWindowActorChild::GetContentWindow( + ErrorResult& aRv) { + if (!mManager) { + ThrowStateErrorForGetter("contentWindow", aRv); + return nullptr; + } + + if (nsGlobalWindowInner* window = mManager->GetWindowGlobal()) { + if (window->IsCurrentInnerWindow()) { + return WindowProxyHolder(window->GetBrowsingContext()); + } + } + + return nullptr; +} + +void JSWindowActorChild::ClearManager() { mManager = nullptr; } + +NS_IMPL_CYCLE_COLLECTION_INHERITED(JSWindowActorChild, JSActor, mManager) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSWindowActorChild) +NS_INTERFACE_MAP_END_INHERITING(JSActor) + +NS_IMPL_ADDREF_INHERITED(JSWindowActorChild, JSActor) +NS_IMPL_RELEASE_INHERITED(JSWindowActorChild, JSActor) + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSWindowActorChild.h b/dom/ipc/jsactor/JSWindowActorChild.h new file mode 100644 index 0000000000..8e43d6a6c7 --- /dev/null +++ b/dom/ipc/jsactor/JSWindowActorChild.h @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_JSWindowActorChild_h +#define mozilla_dom_JSWindowActorChild_h + +#include "js/RootingAPI.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/JSActor.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIGlobalObject.h" +#include "nsISupports.h" +#include "nsStringFwd.h" + +class nsIDocShell; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +template <typename> +struct Nullable; + +class BrowsingContext; +class Document; +class WindowProxyHolder; + +} // namespace dom +} // namespace mozilla + +namespace mozilla::dom { + +class JSWindowActorChild final : public JSActor { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(JSWindowActorChild, JSActor) + + explicit JSWindowActorChild(nsISupports* aGlobal = nullptr) + : JSActor(aGlobal) {} + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<JSWindowActorChild> Constructor( + GlobalObject& aGlobal) { + return MakeAndAddRef<JSWindowActorChild>(aGlobal.GetAsSupports()); + } + + WindowGlobalChild* GetManager() const; + WindowContext* GetWindowContext() const; + void Init(const nsACString& aName, WindowGlobalChild* aManager); + void ClearManager() override; + Document* GetDocument(ErrorResult& aRv); + BrowsingContext* GetBrowsingContext(ErrorResult& aRv); + nsIDocShell* GetDocShell(ErrorResult& aRv); + Nullable<WindowProxyHolder> GetContentWindow(ErrorResult& aRv); + + protected: + void SendRawMessage(const JSActorMessageMeta& aMeta, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, + ErrorResult& aRv) override; + + private: + ~JSWindowActorChild(); + + RefPtr<WindowGlobalChild> mManager; + + nsCOMPtr<nsIGlobalObject> mGlobal; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_JSWindowActorChild_h diff --git a/dom/ipc/jsactor/JSWindowActorParent.cpp b/dom/ipc/jsactor/JSWindowActorParent.cpp new file mode 100644 index 0000000000..0bb82a9b8e --- /dev/null +++ b/dom/ipc/jsactor/JSWindowActorParent.cpp @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/JSWindowActorBinding.h" +#include "mozilla/dom/JSWindowActorParent.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/MessageManagerBinding.h" + +namespace mozilla::dom { + +JSWindowActorParent::~JSWindowActorParent() { MOZ_ASSERT(!mManager); } + +JSObject* JSWindowActorParent::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return JSWindowActorParent_Binding::Wrap(aCx, this, aGivenProto); +} + +WindowGlobalParent* JSWindowActorParent::GetManager() const { return mManager; } + +WindowContext* JSWindowActorParent::GetWindowContext() const { + return mManager; +} + +void JSWindowActorParent::Init(const nsACString& aName, + WindowGlobalParent* aManager) { + MOZ_ASSERT(!mManager, "Cannot Init() a JSWindowActorParent twice!"); + SetName(aName); + mManager = aManager; + + InvokeCallback(CallbackFunction::ActorCreated); +} + +void JSWindowActorParent::SendRawMessage( + const JSActorMessageMeta& aMeta, Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, ErrorResult& aRv) { + if (NS_WARN_IF(!CanSend() || !mManager || !mManager->CanSend())) { + aRv.ThrowInvalidStateError("JSWindowActorParent cannot send at the moment"); + return; + } + + if (mManager->IsInProcess()) { + SendRawMessageInProcess( + aMeta, std::move(aData), std::move(aStack), + [manager{mManager}]() { return manager->GetChildActor(); }); + return; + } + + Maybe<ClonedMessageData> msgData; + if (aData) { + msgData.emplace(); + if (NS_WARN_IF(!aData->BuildClonedMessageData(*msgData))) { + aRv.ThrowDataCloneError( + nsPrintfCString("JSWindowActorParent serialization error: cannot " + "clone, in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; + } + } + + Maybe<ClonedMessageData> stackData; + if (aStack) { + stackData.emplace(); + if (!aStack->BuildClonedMessageData(*stackData)) { + stackData.reset(); + } + } + + if (NS_WARN_IF(!mManager->SendRawMessage(aMeta, msgData, stackData))) { + aRv.ThrowOperationError( + nsPrintfCString("JSWindowActorParent send error in actor '%s'", + PromiseFlatCString(aMeta.actorName()).get())); + return; + } +} + +CanonicalBrowsingContext* JSWindowActorParent::GetBrowsingContext( + ErrorResult& aRv) { + if (!mManager) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + return mManager->BrowsingContext(); +} + +void JSWindowActorParent::ClearManager() { mManager = nullptr; } + +NS_IMPL_CYCLE_COLLECTION_INHERITED(JSWindowActorParent, JSActor, mManager) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSWindowActorParent) +NS_INTERFACE_MAP_END_INHERITING(JSActor) + +NS_IMPL_ADDREF_INHERITED(JSWindowActorParent, JSActor) +NS_IMPL_RELEASE_INHERITED(JSWindowActorParent, JSActor) + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSWindowActorParent.h b/dom/ipc/jsactor/JSWindowActorParent.h new file mode 100644 index 0000000000..05fb88f8ac --- /dev/null +++ b/dom/ipc/jsactor/JSWindowActorParent.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef mozilla_dom_JSWindowActorParent_h +#define mozilla_dom_JSWindowActorParent_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/JSActor.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class WindowGlobalParent; + +} // namespace dom +} // namespace mozilla + +namespace mozilla::dom { + +class JSWindowActorParent final : public JSActor { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(JSWindowActorParent, JSActor) + + explicit JSWindowActorParent(nsISupports* aGlobal = nullptr) + : JSActor(aGlobal) {} + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<JSWindowActorParent> Constructor( + GlobalObject& aGlobal) { + return MakeAndAddRef<JSWindowActorParent>(aGlobal.GetAsSupports()); + } + + WindowGlobalParent* GetManager() const; + WindowContext* GetWindowContext() const; + void Init(const nsACString& aName, WindowGlobalParent* aManager); + void ClearManager() override; + CanonicalBrowsingContext* GetBrowsingContext(ErrorResult& aRv); + + protected: + void SendRawMessage(const JSActorMessageMeta& aMeta, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, + ErrorResult& aRv) override; + + private: + ~JSWindowActorParent(); + + RefPtr<WindowGlobalParent> mManager; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_JSWindowActorParent_h diff --git a/dom/ipc/jsactor/JSWindowActorProtocol.cpp b/dom/ipc/jsactor/JSWindowActorProtocol.cpp new file mode 100644 index 0000000000..a459d94204 --- /dev/null +++ b/dom/ipc/jsactor/JSWindowActorProtocol.cpp @@ -0,0 +1,380 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* 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 "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/JSActorBinding.h" +#include "mozilla/dom/JSActorService.h" +#include "mozilla/dom/JSWindowActorBinding.h" +#include "mozilla/dom/JSWindowActorChild.h" +#include "mozilla/dom/JSWindowActorProtocol.h" +#include "mozilla/dom/PContent.h" +#include "mozilla/dom/WindowGlobalChild.h" + +#include "mozilla/extensions/MatchPattern.h" +#include "nsContentUtils.h" +#include "JSActorProtocolUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(JSWindowActorProtocol) +NS_IMPL_CYCLE_COLLECTING_RELEASE(JSWindowActorProtocol) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSWindowActorProtocol) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(JSWindowActorProtocol) + +/* static */ already_AddRefed<JSWindowActorProtocol> +JSWindowActorProtocol::FromIPC(const JSWindowActorInfo& aInfo) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess()); + + RefPtr<JSWindowActorProtocol> proto = new JSWindowActorProtocol(aInfo.name()); + JSActorProtocolUtils::FromIPCShared(proto, aInfo); + + // Content processes cannot load chrome browsing contexts, so this flag is + // irrelevant and not propagated. + proto->mIncludeChrome = false; + proto->mAllFrames = aInfo.allFrames(); + proto->mMatches = aInfo.matches().Clone(); + proto->mMessageManagerGroups = aInfo.messageManagerGroups().Clone(); + + proto->mChild.mEvents.SetCapacity(aInfo.events().Length()); + for (auto& ipc : aInfo.events()) { + auto event = proto->mChild.mEvents.AppendElement(); + event->mName.Assign(ipc.name()); + event->mFlags.mCapture = ipc.capture(); + event->mFlags.mInSystemGroup = ipc.systemGroup(); + event->mFlags.mAllowUntrustedEvents = ipc.allowUntrusted(); + if (ipc.passive()) { + event->mPassive.Construct(ipc.passive().value()); + } + event->mCreateActor = ipc.createActor(); + } + + return proto.forget(); +} + +JSWindowActorInfo JSWindowActorProtocol::ToIPC() { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + JSWindowActorInfo info; + JSActorProtocolUtils::ToIPCShared(info, this); + + info.allFrames() = mAllFrames; + info.matches() = mMatches.Clone(); + info.messageManagerGroups() = mMessageManagerGroups.Clone(); + + info.events().SetCapacity(mChild.mEvents.Length()); + for (auto& event : mChild.mEvents) { + auto ipc = info.events().AppendElement(); + ipc->name().Assign(event.mName); + ipc->capture() = event.mFlags.mCapture; + ipc->systemGroup() = event.mFlags.mInSystemGroup; + ipc->allowUntrusted() = event.mFlags.mAllowUntrustedEvents; + if (event.mPassive.WasPassed()) { + ipc->passive() = Some(event.mPassive.Value()); + } + ipc->createActor() = event.mCreateActor; + } + + return info; +} + +already_AddRefed<JSWindowActorProtocol> +JSWindowActorProtocol::FromWebIDLOptions(const nsACString& aName, + const WindowActorOptions& aOptions, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + RefPtr<JSWindowActorProtocol> proto = new JSWindowActorProtocol(aName); + if (!JSActorProtocolUtils::FromWebIDLOptionsShared(proto, aOptions, aRv)) { + return nullptr; + } + + proto->mAllFrames = aOptions.mAllFrames; + proto->mIncludeChrome = aOptions.mIncludeChrome; + + if (aOptions.mMatches.WasPassed()) { + MOZ_ASSERT(aOptions.mMatches.Value().Length()); + proto->mMatches = aOptions.mMatches.Value(); + } + + if (aOptions.mMessageManagerGroups.WasPassed()) { + proto->mMessageManagerGroups = aOptions.mMessageManagerGroups.Value(); + } + + // For each event declared in the source dictionary, initialize the + // corresponding event declaration entry in the JSWindowActorProtocol. + if (aOptions.mChild.WasPassed() && + aOptions.mChild.Value().mEvents.WasPassed()) { + auto& entries = aOptions.mChild.Value().mEvents.Value().Entries(); + proto->mChild.mEvents.SetCapacity(entries.Length()); + + for (auto& entry : entries) { + // We don't support the mOnce field, as it doesn't work well in this + // environment. For now, throw an error in that case. + if (entry.mValue.mOnce) { + aRv.ThrowNotSupportedError("mOnce is not supported"); + return nullptr; + } + + // Add the EventDecl to our list of events. + EventDecl* evt = proto->mChild.mEvents.AppendElement(); + evt->mName = entry.mKey; + evt->mFlags.mCapture = entry.mValue.mCapture; + evt->mFlags.mInSystemGroup = entry.mValue.mMozSystemGroup; + evt->mFlags.mAllowUntrustedEvents = + entry.mValue.mWantUntrusted.WasPassed() + ? entry.mValue.mWantUntrusted.Value() + : false; + if (entry.mValue.mPassive.WasPassed()) { + evt->mPassive.Construct(entry.mValue.mPassive.Value()); + } + evt->mCreateActor = entry.mValue.mCreateActor; + } + } + + return proto.forget(); +} + +/** + * This listener only listens for events for the child side of the protocol. + * This will work in both content and parent processes. + */ +NS_IMETHODIMP JSWindowActorProtocol::HandleEvent(Event* aEvent) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + // Determine which inner window we're associated with, and get its + // WindowGlobalChild actor. + EventTarget* target = aEvent->GetOriginalTarget(); + if (NS_WARN_IF(!target)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsPIDOMWindowInner> inner = + do_QueryInterface(target->GetOwnerGlobal()); + if (NS_WARN_IF(!inner)) { + return NS_ERROR_FAILURE; + } + + RefPtr<WindowGlobalChild> wgc = inner->GetWindowGlobalChild(); + if (NS_WARN_IF(!wgc)) { + return NS_ERROR_FAILURE; + } + + if (aEvent->ShouldIgnoreChromeEventTargetListener()) { + return NS_OK; + } + + // Ensure our actor is present. + RefPtr<JSActor> actor = wgc->GetExistingActor(mName); + if (!actor) { + // Check if we're supposed to create the actor when this event is fired. + bool createActor = true; + nsAutoString typeStr; + aEvent->GetType(typeStr); + for (auto& event : mChild.mEvents) { + if (event.mName == typeStr) { + createActor = event.mCreateActor; + break; + } + } + + // If we're supposed to create the actor, call GetActor to cause it to be + // created. + if (createActor) { + AutoJSAPI jsapi; + jsapi.Init(); + actor = wgc->GetActor(jsapi.cx(), mName, IgnoreErrors()); + } + } + if (!actor || NS_WARN_IF(!actor->GetWrapperPreserveColor())) { + return NS_OK; + } + + // Build our event listener & call it. + JS::Rooted<JSObject*> global(RootingCx(), + JS::GetNonCCWObjectGlobal(actor->GetWrapper())); + RefPtr<EventListener> eventListener = + new EventListener(actor->GetWrapper(), global, nullptr, nullptr); + eventListener->HandleEvent(*aEvent, "JSWindowActorProtocol::HandleEvent"); + return NS_OK; +} + +NS_IMETHODIMP JSWindowActorProtocol::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + nsCOMPtr<nsPIDOMWindowInner> inner = do_QueryInterface(aSubject); + RefPtr<WindowGlobalChild> wgc; + + if (!inner) { + nsCOMPtr<nsPIDOMWindowOuter> outer = do_QueryInterface(aSubject); + if (NS_WARN_IF(!outer)) { + nsContentUtils::LogSimpleConsoleError( + NS_ConvertUTF8toUTF16(nsPrintfCString( + "JSWindowActor %s: expected window subject for topic '%s'.", + mName.get(), aTopic)), + "JSActor"_ns, + /* aFromPrivateWindow */ false, + /* aFromChromeContext */ true); + return NS_ERROR_FAILURE; + } + if (NS_WARN_IF(!outer->GetCurrentInnerWindow())) { + return NS_ERROR_FAILURE; + } + wgc = outer->GetCurrentInnerWindow()->GetWindowGlobalChild(); + } else { + wgc = inner->GetWindowGlobalChild(); + } + + if (NS_WARN_IF(!wgc)) { + return NS_ERROR_FAILURE; + } + + // Ensure our actor is present. + AutoJSAPI jsapi; + jsapi.Init(); + RefPtr<JSActor> actor = wgc->GetActor(jsapi.cx(), mName, IgnoreErrors()); + if (!actor || NS_WARN_IF(!actor->GetWrapperPreserveColor())) { + return NS_OK; + } + + // Build a observer callback. + JS::Rooted<JSObject*> global(jsapi.cx(), + JS::GetNonCCWObjectGlobal(actor->GetWrapper())); + RefPtr<MozObserverCallback> observerCallback = + new MozObserverCallback(actor->GetWrapper(), global, nullptr, nullptr); + observerCallback->Observe(aSubject, nsDependentCString(aTopic), + aData ? nsDependentString(aData) : VoidString()); + return NS_OK; +} + +void JSWindowActorProtocol::RegisterListenersFor(EventTarget* aTarget) { + EventListenerManager* elm = aTarget->GetOrCreateListenerManager(); + + for (auto& event : mChild.mEvents) { + elm->AddEventListenerByType(EventListenerHolder(this), event.mName, + event.mFlags, event.mPassive); + } +} + +void JSWindowActorProtocol::UnregisterListenersFor(EventTarget* aTarget) { + EventListenerManager* elm = aTarget->GetOrCreateListenerManager(); + + for (auto& event : mChild.mEvents) { + elm->RemoveEventListenerByType(EventListenerHolder(this), event.mName, + event.mFlags); + } +} + +void JSWindowActorProtocol::AddObservers() { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + for (auto& topic : mChild.mObservers) { + // This makes the observer service hold an owning reference to the + // JSWindowActorProtocol. The JSWindowActorProtocol objects will be living + // for the full lifetime of the content process, thus the extra strong + // referencec doesn't have a negative impact. + os->AddObserver(this, topic.get(), false); + } +} + +void JSWindowActorProtocol::RemoveObservers() { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + for (auto& topic : mChild.mObservers) { + os->RemoveObserver(this, topic.get()); + } +} + +extensions::MatchPatternSetCore* JSWindowActorProtocol::GetURIMatcher() { + // If we've already created the pattern set, return it. + if (mURIMatcher || mMatches.IsEmpty()) { + return mURIMatcher; + } + + nsTArray<RefPtr<extensions::MatchPatternCore>> patterns(mMatches.Length()); + for (const nsString& pattern : mMatches) { + patterns.AppendElement(new extensions::MatchPatternCore( + pattern, false, false, IgnoreErrors())); + } + mURIMatcher = new extensions::MatchPatternSetCore(std::move(patterns)); + return mURIMatcher; +} + +bool JSWindowActorProtocol::RemoteTypePrefixMatches( + const nsDependentCSubstring& aRemoteType) { + for (auto& remoteType : mRemoteTypes) { + if (StringBeginsWith(aRemoteType, remoteType)) { + return true; + } + } + return false; +} + +bool JSWindowActorProtocol::MessageManagerGroupMatches( + BrowsingContext* aBrowsingContext) { + BrowsingContext* top = aBrowsingContext->Top(); + for (auto& group : mMessageManagerGroups) { + if (group == top->GetMessageManagerGroup()) { + return true; + } + } + return false; +} + +bool JSWindowActorProtocol::Matches(BrowsingContext* aBrowsingContext, + nsIURI* aURI, const nsACString& aRemoteType, + ErrorResult& aRv) { + MOZ_ASSERT(aBrowsingContext, "DocShell without a BrowsingContext!"); + MOZ_ASSERT(aURI, "Must have URI!"); + + if (!mAllFrames && aBrowsingContext->GetParent()) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Window protocol '%s' doesn't match subframes", mName.get())); + return false; + } + + if (!mIncludeChrome && !aBrowsingContext->IsContent()) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Window protocol '%s' doesn't match chrome browsing contexts", + mName.get())); + return false; + } + + if (!mRemoteTypes.IsEmpty() && + !RemoteTypePrefixMatches(RemoteTypePrefix(aRemoteType))) { + aRv.ThrowNotSupportedError( + nsPrintfCString("Window protocol '%s' doesn't match remote type '%s'", + mName.get(), PromiseFlatCString(aRemoteType).get())); + return false; + } + + if (!mMessageManagerGroups.IsEmpty() && + !MessageManagerGroupMatches(aBrowsingContext)) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Window protocol '%s' doesn't match message manager group", + mName.get())); + return false; + } + + if (extensions::MatchPatternSetCore* uriMatcher = GetURIMatcher()) { + if (!uriMatcher->Matches(aURI)) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Window protocol '%s' doesn't match uri %s", mName.get(), + nsContentUtils::TruncatedURLForDisplay(aURI).get())); + return false; + } + } + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/ipc/jsactor/JSWindowActorProtocol.h b/dom/ipc/jsactor/JSWindowActorProtocol.h new file mode 100644 index 0000000000..b0128d9307 --- /dev/null +++ b/dom/ipc/jsactor/JSWindowActorProtocol.h @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_dom_JSWindowActorProtocol_h +#define mozilla_dom_JSWindowActorProtocol_h + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/JSActorService.h" +#include "mozilla/extensions/MatchPattern.h" +#include "nsIURI.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIObserver.h" +#include "nsIDOMEventListener.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +struct WindowActorOptions; +class JSWindowActorInfo; +class EventTarget; +class JSActorProtocolUtils; + +/** + * Object corresponding to a single window actor protocol. This object acts as + * an Event listener for the actor which is called for events which would + * trigger actor creation. + * + * This object also can act as a carrier for methods and other state related to + * a single protocol managed by the JSActorService. + */ +class JSWindowActorProtocol final : public JSActorProtocol, + public nsIObserver, + public nsIDOMEventListener { + public: + NS_DECL_NSIOBSERVER + NS_DECL_NSIDOMEVENTLISTENER + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(JSWindowActorProtocol, nsIObserver) + + static already_AddRefed<JSWindowActorProtocol> FromIPC( + const JSWindowActorInfo& aInfo); + JSWindowActorInfo ToIPC(); + + static already_AddRefed<JSWindowActorProtocol> FromWebIDLOptions( + const nsACString& aName, const WindowActorOptions& aOptions, + ErrorResult& aRv); + + struct ParentSide : public Sided {}; + + struct EventDecl { + nsString mName; + EventListenerFlags mFlags; + Optional<bool> mPassive; + bool mCreateActor = true; + }; + + struct ChildSide : public Sided { + nsTArray<EventDecl> mEvents; + nsTArray<nsCString> mObservers; + }; + + const ParentSide& Parent() const override { return mParent; } + const ChildSide& Child() const override { return mChild; } + + void RegisterListenersFor(EventTarget* aTarget); + void UnregisterListenersFor(EventTarget* aTarget); + void AddObservers(); + void RemoveObservers(); + bool Matches(BrowsingContext* aBrowsingContext, nsIURI* aURI, + const nsACString& aRemoteType, ErrorResult& aRv); + + private: + explicit JSWindowActorProtocol(const nsACString& aName) : mName(aName) {} + extensions::MatchPatternSetCore* GetURIMatcher(); + bool RemoteTypePrefixMatches(const nsDependentCSubstring& aRemoteType); + bool MessageManagerGroupMatches(BrowsingContext* aBrowsingContext); + ~JSWindowActorProtocol() = default; + + nsCString mName; + bool mAllFrames = false; + bool mIncludeChrome = false; + nsTArray<nsString> mMatches; + nsTArray<nsCString> mRemoteTypes; + nsTArray<nsString> mMessageManagerGroups; + + friend class JSActorProtocolUtils; + + ParentSide mParent; + ChildSide mChild; + + RefPtr<extensions::MatchPatternSetCore> mURIMatcher; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_JSWindowActorProtocol_h diff --git a/dom/ipc/jsactor/moz.build b/dom/ipc/jsactor/moz.build new file mode 100644 index 0000000000..6bc66df923 --- /dev/null +++ b/dom/ipc/jsactor/moz.build @@ -0,0 +1,42 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.mozilla.dom += [ + "JSActor.h", + "JSActorManager.h", + "JSActorService.h", + "JSProcessActorChild.h", + "JSProcessActorParent.h", + "JSProcessActorProtocol.h", + "JSWindowActorChild.h", + "JSWindowActorParent.h", + "JSWindowActorProtocol.h", +] + +EXPORTS += [ + "nsQueryActor.h", +] + +UNIFIED_SOURCES += [ + "JSActor.cpp", + "JSActorManager.cpp", + "JSActorService.cpp", + "JSProcessActorChild.cpp", + "JSProcessActorParent.cpp", + "JSProcessActorProtocol.cpp", + "JSWindowActorChild.cpp", + "JSWindowActorParent.cpp", + "JSWindowActorProtocol.cpp", +] + +LOCAL_INCLUDES += [ + "/js/xpconnect/loader", + "/js/xpconnect/src", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/ipc/jsactor/nsQueryActor.h b/dom/ipc/jsactor/nsQueryActor.h new file mode 100644 index 0000000000..cbbda7e473 --- /dev/null +++ b/dom/ipc/jsactor/nsQueryActor.h @@ -0,0 +1,90 @@ +/* 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/. */ + +#ifndef nsQueryActor_h +#define nsQueryActor_h + +#include <type_traits> + +#include "nsCOMPtr.h" +#include "nsPIDOMWindow.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/JSActor.h" +#include "mozilla/dom/JSActorManager.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/WindowGlobalChild.h" + +class nsIDOMProcessChild; +class nsIDOMProcessParent; + +// This type is used to get an XPCOM interface implemented by a JSActor from its +// native manager. +class MOZ_STACK_CLASS nsQueryJSActor final : public nsCOMPtr_helper { + public: + nsQueryJSActor(const nsLiteralCString aActorName, + mozilla::dom::JSActorManager* aManager) + : mActorName(aActorName), mManager(aManager) {} + + nsresult NS_FASTCALL operator()(const nsIID& aIID, + void** aResult) const override { + if (!mManager) { + return NS_ERROR_NO_INTERFACE; + } + + mozilla::dom::AutoJSAPI jsapi; + jsapi.Init(); + + RefPtr<mozilla::dom::JSActor> actor = + mManager->GetActor(jsapi.cx(), mActorName, mozilla::IgnoreErrors()); + if (!actor) { + return NS_ERROR_NO_INTERFACE; + } + + return actor->QueryInterfaceActor(aIID, aResult); + } + + private: + const nsLiteralCString mActorName; + mozilla::dom::JSActorManager* mManager; +}; + +// Request an XPCOM interface from a JSActor managed by `aManager`. +// +// These APIs will work with both JSWindowActors and JSProcessActors. +template <size_t length> +inline nsQueryJSActor do_QueryActor(const char (&aActorName)[length], + mozilla::dom::JSActorManager* aManager) { + return nsQueryJSActor(nsLiteralCString(aActorName), aManager); +} + +// Overload for directly querying a JSWindowActorChild from an inner window. +template <size_t length> +inline nsQueryJSActor do_QueryActor(const char (&aActorName)[length], + nsPIDOMWindowInner* aWindow) { + return nsQueryJSActor(nsLiteralCString(aActorName), + aWindow ? aWindow->GetWindowGlobalChild() : nullptr); +} + +// Overload for directly querying a JSWindowActorChild from a document. +template <size_t length> +inline nsQueryJSActor do_QueryActor(const char (&aActorName)[length], + mozilla::dom::Document* aDoc) { + return nsQueryJSActor(nsLiteralCString(aActorName), + aDoc ? aDoc->GetWindowGlobalChild() : nullptr); +} + +// Overload for directly querying from a nsIDOMProcess{Parent,Child} without +// confusing overload selection for types inheriting from both +// nsIDOMProcess{Parent,Child} and JSActorManager. +template <size_t length, typename T, + typename = std::enable_if_t<std::is_same_v<T, nsIDOMProcessParent> || + std::is_same_v<T, nsIDOMProcessChild>>> +inline nsQueryJSActor do_QueryActor(const char (&aActorName)[length], + T* aManager) { + return nsQueryJSActor(nsLiteralCString(aActorName), + aManager ? aManager->AsJSActorManager() : nullptr); +} + +#endif // defined nsQueryActor_h |