diff options
Diffstat (limited to 'dom/broadcastchannel')
35 files changed, 1999 insertions, 0 deletions
diff --git a/dom/broadcastchannel/BroadcastChannel.cpp b/dom/broadcastchannel/BroadcastChannel.cpp new file mode 100644 index 0000000000..c89231badf --- /dev/null +++ b/dom/broadcastchannel/BroadcastChannel.cpp @@ -0,0 +1,466 @@ +/* -*- 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 "BroadcastChannel.h" +#include "BroadcastChannelChild.h" +#include "mozilla/dom/BroadcastChannelBinding.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/ipc/StructuredCloneData.h" +#include "mozilla/dom/RefMessageBodyService.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/SharedMessageBody.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/StorageAccess.h" + +#include "nsICookieJarSettings.h" +#include "mozilla/dom/Document.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +using namespace ipc; + +namespace { + +class CloseRunnable final : public DiscardableRunnable { + public: + explicit CloseRunnable(BroadcastChannel* aBC) + : DiscardableRunnable("BroadcastChannel CloseRunnable"), mBC(aBC) { + MOZ_ASSERT(mBC); + } + + NS_IMETHOD Run() override { + mBC->Shutdown(); + return NS_OK; + } + + private: + ~CloseRunnable() = default; + + RefPtr<BroadcastChannel> mBC; +}; + +class TeardownRunnable { + protected: + explicit TeardownRunnable(BroadcastChannelChild* aActor) : mActor(aActor) { + MOZ_ASSERT(mActor); + } + + void RunInternal() { + MOZ_ASSERT(mActor); + if (!mActor->IsActorDestroyed()) { + mActor->SendClose(); + } + } + + protected: + virtual ~TeardownRunnable() = default; + + private: + RefPtr<BroadcastChannelChild> mActor; +}; + +class TeardownRunnableOnMainThread final : public Runnable, + public TeardownRunnable { + public: + explicit TeardownRunnableOnMainThread(BroadcastChannelChild* aActor) + : Runnable("TeardownRunnableOnMainThread"), TeardownRunnable(aActor) {} + + NS_IMETHOD Run() override { + RunInternal(); + return NS_OK; + } +}; + +class TeardownRunnableOnWorker final : public WorkerControlRunnable, + public TeardownRunnable { + public: + TeardownRunnableOnWorker(WorkerPrivate* aWorkerPrivate, + BroadcastChannelChild* aActor) + : WorkerControlRunnable(aWorkerPrivate, "TeardownRunnableOnWorker", + WorkerThread), + TeardownRunnable(aActor) {} + + bool WorkerRun(JSContext*, WorkerPrivate*) override { + RunInternal(); + return true; + } + + bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { return true; } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override {} + + bool PreRun(WorkerPrivate* aWorkerPrivate) override { return true; } + + void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + bool aRunResult) override {} +}; + +} // namespace + +BroadcastChannel::BroadcastChannel(nsIGlobalObject* aGlobal, + const nsAString& aChannel, + const nsID& aPortUUID) + : DOMEventTargetHelper(aGlobal), + mRefMessageBodyService(RefMessageBodyService::GetOrCreate()), + mChannel(aChannel), + mState(StateActive), + mPortUUID(aPortUUID) { + MOZ_ASSERT(aGlobal); + KeepAliveIfHasListenersFor(nsGkAtoms::onmessage); +} + +BroadcastChannel::~BroadcastChannel() { + Shutdown(); + MOZ_ASSERT(!mWorkerRef); +} + +JSObject* BroadcastChannel::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return BroadcastChannel_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +already_AddRefed<BroadcastChannel> BroadcastChannel::Constructor( + const GlobalObject& aGlobal, const nsAString& aChannel, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsID portUUID = {}; + aRv = nsID::GenerateUUIDInPlace(portUUID); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<BroadcastChannel> bc = + new BroadcastChannel(global, aChannel, portUUID); + + nsCOMPtr<nsIPrincipal> storagePrincipal; + + StorageAccess storageAccess; + + nsCOMPtr<nsICookieJarSettings> cjs; + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> incumbent = mozilla::dom::GetIncumbentGlobal(); + + if (!incumbent) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(incumbent); + if (NS_WARN_IF(!sop)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + storagePrincipal = sop->GetEffectiveStoragePrincipal(); + if (!storagePrincipal) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + storageAccess = StorageAllowedForWindow(window); + + Document* doc = window->GetExtantDoc(); + if (doc) { + cjs = doc->CookieJarSettings(); + } + } else { + JSContext* cx = aGlobal.Context(); + + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); + MOZ_ASSERT(workerPrivate); + + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + workerPrivate, "BroadcastChannel", [bc]() { bc->Shutdown(); }); + // We are already shutting down the worker. Let's return a non-active + // object. + if (NS_WARN_IF(!workerRef)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + storageAccess = workerPrivate->StorageAccess(); + + storagePrincipal = workerPrivate->GetEffectiveStoragePrincipal(); + + bc->mWorkerRef = workerRef; + + cjs = workerPrivate->CookieJarSettings(); + } + + // We want to allow opaque origins. + if (!storagePrincipal->GetIsNullPrincipal() && + (storageAccess == StorageAccess::eDeny || + (ShouldPartitionStorage(storageAccess) && + !StoragePartitioningEnabled(storageAccess, cjs)))) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + // Register this component to PBackground. + PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actorChild)) { + // Firefox is probably shutting down. Let's return a 'generic' error. + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsAutoCString origin; + aRv = storagePrincipal->GetOrigin(origin); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsString originForEvents; + aRv = nsContentUtils::GetWebExposedOriginSerialization(storagePrincipal, + originForEvents); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + PrincipalInfo storagePrincipalInfo; + aRv = PrincipalToPrincipalInfo(storagePrincipal, &storagePrincipalInfo); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + PBroadcastChannelChild* actor = actorChild->SendPBroadcastChannelConstructor( + storagePrincipalInfo, origin, nsString(aChannel)); + if (!actor) { + // The PBackground actor is shutting down, return a 'generic' error. + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + bc->mActor = static_cast<BroadcastChannelChild*>(actor); + bc->mActor->SetParent(bc); + bc->mOriginForEvents = std::move(originForEvents); + + return bc.forget(); +} + +void BroadcastChannel::PostMessage(JSContext* aCx, + JS::Handle<JS::Value> aMessage, + ErrorResult& aRv) { + if (mState != StateActive) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + Maybe<nsID> agentClusterId; + nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); + MOZ_ASSERT(global); + if (global) { + agentClusterId = global->GetAgentClusterId(); + } + + if (!global->IsEligibleForMessaging()) { + return; + } + + RefPtr<SharedMessageBody> data = new SharedMessageBody( + StructuredCloneHolder::TransferringNotSupported, agentClusterId); + + data->Write(aCx, aMessage, JS::UndefinedHandleValue, mPortUUID, + mRefMessageBodyService, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + RemoveDocFromBFCache(); + + MessageData message; + SharedMessageBody::FromSharedToMessageChild(mActor->Manager(), data, message); + mActor->SendPostMessage(message); +} + +void BroadcastChannel::Close() { + if (mState != StateActive) { + return; + } + + // We cannot call Shutdown() immediatelly because we could have some + // postMessage runnable already dispatched. Instead, we change the state to + // StateClosed and we shutdown the actor asynchrounsly. + + mState = StateClosed; + RefPtr<CloseRunnable> runnable = new CloseRunnable(this); + + if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) { + NS_WARNING("Failed to dispatch to the current thread!"); + } +} + +void BroadcastChannel::Shutdown() { + mState = StateClosed; + + // The DTOR of this WorkerRef will release the worker for us. + mWorkerRef = nullptr; + + if (mActor) { + mActor->SetParent(nullptr); + + if (NS_IsMainThread()) { + RefPtr<TeardownRunnableOnMainThread> runnable = + new TeardownRunnableOnMainThread(mActor); + NS_DispatchToCurrentThread(runnable); + } else { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<TeardownRunnableOnWorker> runnable = + new TeardownRunnableOnWorker(workerPrivate, mActor); + runnable->Dispatch(); + } + + mActor = nullptr; + } + + IgnoreKeepAliveIfHasListenersFor(nsGkAtoms::onmessage); +} + +void BroadcastChannel::RemoveDocFromBFCache() { + if (!NS_IsMainThread()) { + return; + } + + if (nsPIDOMWindowInner* window = GetOwner()) { + window->RemoveFromBFCacheSync(); + } +} + +void BroadcastChannel::DisconnectFromOwner() { + Shutdown(); + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void BroadcastChannel::MessageReceived(const MessageData& aData) { + if (NS_FAILED(CheckCurrentGlobalCorrectness())) { + RemoveDocFromBFCache(); + return; + } + + // Let's ignore messages after a close/shutdown. + if (mState != StateActive) { + return; + } + + nsCOMPtr<nsIGlobalObject> globalObject; + + if (NS_IsMainThread()) { + globalObject = GetParentObject(); + } else { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + globalObject = workerPrivate->GlobalScope(); + } + + AutoJSAPI jsapi; + if (!globalObject || !jsapi.Init(globalObject)) { + NS_WARNING("Failed to initialize AutoJSAPI object."); + return; + } + + JSContext* cx = jsapi.cx(); + + RefPtr<SharedMessageBody> data = SharedMessageBody::FromMessageToSharedChild( + aData, StructuredCloneHolder::TransferringNotSupported); + if (NS_WARN_IF(!data)) { + DispatchError(cx); + return; + } + + IgnoredErrorResult rv; + JS::Rooted<JS::Value> value(cx); + + data->Read(cx, &value, mRefMessageBodyService, + SharedMessageBody::ReadMethod::KeepRefMessageBody, rv); + if (NS_WARN_IF(rv.Failed())) { + JS_ClearPendingException(cx); + DispatchError(cx); + return; + } + + RemoveDocFromBFCache(); + + RootedDictionary<MessageEventInit> init(cx); + init.mBubbles = false; + init.mCancelable = false; + init.mOrigin = mOriginForEvents; + init.mData = value; + + RefPtr<MessageEvent> event = + MessageEvent::Constructor(this, u"message"_ns, init); + + event->SetTrusted(true); + + DispatchEvent(*event); +} + +void BroadcastChannel::MessageDelivered(const nsID& aMessageID, + uint32_t aOtherBCs) { + mRefMessageBodyService->SetMaxCount(aMessageID, aOtherBCs); +} + +void BroadcastChannel::DispatchError(JSContext* aCx) { + RootedDictionary<MessageEventInit> init(aCx); + init.mBubbles = false; + init.mCancelable = false; + init.mOrigin = mOriginForEvents; + + RefPtr<Event> event = + MessageEvent::Constructor(this, u"messageerror"_ns, init); + event->SetTrusted(true); + + DispatchEvent(*event); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(BroadcastChannel) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(BroadcastChannel, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(BroadcastChannel, + DOMEventTargetHelper) + tmp->Shutdown(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BroadcastChannel) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(BroadcastChannel, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(BroadcastChannel, DOMEventTargetHelper) + +} // namespace dom +} // namespace mozilla diff --git a/dom/broadcastchannel/BroadcastChannel.h b/dom/broadcastchannel/BroadcastChannel.h new file mode 100644 index 0000000000..280913e8f8 --- /dev/null +++ b/dom/broadcastchannel/BroadcastChannel.h @@ -0,0 +1,93 @@ +/* -*- 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_BroadcastChannel_h +#define mozilla_dom_BroadcastChannel_h + +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "nsTArray.h" +#include "mozilla/RefPtr.h" + +class nsIGlobalObject; + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class BroadcastChannelChild; +class RefMessageBodyService; +class WorkerRef; + +class BroadcastChannel final : public DOMEventTargetHelper { + friend class BroadcastChannelChild; + + using PrincipalInfo = mozilla::ipc::PrincipalInfo; + + public: + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(BroadcastChannel, + DOMEventTargetHelper) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<BroadcastChannel> Constructor( + const GlobalObject& aGlobal, const nsAString& aChannel, ErrorResult& aRv); + + void GetName(nsAString& aName) const { aName = mChannel; } + + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + ErrorResult& aRv); + + void Close(); + + IMPL_EVENT_HANDLER(message) + IMPL_EVENT_HANDLER(messageerror) + + void Shutdown(); + + private: + BroadcastChannel(nsIGlobalObject* aGlobal, const nsAString& aChannel, + const nsID& aPortUUID); + + ~BroadcastChannel(); + + void MessageReceived(const MessageData& aData); + + void MessageDelivered(const nsID& aMessageID, uint32_t aOtherBCs); + + void RemoveDocFromBFCache(); + + void DisconnectFromOwner() override; + + void DispatchError(JSContext* aCx); + + RefPtr<BroadcastChannelChild> mActor; + + RefPtr<RefMessageBodyService> mRefMessageBodyService; + + RefPtr<WorkerRef> mWorkerRef; + + nsString mChannel; + nsString mOriginForEvents; + + enum { StateActive, StateClosed } mState; + + // This ID is used to identify the messages-by-reference sent by this port. + // See RefMessageBodyService. + nsID mPortUUID; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_BroadcastChannel_h diff --git a/dom/broadcastchannel/BroadcastChannelChild.cpp b/dom/broadcastchannel/BroadcastChannelChild.cpp new file mode 100644 index 0000000000..30e2633c5d --- /dev/null +++ b/dom/broadcastchannel/BroadcastChannelChild.cpp @@ -0,0 +1,48 @@ +/* -*- 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 "BroadcastChannelChild.h" +#include "BroadcastChannel.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +BroadcastChannelChild::BroadcastChannelChild() + : mBC(nullptr), mActorDestroyed(false) {} + +BroadcastChannelChild::~BroadcastChannelChild() { MOZ_ASSERT(!mBC); } + +mozilla::ipc::IPCResult BroadcastChannelChild::RecvNotify( + const MessageData& aData) { + if (!mBC) { + // The object is going to be deleted soon. No notify is required. + return IPC_OK(); + } + + mBC->MessageReceived(aData); + return IPC_OK(); +} + +mozilla::ipc::IPCResult BroadcastChannelChild::RecvRefMessageDelivered( + const nsID& aMessageID, const uint32_t& aOtherBCs) { + if (!mBC) { + // The object is going to be deleted soon. No notify is required. + return IPC_OK(); + } + + mBC->MessageDelivered(aMessageID, aOtherBCs); + return IPC_OK(); +} + +void BroadcastChannelChild::ActorDestroy(ActorDestroyReason aWhy) { + mActorDestroyed = true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/broadcastchannel/BroadcastChannelChild.h b/dom/broadcastchannel/BroadcastChannelChild.h new file mode 100644 index 0000000000..5c5cae4fd7 --- /dev/null +++ b/dom/broadcastchannel/BroadcastChannelChild.h @@ -0,0 +1,53 @@ +/* -*- 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_BroadcastChannelChild_h +#define mozilla_dom_BroadcastChannelChild_h + +#include "mozilla/dom/PBroadcastChannelChild.h" + +namespace mozilla { + +namespace ipc { +class BackgroundChildImpl; +} // namespace ipc + +namespace dom { + +class BroadcastChannel; + +class BroadcastChannelChild final : public PBroadcastChannelChild { + friend class mozilla::ipc::BackgroundChildImpl; + + public: + NS_INLINE_DECL_REFCOUNTING(BroadcastChannelChild) + + void SetParent(BroadcastChannel* aBC) { mBC = aBC; } + + virtual mozilla::ipc::IPCResult RecvNotify(const MessageData& aData) override; + + virtual mozilla::ipc::IPCResult RecvRefMessageDelivered( + const nsID& aMessageID, const uint32_t& aOtherBCs) override; + + bool IsActorDestroyed() const { return mActorDestroyed; } + + private: + BroadcastChannelChild(); + ~BroadcastChannelChild(); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + // This raw pointer is actually the parent object. + // It's set to null when the parent object is deleted. + BroadcastChannel* mBC; + + bool mActorDestroyed; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_BroadcastChannelChild_h diff --git a/dom/broadcastchannel/BroadcastChannelParent.cpp b/dom/broadcastchannel/BroadcastChannelParent.cpp new file mode 100644 index 0000000000..56c528c933 --- /dev/null +++ b/dom/broadcastchannel/BroadcastChannelParent.cpp @@ -0,0 +1,71 @@ +/* -*- 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 "BroadcastChannelParent.h" +#include "BroadcastChannelService.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/Unused.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +BroadcastChannelParent::BroadcastChannelParent( + const nsAString& aOriginChannelKey) + : mService(BroadcastChannelService::GetOrCreate()), + mOriginChannelKey(aOriginChannelKey) { + AssertIsOnBackgroundThread(); + mService->RegisterActor(this, mOriginChannelKey); +} + +BroadcastChannelParent::~BroadcastChannelParent() { + AssertIsOnBackgroundThread(); +} + +mozilla::ipc::IPCResult BroadcastChannelParent::RecvPostMessage( + const MessageData& aData) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!mService)) { + return IPC_FAIL_NO_REASON(this); + } + + mService->PostMessage(this, aData, mOriginChannelKey); + return IPC_OK(); +} + +mozilla::ipc::IPCResult BroadcastChannelParent::RecvClose() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!mService)) { + return IPC_FAIL_NO_REASON(this); + } + + mService->UnregisterActor(this, mOriginChannelKey); + mService = nullptr; + + Unused << Send__delete__(this); + + return IPC_OK(); +} + +void BroadcastChannelParent::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); + + if (mService) { + // This object is about to be released and with it, also mService will be + // released too. + mService->UnregisterActor(this, mOriginChannelKey); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/broadcastchannel/BroadcastChannelParent.h b/dom/broadcastchannel/BroadcastChannelParent.h new file mode 100644 index 0000000000..fdca75d213 --- /dev/null +++ b/dom/broadcastchannel/BroadcastChannelParent.h @@ -0,0 +1,46 @@ +/* -*- 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_BroadcastChannelParent_h +#define mozilla_dom_BroadcastChannelParent_h + +#include "mozilla/dom/PBroadcastChannelParent.h" + +namespace mozilla { + +namespace ipc { +class BackgroundParentImpl; +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class BroadcastChannelService; + +class BroadcastChannelParent final : public PBroadcastChannelParent { + friend class mozilla::ipc::BackgroundParentImpl; + + using PrincipalInfo = mozilla::ipc::PrincipalInfo; + + private: + explicit BroadcastChannelParent(const nsAString& aOriginChannelKey); + ~BroadcastChannelParent(); + + virtual mozilla::ipc::IPCResult RecvPostMessage( + const MessageData& aData) override; + + virtual mozilla::ipc::IPCResult RecvClose() override; + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + RefPtr<BroadcastChannelService> mService; + const nsString mOriginChannelKey; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_BroadcastChannelParent_h diff --git a/dom/broadcastchannel/BroadcastChannelService.cpp b/dom/broadcastchannel/BroadcastChannelService.cpp new file mode 100644 index 0000000000..c3dab2b28a --- /dev/null +++ b/dom/broadcastchannel/BroadcastChannelService.cpp @@ -0,0 +1,178 @@ +/* -*- 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 "BroadcastChannelService.h" +#include "BroadcastChannelParent.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/ipc/BackgroundParent.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +BroadcastChannelService* sInstance = nullptr; + +ClonedMessageData CloneClonedMessageData(const ClonedMessageData& aOther) { + auto cloneData = SerializedStructuredCloneBuffer{}; + cloneData.data.initScope(aOther.data().data.scope()); + const bool res = cloneData.data.Append(aOther.data().data); + MOZ_RELEASE_ASSERT(res, "out of memory"); + return {std::move(cloneData), aOther.blobs(), aOther.inputStreams(), + aOther.identifiers()}; +} + +MessageData CloneMessageData(const MessageData& aOther) { + switch (aOther.data().type()) { + case MessageDataType::TClonedMessageData: + return {aOther.agentClusterId(), + CloneClonedMessageData(aOther.data().get_ClonedMessageData())}; + case MessageDataType::TRefMessageData: + return {aOther.agentClusterId(), aOther.data().get_RefMessageData()}; + default: + MOZ_CRASH("Unexpected MessageDataType type"); + } +} + +} // namespace + +BroadcastChannelService::BroadcastChannelService() { + AssertIsOnBackgroundThread(); + + // sInstance is a raw BroadcastChannelService*. + MOZ_ASSERT(!sInstance); + sInstance = this; +} + +BroadcastChannelService::~BroadcastChannelService() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(sInstance == this); + MOZ_ASSERT(mAgents.Count() == 0); + + sInstance = nullptr; +} + +// static +already_AddRefed<BroadcastChannelService> +BroadcastChannelService::GetOrCreate() { + AssertIsOnBackgroundThread(); + + RefPtr<BroadcastChannelService> instance = sInstance; + if (!instance) { + instance = new BroadcastChannelService(); + } + return instance.forget(); +} + +void BroadcastChannelService::RegisterActor( + BroadcastChannelParent* aParent, const nsAString& aOriginChannelKey) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + + auto* const parents = mAgents.GetOrInsertNew(aOriginChannelKey); + + MOZ_ASSERT(!parents->Contains(aParent)); + parents->AppendElement(aParent); +} + +void BroadcastChannelService::UnregisterActor( + BroadcastChannelParent* aParent, const nsAString& aOriginChannelKey) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + + if (auto entry = mAgents.Lookup(aOriginChannelKey)) { + entry.Data()->RemoveElement(aParent); + // remove the entry if the array is now empty + if (entry.Data()->IsEmpty()) { + entry.Remove(); + } + } else { + MOZ_CRASH("Invalid state"); + } +} + +void BroadcastChannelService::PostMessage(BroadcastChannelParent* aParent, + const MessageData& aData, + const nsAString& aOriginChannelKey) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + + nsTArray<BroadcastChannelParent*>* parents; + if (!mAgents.Get(aOriginChannelKey, &parents)) { + MOZ_CRASH("Invalid state"); + } + + // We need to keep the array alive for the life-time of this operation. + nsTArray<RefPtr<BlobImpl>> blobImpls; + if (aData.data().type() == MessageDataType::TClonedMessageData) { + const nsTArray<IPCBlob>& blobs = + aData.data().get_ClonedMessageData().blobs(); + if (!blobs.IsEmpty()) { + blobImpls.SetCapacity(blobs.Length()); + + for (uint32_t i = 0, len = blobs.Length(); i < len; ++i) { + RefPtr<BlobImpl> impl = IPCBlobUtils::Deserialize(blobs[i]); + + MOZ_ASSERT(impl); + blobImpls.AppendElement(impl); + } + } + } + + uint32_t selectedActorsOnSamePid = 0; + + // For each parent actor, we notify the message. + for (uint32_t i = 0; i < parents->Length(); ++i) { + BroadcastChannelParent* parent = parents->ElementAt(i); + MOZ_ASSERT(parent); + + if (parent == aParent) { + continue; + } + + if (parent->OtherPid() == aParent->OtherPid()) { + ++selectedActorsOnSamePid; + } + + // We need to have a copy of the data for this parent. + MessageData newData = CloneMessageData(aData); + MOZ_ASSERT(newData.data().type() == aData.data().type()); + + if (!blobImpls.IsEmpty()) { + nsTArray<IPCBlob>& newBlobImpls = + newData.data().get_ClonedMessageData().blobs(); + MOZ_ASSERT(blobImpls.Length() == newBlobImpls.Length()); + + // Serialize Blob objects for this message. + for (uint32_t i = 0, len = blobImpls.Length(); i < len; ++i) { + nsresult rv = IPCBlobUtils::Serialize(blobImpls[i], newBlobImpls[i]); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + } + + Unused << parent->SendNotify(newData); + } + + // If this is a refMessageData, we need to know when it can be released. + if (aData.data().type() == MessageDataType::TRefMessageData) { + Unused << aParent->SendRefMessageDelivered( + aData.data().get_RefMessageData().uuid(), selectedActorsOnSamePid); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/broadcastchannel/BroadcastChannelService.h b/dom/broadcastchannel/BroadcastChannelService.h new file mode 100644 index 0000000000..5ec525d799 --- /dev/null +++ b/dom/broadcastchannel/BroadcastChannelService.h @@ -0,0 +1,47 @@ +/* -*- 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_BroadcastChannelService_h +#define mozilla_dom_BroadcastChannelService_h + +#include "nsISupportsImpl.h" +#include "nsHashKeys.h" +#include "nsClassHashtable.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +namespace mozilla::dom { + +class BroadcastChannelParent; +class MessageData; + +class BroadcastChannelService final { + public: + NS_INLINE_DECL_REFCOUNTING(BroadcastChannelService) + + static already_AddRefed<BroadcastChannelService> GetOrCreate(); + + void RegisterActor(BroadcastChannelParent* aParent, + const nsAString& aOriginChannelKey); + void UnregisterActor(BroadcastChannelParent* aParent, + const nsAString& aOriginChannelKey); + + void PostMessage(BroadcastChannelParent* aParent, const MessageData& aData, + const nsAString& aOriginChannelKey); + + private: + BroadcastChannelService(); + ~BroadcastChannelService(); + + // Raw Pointers because the actors keep alive this service. + nsClassHashtable<nsStringHashKey, nsTArray<BroadcastChannelParent*>> mAgents; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_BroadcastChannelService_h diff --git a/dom/broadcastchannel/PBroadcastChannel.ipdl b/dom/broadcastchannel/PBroadcastChannel.ipdl new file mode 100644 index 0000000000..6ff27b45d8 --- /dev/null +++ b/dom/broadcastchannel/PBroadcastChannel.ipdl @@ -0,0 +1,36 @@ +/* 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 protocol PBackground; + +include DOMTypes; + +using struct nsID from "nsID.h"; + +namespace mozilla { +namespace dom { + +// This protocol is used for the BroadcastChannel API +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PBroadcastChannel +{ + manager PBackground; + +parent: + async PostMessage(MessageData message); + async Close(); + +child: + // A message must be delivered. + async Notify(MessageData message); + + // A message has been delivered to other channels. It can be removed after + // all the actorsOnSamePid have retrieved it. + async RefMessageDelivered(nsID messageID, uint32_t actorsOnSamePid); + + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/broadcastchannel/moz.build b/dom/broadcastchannel/moz.build new file mode 100644 index 0000000000..573686bf30 --- /dev/null +++ b/dom/broadcastchannel/moz.build @@ -0,0 +1,30 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: postMessage") + +EXPORTS.mozilla.dom += [ + "BroadcastChannel.h", +] + +UNIFIED_SOURCES += [ + "BroadcastChannel.cpp", + "BroadcastChannelChild.cpp", + "BroadcastChannelParent.cpp", + "BroadcastChannelService.cpp", +] + +IPDL_SOURCES += [ + "PBroadcastChannel.ipdl", +] + +MOCHITEST_MANIFESTS += ["tests/mochitest.toml"] +BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/broadcastchannel/tests/blank.html b/dom/broadcastchannel/tests/blank.html new file mode 100644 index 0000000000..358db717dd --- /dev/null +++ b/dom/broadcastchannel/tests/blank.html @@ -0,0 +1,2 @@ +<!DOCTYPE HTML> +<html><body></body></html> diff --git a/dom/broadcastchannel/tests/broadcastchannel_sharedWorker.js b/dom/broadcastchannel/tests/broadcastchannel_sharedWorker.js new file mode 100644 index 0000000000..17fb806371 --- /dev/null +++ b/dom/broadcastchannel/tests/broadcastchannel_sharedWorker.js @@ -0,0 +1,17 @@ +/* eslint-env worker */ + +onconnect = function (evt) { + evt.ports[0].onmessage = function (evt1) { + var bc = new BroadcastChannel("foobar"); + bc.addEventListener("message", function (event) { + bc.postMessage( + event.data == "hello world from the window" + ? "hello world from the worker" + : "KO" + ); + bc.close(); + }); + + evt1.target.postMessage("READY"); + }; +}; diff --git a/dom/broadcastchannel/tests/broadcastchannel_worker_alive.js b/dom/broadcastchannel/tests/broadcastchannel_worker_alive.js new file mode 100644 index 0000000000..da608c6d20 --- /dev/null +++ b/dom/broadcastchannel/tests/broadcastchannel_worker_alive.js @@ -0,0 +1,7 @@ +new BroadcastChannel("foobar").addEventListener("message", function (event) { + if (event.data != "READY") { + event.target.postMessage(event.data); + } +}); + +new BroadcastChannel("foobar").postMessage("READY"); diff --git a/dom/broadcastchannel/tests/browser.toml b/dom/broadcastchannel/tests/browser.toml new file mode 100644 index 0000000000..4162a34a68 --- /dev/null +++ b/dom/broadcastchannel/tests/browser.toml @@ -0,0 +1,5 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = ["blank.html"] + +["browser_private_browsing.js"] diff --git a/dom/broadcastchannel/tests/browser_private_browsing.js b/dom/broadcastchannel/tests/browser_private_browsing.js new file mode 100644 index 0000000000..e61e8ae58e --- /dev/null +++ b/dom/broadcastchannel/tests/browser_private_browsing.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const URL = + "http://mochi.test:8888/browser/dom/broadcastchannel/tests/blank.html"; + +add_task(async function () { + var win1 = OpenBrowserWindow({ private: true }); + var win1Promise = new win1.Promise(resolve => { + win1.addEventListener( + "load", + function () { + resolve(); + }, + { once: true } + ); + }); + await win1Promise; + + var win2 = OpenBrowserWindow({ private: false }); + var win2Promise = new win2.Promise(resolve => { + win2.addEventListener( + "load", + function () { + resolve(); + }, + { once: true } + ); + }); + await win2Promise; + + var tab1 = BrowserTestUtils.addTab(win1.gBrowser, URL); + await BrowserTestUtils.browserLoaded(win1.gBrowser.getBrowserForTab(tab1)); + var browser1 = gBrowser.getBrowserForTab(tab1); + + var tab2 = BrowserTestUtils.addTab(win2.gBrowser, URL); + await BrowserTestUtils.browserLoaded(win2.gBrowser.getBrowserForTab(tab2)); + var browser2 = gBrowser.getBrowserForTab(tab2); + + var p1 = SpecialPowers.spawn(browser1, [], function (opts) { + return new content.window.Promise(resolve => { + content.window.bc = new content.window.BroadcastChannel("foobar"); + content.window.bc.onmessage = function (e) { + resolve(e.data); + }; + }); + }); + + var p2 = SpecialPowers.spawn(browser2, [], function (opts) { + return new content.window.Promise(resolve => { + content.window.bc = new content.window.BroadcastChannel("foobar"); + content.window.bc.onmessage = function (e) { + resolve(e.data); + }; + }); + }); + + await SpecialPowers.spawn(browser1, [], function (opts) { + return new content.window.Promise(resolve => { + var bc = new content.window.BroadcastChannel("foobar"); + bc.postMessage("hello world from private browsing"); + resolve(); + }); + }); + + await SpecialPowers.spawn(browser2, [], function (opts) { + return new content.window.Promise(resolve => { + var bc = new content.window.BroadcastChannel("foobar"); + bc.postMessage("hello world from non private browsing"); + resolve(); + }); + }); + + var what1 = await p1; + is( + what1, + "hello world from private browsing", + "No messages received from the other window." + ); + + var what2 = await p2; + is( + what2, + "hello world from non private browsing", + "No messages received from the other window." + ); + + BrowserTestUtils.removeTab(tab1); + await BrowserTestUtils.closeWindow(win1); + + BrowserTestUtils.removeTab(tab2); + await BrowserTestUtils.closeWindow(win2); +}); diff --git a/dom/broadcastchannel/tests/file_mozbrowser.html b/dom/broadcastchannel/tests/file_mozbrowser.html new file mode 100644 index 0000000000..6370500d98 --- /dev/null +++ b/dom/broadcastchannel/tests/file_mozbrowser.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozBrowser iframe</title> +</head> +<body> +<div id="container"></div> + <script type="application/javascript"> + + var ifr = document.createElement("iframe"); + ifr.src = "http://mochi.test:8888/tests/dom/broadcastchannel/tests/iframe_mozbrowser.html"; + ifr.onload = function() { alert("DONE"); }; + + var domParent = document.getElementById("container"); + domParent.appendChild(ifr); + + </script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/file_mozbrowser2.html b/dom/broadcastchannel/tests/file_mozbrowser2.html new file mode 100644 index 0000000000..2e5f394eb8 --- /dev/null +++ b/dom/broadcastchannel/tests/file_mozbrowser2.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozBrowser iframe</title> +</head> +<body> +<div id="container"></div> + <script type="application/javascript"> + + var ifr = document.createElement("iframe"); + ifr.setAttribute("mozbrowser", true); + ifr.src = "http://mochi.test:8888/tests/dom/broadcastchannel/tests/iframe_mozbrowser2.html"; + ifr.onload = function() { alert("DONE"); }; + + var domParent = document.getElementById("container"); + domParent.appendChild(ifr); + + </script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/iframe_broadcastchannel.html b/dom/broadcastchannel/tests/iframe_broadcastchannel.html new file mode 100644 index 0000000000..2d1724da0b --- /dev/null +++ b/dom/broadcastchannel/tests/iframe_broadcastchannel.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<body> + <script type="application/javascript"> + +function is(a, b, msg) { + ok(a == b, msg); +} + +function ok(a, msg) { + window.parent.postMessage({ status: a ? "OK" : "KO", message: msg }, "*"); +} + +ok("BroadcastChannel" in window, "BroadcastChannel exists"); + +var bc = new BroadcastChannel("foobar"); +ok(bc, "BroadcastChannel can be created"); +is(bc.name, "foobar", "BroadcastChannel.name is foobar"); + +ok("postMessage" in bc, "BroadcastChannel has postMessage() method"); + +bc.onmessage = function(evt) { + ok(evt instanceof MessageEvent, "evt is a MessageEvent"); + is(evt.target, bc, "MessageEvent.target is bc"); + is(evt.target.name, "foobar", "MessageEvent.target.name is foobar"); + is(evt.target.name, bc.name, "MessageEvent.target.name is bc.name"); + is(evt.data, "Hello world from the window!", "Message received from the window"); + bc.postMessage("Hello world from the iframe!"); +}; + + </script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/iframe_mozbrowser.html b/dom/broadcastchannel/tests/iframe_mozbrowser.html new file mode 100644 index 0000000000..adbb77a061 --- /dev/null +++ b/dom/broadcastchannel/tests/iframe_mozbrowser.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozBrowser iframe</title> +</head> +<body> + <script type="application/javascript"> + +var bc = new BroadcastChannel("foobar"); +bc.postMessage("This is wrong!"); + + </script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/iframe_mozbrowser2.html b/dom/broadcastchannel/tests/iframe_mozbrowser2.html new file mode 100644 index 0000000000..adbb77a061 --- /dev/null +++ b/dom/broadcastchannel/tests/iframe_mozbrowser2.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozBrowser iframe</title> +</head> +<body> + <script type="application/javascript"> + +var bc = new BroadcastChannel("foobar"); +bc.postMessage("This is wrong!"); + + </script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/mochitest.toml b/dom/broadcastchannel/tests/mochitest.toml new file mode 100644 index 0000000000..40264f7a3d --- /dev/null +++ b/dom/broadcastchannel/tests/mochitest.toml @@ -0,0 +1,41 @@ +[DEFAULT] +support-files = [ + "iframe_broadcastchannel.html", + "broadcastchannel_sharedWorker.js", + "broadcastchannel_worker_alive.js", + "!/dom/events/test/event_leak_utils.js", + "file_mozbrowser.html", + "file_mozbrowser2.html", + "iframe_mozbrowser.html", + "iframe_mozbrowser2.html", + "testUrl1_bfcache.html", + "testUrl2_bfcache.html", +] + +["test_bfcache.html"] + +["test_broadcastchannel_basic.html"] +skip-if = [ + "http3", + "http2", +] + +["test_broadcastchannel_close.html"] + +["test_broadcastchannel_self.html"] + +["test_broadcastchannel_sharedWorker.html"] + +["test_broadcastchannel_worker_alive.html"] + +["test_dataCloning.html"] + +["test_dataURL.html"] + +["test_event_listener_leaks.html"] + +["test_invalidState.html"] + +["test_message_after_close.html"] + +["test_ordering.html"] diff --git a/dom/broadcastchannel/tests/testUrl1_bfcache.html b/dom/broadcastchannel/tests/testUrl1_bfcache.html new file mode 100644 index 0000000000..d31b13bd25 --- /dev/null +++ b/dom/broadcastchannel/tests/testUrl1_bfcache.html @@ -0,0 +1,18 @@ +<script> +var bc = new BroadcastChannel("a"); +onpageshow = function(e) { + var bc1 = new BroadcastChannel("testUrl1_bfcache"); + bc1.onmessage = function(event) { + if (event.data == "close") { + bc1.postMessage("closed"); + bc1.close(); + bc.close(); + window.close(); + } else if (event.data == "load") { + bc1.close(); + location.href = "testUrl2_bfcache.html"; + } + }; + bc1.postMessage({type: e.type, persisted: e.persisted}); +}; +</script> diff --git a/dom/broadcastchannel/tests/testUrl2_bfcache.html b/dom/broadcastchannel/tests/testUrl2_bfcache.html new file mode 100644 index 0000000000..a1eed7927a --- /dev/null +++ b/dom/broadcastchannel/tests/testUrl2_bfcache.html @@ -0,0 +1,12 @@ +<script> +onpageshow = function(e) { + var bc2 = new BroadcastChannel("testUrl2_bfcache"); + bc2.onmessage = function(event) { + if (event.data == "back") { + bc2.close(); + history.back(); + } + }; + bc2.postMessage({type: e.type, persisted: e.persisted}); +}; +</script> diff --git a/dom/broadcastchannel/tests/test_bfcache.html b/dom/broadcastchannel/tests/test_bfcache.html new file mode 100644 index 0000000000..0197f343e2 --- /dev/null +++ b/dom/broadcastchannel/tests/test_bfcache.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for bfcache and BroadcastChannel</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + + /* + * The test opens a new window. Then a message 'load' is sent there to load + * another page. If expectedPersisted is false, a dummy message is sent + * through a BroadcastChannel which the first has registered to use. + * That should evict the page from bfcache. + * 'back' message is sent to call history.back(). + * The page which is loaded from session history should be persisted if + * expectedPersisted is true. + */ + + SimpleTest.waitForExplicitFinish(); + var testUrl1 = "testUrl1_bfcache.html"; + + + function executeTest() { + var bc1 = new BroadcastChannel("testUrl1_bfcache"); + var bc2 = new BroadcastChannel("testUrl2_bfcache"); + bc1.onmessage = function(event) { + if (event.data == "closed") { + info("Closed"); + runTest(); + return; + } + page1Shown(event.data); + }; + bc2.onmessage = function(event) { page2Shown(event.data); }; + + var counter = 0; + var expectedPersisted = false; + var bc = new BroadcastChannel("a"); + + function page1Shown(e) { + if (counter == 0) { + ok(!e.persisted, "test page should have been persisted initially"); + bc1.postMessage("load"); + } else { + is(e.persisted, expectedPersisted, "test page should have been persisted in pageshow"); + bc1.postMessage("close"); + } + + counter++; + } + + function page2Shown(e) { + if (!expectedPersisted) { + SimpleTest.executeSoon(function() { + info("Posting a message."); + bc.postMessage(42); + }); + } + + SimpleTest.executeSoon(function() { + info("Going back"); + bc2.postMessage("back"); + }); + } + + var tests = [ + { expectedPersisted: true }, + { expectedPersisted: false }, + ]; + + function runTest() { + if (!tests.length) { + bc.close(); + bc1.close(); + bc2.close(); + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + + counter = 0; + expectedPersisted = test.expectedPersisted; + window.open(testUrl1, "", "noopener"); + } + + + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + runTest(); + }); + + } + + if (isXOrigin) { + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the BroadcastChannel used to + // communicate with the opened windows works in xorigin tests. Otherwise, + // the iframe containing this page is isolated from first-party storage access, + // which isolates BroadcastChannel communication. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + SpecialPowers.addPermission("storageAccessAPI", true, window.location.href).then(() => { + SpecialPowers.wrap(document).requestStorageAccess().then(() => { + SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]] + }).then(() => { + executeTest(); + }); + }); + }); + } else { + executeTest(); + } + + </script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_broadcastchannel_basic.html b/dom/broadcastchannel/tests/test_broadcastchannel_basic.html new file mode 100644 index 0000000000..09196b0c52 --- /dev/null +++ b/dom/broadcastchannel/tests/test_broadcastchannel_basic.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for BroadcastChannel</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<div id="content"></div> + +<script type="application/javascript"> + +function runTest() { + addEventListener("message", receiveMessage, false); + function receiveMessage(evt) { + if (evt.data.status == "OK") { + ok(true, evt.data.message); + } else if (evt.data.status == "KO") { + ok(false, evt.data.message); + } else { + ok(false, "Unknown message"); + } + } + + ok("BroadcastChannel" in window, "BroadcastChannel exists"); + + var bc = new BroadcastChannel("foobar"); + ok(bc, "BroadcastChannel can be created"); + is(bc.name, "foobar", "BroadcastChannel.name is foobar"); + + ok("postMessage" in bc, "BroadcastChannel has postMessage() method"); + + bc.onmessage = function(evt) { + ok(evt instanceof MessageEvent, "This is a MessageEvent"); + is(evt.target, bc, "MessageEvent.target is bc"); + is(evt.target.name, "foobar", "MessageEvent.target.name is foobar"); + is(evt.target.name, bc.name, "MessageEvent.target.name == bc.name"); + ok(evt.origin.indexOf("http://mochi.test:8888") == 0, "MessageEvent.origin is correct"); + is(evt.data, "Hello world from the iframe!", "The message from the iframe has been received!"); + SimpleTest.finish(); + }; + + var div = document.getElementById("content"); + ok(div, "Parent exists"); + + var ifr = document.createElement("iframe"); + ifr.addEventListener("load", iframeLoaded); + ifr.setAttribute("src", "iframe_broadcastchannel.html"); + div.appendChild(ifr); + + function iframeLoaded() { + bc.postMessage("Hello world from the window!"); + } + + // A leak test + var dummyBc = new BroadcastChannel("dont_leak_this"); + dummyBc.foo = "bar"; + // don't add message listener! +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + +</script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_broadcastchannel_close.html b/dom/broadcastchannel/tests/test_broadcastchannel_close.html new file mode 100644 index 0000000000..91cb9c92a4 --- /dev/null +++ b/dom/broadcastchannel/tests/test_broadcastchannel_close.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for BroadcastChannel</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<div id="content"></div> + +<script type="application/javascript"> + +function runTest() { + var receiver = new BroadcastChannel("foo"); + var sequence = [ "2", "done" ]; + receiver.onmessage = function(e) { + if (!sequence.length) { + ok(false, "No more data is expected"); + return; + } + + var data = sequence.shift(); + is(e.data, data); + + if (!sequence.length) { + SimpleTest.executeSoon(function() { + SimpleTest.finish(); + }); + } + }; + + var x = new BroadcastChannel("foo"); + x.close(); + try { + x.postMessage("1"); + ok(false, "PostMessage should throw if called after a close()."); + } catch (e) { + ok(true, "PostMessage should throw if called after a close()."); + } + + var y = new BroadcastChannel("foo"); + y.postMessage("2"); + y.close(); + try { + y.postMessage("3"); + ok(false, "PostMessage should throw if called after a close()."); + } catch (e) { + ok(true, "PostMessage should throw if called after a close()."); + } + + var z = new BroadcastChannel("foo"); + z.postMessage("done"); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + +</script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_broadcastchannel_self.html b/dom/broadcastchannel/tests/test_broadcastchannel_self.html new file mode 100644 index 0000000000..501f71f09c --- /dev/null +++ b/dom/broadcastchannel/tests/test_broadcastchannel_self.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for BroadcastChannel</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<div id="content"></div> + +<script type="application/javascript"> + +function runTest() { + let x = new BroadcastChannel("foo"); + let y = new BroadcastChannel("foo"); + + function func(e) { + is(e.target, y, "The target is !x"); + + SimpleTest.executeSoon(function() { + SimpleTest.finish(); + }); + } + + x.onmessage = func; + y.onmessage = func; + + x.postMessage("foo"); +} + +SimpleTest.waitForExplicitFinish(); +runTest(); + +</script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_broadcastchannel_sharedWorker.html b/dom/broadcastchannel/tests/test_broadcastchannel_sharedWorker.html new file mode 100644 index 0000000000..f76c17cbd8 --- /dev/null +++ b/dom/broadcastchannel/tests/test_broadcastchannel_sharedWorker.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM BroadcastChannel in SharedWorkers +--> +<head> + <title>Test for BroadcastChannel in SharedWorkers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + +function runTests() { + var worker = new SharedWorker("broadcastchannel_sharedWorker.js"); + + var bc = new BroadcastChannel("foobar"); + + worker.port.onmessage = function(event) { + if (event.data == "READY") { + ok(true, "SharedWorker is ready!"); + bc.postMessage("hello world from the window"); + } else { + ok(false, "Something wrong happened"); + } + }; + + bc.onmessage = function(event) { + is("hello world from the worker", event.data, "The message matches!"); + bc.close(); + SimpleTest.finish(); + }; + + worker.port.postMessage("go"); +} + +SimpleTest.waitForExplicitFinish(); +runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_broadcastchannel_worker_alive.html b/dom/broadcastchannel/tests/test_broadcastchannel_worker_alive.html new file mode 100644 index 0000000000..3667c8ae9d --- /dev/null +++ b/dom/broadcastchannel/tests/test_broadcastchannel_worker_alive.html @@ -0,0 +1,56 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM BroadcastChannel in workers +--> +<head> + <title>Test for BroadcastChannel in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + +function runTests() { + var id = 0; + (new BroadcastChannel("foobar")).onmessage = function(event) { + info("MSG: " + event.data); + + if (event.data == "READY") { + ok(true, "Worker is ready!"); + } else { + is(id, event.data, "The message is correct: " + id); + } + + for (var i = 0; i < 3; ++i) { + SpecialPowers.forceCC(); + SpecialPowers.forceGC(); + } + + if (id == 5) { + SimpleTest.finish(); + return; + } + + event.target.postMessage(++id); + }; + + new Worker("broadcastchannel_worker_alive.js"); +} + +SimpleTest.waitForExplicitFinish(); +runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_dataCloning.html b/dom/broadcastchannel/tests/test_dataCloning.html new file mode 100644 index 0000000000..26c1ce370a --- /dev/null +++ b/dom/broadcastchannel/tests/test_dataCloning.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for BroadcastChannel.postMessage invalid State</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<div id="content"></div> + +<script type="application/javascript"> + + +let c = new BroadcastChannel("foo"); + +try { + c.postMessage(Symbol()); + ok(false, "This should throw!"); +} catch (e) { + ok(true, "This should throw!"); + is(e.name, "DataCloneError", "Correct DataCloneError exception thrown"); +} + +</script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_dataURL.html b/dom/broadcastchannel/tests/test_dataURL.html new file mode 100644 index 0000000000..f4f9c71db3 --- /dev/null +++ b/dom/broadcastchannel/tests/test_dataURL.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for BroadcastChannel in data: URL</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div id="dataURL"> +var a = new BroadcastChannel('a'); +var b = new BroadcastChannel('a'); +a.onmessage = function(e) { parent.postMessage(e.data, "*"); }; +b.postMessage(42); +</div> + +<script> + +SimpleTest.waitForExplicitFinish(); + +onmessage = function(e) { + is(e.data, 42, "BroadcastChannel works with data URLs"); + SimpleTest.finish(); +}; + +// eslint-disable-next-line no-useless-concat +var url = "data:text/html,<script>" + document.getElementById("dataURL").textContent + "</" + "script>"; + +var ifr = document.createElement("iframe"); +document.body.appendChild(ifr); + +ifr.setAttribute("sandbox", "allow-scripts"); +ifr.src = url; +</script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_event_listener_leaks.html b/dom/broadcastchannel/tests/test_event_listener_leaks.html new file mode 100644 index 0000000000..33568913fc --- /dev/null +++ b/dom/broadcastchannel/tests/test_event_listener_leaks.html @@ -0,0 +1,55 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1450358 - Test BroadcastChannel event listener leak conditions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +// Manipulate BroadcastChannel objects in the frame's context. +// Its important here that we create a listener callback from +// the DOM objects back to the frame's global in order to +// exercise the leak condition. +let count = 0; +async function useBroadcastChannel(contentWindow) { + contentWindow.messageCount = 0; + + count += 1; + const name = `test_event_listener_leaks-${count}`; + + let bc = new contentWindow.BroadcastChannel(name); + let outer = new BroadcastChannel(name); + outer.postMessage("foo"); + + await new Promise(resolve => { + bc.onmessage = e => { + contentWindow.messageCount += 1; + resolve(); + }; + }); + + is(contentWindow.messageCount, 1, "message should be received"); +} + +async function runTest() { + try { + await checkForEventListenerLeaks("BroadcastChannel", useBroadcastChannel); + } catch (e) { + ok(false, e); + } finally { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +addEventListener("load", runTest, { once: true }); +</script> +</pre> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_invalidState.html b/dom/broadcastchannel/tests/test_invalidState.html new file mode 100644 index 0000000000..371a58768a --- /dev/null +++ b/dom/broadcastchannel/tests/test_invalidState.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for BroadcastChannel.postMessage invalid State</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<div id="content"></div> + +<script type="application/javascript"> + +var c = new BroadcastChannel("foo"); +c.close(); + +try { + c.postMessage("bar"); + ok(false, "This should throw!"); +} catch (e) { + ok(true, "This should throw!"); + is(e.name, "InvalidStateError", "Correct invalid-state exception thrown"); +} + +</script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_message_after_close.html b/dom/broadcastchannel/tests/test_message_after_close.html new file mode 100644 index 0000000000..1ef8a018ea --- /dev/null +++ b/dom/broadcastchannel/tests/test_message_after_close.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for BroadcastChannel - message after close</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script> +SimpleTest.waitForExplicitFinish(); + +let a = new BroadcastChannel('a'); +let b = new BroadcastChannel('a'); +let count = 0; + +a.onmessage = function(e) { + ++count; + a.close(); + + setTimeout(() => { + is(count, 1, "Only 1 message received"); + SimpleTest.finish(); + }, 0); +} + +for (let i = 0; i < 100; ++i) { + b.postMessage(42); +} +</script> +</body> +</html> diff --git a/dom/broadcastchannel/tests/test_ordering.html b/dom/broadcastchannel/tests/test_ordering.html new file mode 100644 index 0000000000..5dd36bc77d --- /dev/null +++ b/dom/broadcastchannel/tests/test_ordering.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for BroadcastChannel.postMessage invalid State</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<div id="content"></div> + +<script type="application/javascript"> + +let c1 = new BroadcastChannel("order"); +let c2 = new BroadcastChannel("order"); +let c3 = new BroadcastChannel("order"); + +let events = []; +let doneCount = 0; + +function whichBC(bc) { + if (bc == c1) return "c1"; + if (bc == c2) return "c2"; + if (bc == c3) return "c3"; + return "What?!?"; +} + +function handler(e) { + events.push(e); + if (e.data == "done") { + doneCount++; + if (doneCount == 2) { + is(events.length, 6, "Correct length"); + is(whichBC(events[0].target), "c2", "target for event 0"); + is(events[0].data, "from c1"); + is(whichBC(events[1].target), "c3", "target for event 1"); + is(events[1].data, "from c1"); + is(whichBC(events[2].target), "c1", "target for event 2"); + is(events[2].data, "from c3"); + is(whichBC(events[3].target), "c2", "target for event 3"); + is(events[3].data, "from c3"); + is(whichBC(events[4].target), "c1", "target for event 4"); + is(events[4].data, "done"); + is(whichBC(events[5].target), "c3", "target for event 5"); + is(events[5].data, "done"); + + SimpleTest.finish(); + } + } +} + +c1.onmessage = handler; +c2.onmessage = handler; +c3.onmessage = handler; + +c1.postMessage("from c1"); +c3.postMessage("from c3"); +c2.postMessage("done"); + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> |