Commit 286a3c23 authored by Yaron Tausky's avatar Yaron Tausky
Browse files

Bug 1263734: Implement ServiceWorkerContainer.startMessages() r=asuth,smaug

Differential Revision: https://phabricator.services.mozilla.com/D4237

--HG--
extra : moz-landing-system : lando
parent 6347cf34
......@@ -67,6 +67,8 @@
#include "mozilla/dom/FeaturePolicy.h"
#include "mozilla/dom/FramingChecker.h"
#include "mozilla/dom/HTMLSharedElement.h"
#include "mozilla/dom/Navigator.h"
#include "mozilla/dom/ServiceWorkerContainer.h"
#include "mozilla/dom/SVGUseElement.h"
#include "nsGenericHTMLElement.h"
#include "mozilla/dom/CDATASection.h"
......@@ -5195,6 +5197,14 @@ nsIDocument::DispatchContentLoadedEvents()
NS_LITERAL_STRING("DOMContentLoaded"),
CanBubble::eYes, Cancelable::eNo);
if (auto* const window = GetInnerWindow()) {
const RefPtr<ServiceWorkerContainer> serviceWorker = window->Navigator()->ServiceWorker();
// This could cause queued messages from a service worker to get
// dispatched on serviceWorker.
serviceWorker->StartMessages();
}
if (MayStartLayout()) {
MaybeResolveReadyForIdle();
}
......
......@@ -605,125 +605,17 @@ RefPtr<ClientOpPromise>
ClientSource::PostMessage(const ClientPostMessageArgs& aArgs)
{
NS_ASSERT_OWNINGTHREAD(ClientSource);
RefPtr<ClientOpPromise> ref;
ServiceWorkerDescriptor source(aArgs.serviceWorker());
const PrincipalInfo& principalInfo = source.PrincipalInfo();
StructuredCloneData clonedData;
clonedData.BorrowFromClonedMessageDataForBackgroundChild(aArgs.clonedData());
// Currently we only support firing these messages on window Clients.
// Once we expose ServiceWorkerContainer and the ServiceWorker on Worker
// threads then this will need to change. See bug 1113522.
if (mClientInfo.Type() != ClientType::Window) {
ref = ClientOpPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__);
return ref.forget();
// TODO: Currently this function only supports clients whose global
// object is a Window; it should also support those whose global
// object is a WorkerGlobalScope.
if (nsPIDOMWindowInner* const window = GetInnerWindow()) {
const RefPtr<ServiceWorkerContainer> container = window->Navigator()->ServiceWorker();
container->ReceiveMessage(aArgs);
return ClientOpPromise::CreateAndResolve(NS_OK, __func__).forget();
}
MOZ_ASSERT(NS_IsMainThread());
RefPtr<ServiceWorkerContainer> target;
nsCOMPtr<nsIGlobalObject> globalObject;
// We don't need to force the creation of the about:blank document
// here because there is no postMessage listener. If a listener
// was registered then the document will already be created.
nsPIDOMWindowInner* window = GetInnerWindow();
if (window) {
globalObject = do_QueryInterface(window);
target = window->Navigator()->ServiceWorker();
}
if (NS_WARN_IF(!target)) {
ref = ClientOpPromise::CreateAndReject(NS_ERROR_DOM_INVALID_STATE_ERR,
__func__);
return ref.forget();
}
// If AutoJSAPI::Init() fails then either global is nullptr or not
// in a usable state.
AutoJSAPI jsapi;
if (!jsapi.Init(globalObject)) {
ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__);
return ref.forget();
}
JSContext* cx = jsapi.cx();
ErrorResult result;
JS::Rooted<JS::Value> messageData(cx);
clonedData.Read(cx, &messageData, result);
if (result.MaybeSetPendingException(cx)) {
// We reported the error in the current window context. Resolve
// promise instead of rejecting.
ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__);
return ref.forget();
}
RootedDictionary<MessageEventInit> init(cx);
init.mData = messageData;
if (!clonedData.TakeTransferredPortsAsSequence(init.mPorts)) {
// Report the error in the current window context and resolve the
// promise instead of rejecting.
xpc::Throw(cx, NS_ERROR_OUT_OF_MEMORY);
ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__);
return ref.forget();
}
nsresult rv = NS_OK;
nsCOMPtr<nsIPrincipal> principal =
PrincipalInfoToPrincipal(principalInfo, &rv);
if (NS_FAILED(rv) || !principal) {
ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
return ref.forget();
}
nsAutoCString origin;
rv = principal->GetOriginNoSuffix(origin);
if (NS_SUCCEEDED(rv)) {
CopyUTF8toUTF16(origin, init.mOrigin);
}
RefPtr<ServiceWorker> instance;
if (ServiceWorkerParentInterceptEnabled()) {
instance = globalObject->GetOrCreateServiceWorker(source);
} else {
// If we are in legacy child-side intercept mode then we need to verify
// this registration exists in the current process.
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
if (!swm) {
// Shutting down. Just don't deliver this message.
ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
return ref.forget();
}
RefPtr<ServiceWorkerRegistrationInfo> reg =
swm->GetRegistration(principal, source.Scope());
if (reg) {
instance = globalObject->GetOrCreateServiceWorker(source);
}
}
if (instance) {
init.mSource.SetValue().SetAsServiceWorker() = instance;
}
RefPtr<MessageEvent> event =
MessageEvent::Constructor(target, NS_LITERAL_STRING("message"), init);
event->SetTrusted(true);
target->DispatchEvent(*event, result);
if (result.Failed()) {
result.SuppressException();
ref = ClientOpPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
return ref.forget();
}
ref = ClientOpPromise::CreateAndResolve(NS_OK, __func__);
return ref.forget();
return ClientOpPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__).forget();
}
RefPtr<ClientOpPromise>
......
......@@ -12,6 +12,7 @@
#include "nsIDocument.h"
#include "nsIServiceWorkerManager.h"
#include "nsIScriptError.h"
#include "nsThreadUtils.h"
#include "nsIURL.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"
......@@ -22,11 +23,16 @@
#include "nsServiceManagerUtils.h"
#include "mozilla/LoadInfo.h"
#include "mozilla/dom/ClientIPCTypes.h"
#include "mozilla/dom/DOMMozPromiseRequestHolder.h"
#include "mozilla/dom/MessageEvent.h"
#include "mozilla/dom/MessageEventBinding.h"
#include "mozilla/dom/Navigator.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/ServiceWorker.h"
#include "mozilla/dom/ServiceWorkerContainerBinding.h"
#include "mozilla/dom/ServiceWorkerManager.h"
#include "mozilla/dom/ipc/StructuredCloneData.h"
#include "RemoteServiceWorkerContainerImpl.h"
#include "ServiceWorker.h"
......@@ -34,6 +40,11 @@
#include "ServiceWorkerRegistration.h"
#include "ServiceWorkerUtils.h"
// This is defined to something else on Windows
#ifdef DispatchMessage
#undef DispatchMessage
#endif
namespace mozilla {
namespace dom {
......@@ -152,6 +163,39 @@ ServiceWorkerContainer::ControllerChanged(ErrorResult& aRv)
aRv = DispatchTrustedEvent(NS_LITERAL_STRING("controllerchange"));
}
using mozilla::dom::ipc::StructuredCloneData;
// A ReceivedMessage represents a message sent via
// Client.postMessage(). It is used as used both for queuing of
// incoming messages and as an interface to DispatchMessage().
struct MOZ_HEAP_CLASS ServiceWorkerContainer::ReceivedMessage
{
explicit ReceivedMessage(const ClientPostMessageArgs& aArgs)
: mServiceWorker(aArgs.serviceWorker())
{
mClonedData.CopyFromClonedMessageDataForBackgroundChild(aArgs.clonedData());
}
ServiceWorkerDescriptor mServiceWorker;
StructuredCloneData mClonedData;
NS_INLINE_DECL_REFCOUNTING(ReceivedMessage)
private:
~ReceivedMessage() = default;
};
void
ServiceWorkerContainer::ReceiveMessage(const ClientPostMessageArgs& aArgs)
{
RefPtr<ReceivedMessage> message = new ReceivedMessage(aArgs);
if (mMessagesStarted) {
EnqueueReceivedMessageDispatch(message.forget());
} else {
mPendingMessages.AppendElement(message.forget());
}
}
JSObject*
ServiceWorkerContainer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
......@@ -426,6 +470,16 @@ ServiceWorkerContainer::GetRegistrations(ErrorResult& aRv)
return outer.forget();
}
void
ServiceWorkerContainer::StartMessages()
{
while (!mPendingMessages.IsEmpty()) {
EnqueueReceivedMessageDispatch(mPendingMessages.ElementAt(0));
mPendingMessages.RemoveElementAt(0);
}
mMessagesStarted = true;
}
already_AddRefed<Promise>
ServiceWorkerContainer::GetRegistration(const nsAString& aURL,
ErrorResult& aRv)
......@@ -618,5 +672,153 @@ ServiceWorkerContainer::GetGlobalIfValid(ErrorResult& aRv,
return window->AsGlobal();
}
void
ServiceWorkerContainer::EnqueueReceivedMessageDispatch(RefPtr<ReceivedMessage> aMessage) {
if (nsPIDOMWindowInner* const window = GetOwner()) {
if (auto* const target = window->EventTargetFor(TaskCategory::Other)) {
target->Dispatch(
NewRunnableMethod<RefPtr<ReceivedMessage>>(
"ServiceWorkerContainer::DispatchMessage",
this,
&ServiceWorkerContainer::DispatchMessage,
std::move(aMessage)
)
);
}
}
}
template <typename F>
void
ServiceWorkerContainer::RunWithJSContext(F&& aCallable)
{
nsCOMPtr<nsIGlobalObject> globalObject;
if (nsPIDOMWindowInner* const window = GetOwner()) {
globalObject = do_QueryInterface(window);
}
// If AutoJSAPI::Init() fails then either global is nullptr or not
// in a usable state.
AutoJSAPI jsapi;
if (!jsapi.Init(globalObject)) {
return;
}
aCallable(jsapi.cx(), globalObject);
}
void
ServiceWorkerContainer::DispatchMessage(RefPtr<ReceivedMessage> aMessage)
{
MOZ_ASSERT(NS_IsMainThread());
// When dispatching a message, either DOMContentLoaded has already
// been fired, or someone called startMessages() or set onmessage.
// Either way, a global object is supposed to be present. If it's
// not, we'd fail to initialize the JS API and exit.
RunWithJSContext([this, message = std::move(aMessage)](JSContext* const aCx,
nsIGlobalObject* const aGlobal) {
RootedDictionary<MessageEventInit> init(aCx);
if (!FillInMessageEventInit(aCx, aGlobal, *message, init)) {
// TODO: The spec requires us to fire a messageerror event here.
return;
}
RefPtr<MessageEvent> event =
MessageEvent::Constructor(this, NS_LITERAL_STRING("message"), init);
event->SetTrusted(true);
ErrorResult result;
DispatchEvent(*event, result);
if (result.Failed()) {
result.SuppressException();
}
});
}
namespace {
nsresult
FillInOriginNoSuffix(const ServiceWorkerDescriptor& aServiceWorker, nsString& aOrigin)
{
using mozilla::ipc::PrincipalInfoToPrincipal;
nsresult rv;
nsCOMPtr<nsIPrincipal> principal = PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo(), &rv);
if (NS_FAILED(rv) || !principal) {
return rv;
}
nsAutoCString originUTF8;
rv = principal->GetOriginNoSuffix(originUTF8);
if (NS_FAILED(rv)) {
return rv;
}
CopyUTF8toUTF16(originUTF8, aOrigin);
return NS_OK;
}
already_AddRefed<ServiceWorker>
GetOrCreateServiceWorkerWithoutWarnings(nsIGlobalObject* const aGlobal,
const ServiceWorkerDescriptor& aDescriptor)
{
// In child-intercept mode we have to verify that the registration
// exists in the current process. This exact check is also performed
// (indirectly) in nsIGlobalObject::GetOrCreateServiceWorker, but it
// also emits a warning when the registration is not present. To
// to avoid having too many warnings, we do a precheck here.
if (!ServiceWorkerParentInterceptEnabled()) {
const RefPtr<ServiceWorkerManager> serviceWorkerManager = ServiceWorkerManager::GetInstance();
if (!serviceWorkerManager) {
return nullptr;
}
const RefPtr<ServiceWorkerRegistrationInfo> registration =
serviceWorkerManager->GetRegistration(aDescriptor.PrincipalInfo(), aDescriptor.Scope());
if (!registration) {
return nullptr;
}
}
return aGlobal->GetOrCreateServiceWorker(aDescriptor).forget();
}
}
bool
ServiceWorkerContainer::FillInMessageEventInit(JSContext* const aCx,
nsIGlobalObject* const aGlobal,
ReceivedMessage& aMessage,
MessageEventInit& aInit)
{
ErrorResult result;
JS::Rooted<JS::Value> messageData(aCx);
aMessage.mClonedData.Read(aCx, &messageData, result);
if (result.Failed()) {
return false;
}
aInit.mData = messageData;
if (!aMessage.mClonedData.TakeTransferredPortsAsSequence(aInit.mPorts)) {
return false;
}
const nsresult rv = FillInOriginNoSuffix(aMessage.mServiceWorker, aInit.mOrigin);
if (NS_FAILED(rv)) {
return false;
}
const RefPtr<ServiceWorker> serviceWorkerInstance =
GetOrCreateServiceWorkerWithoutWarnings(aGlobal, aMessage.mServiceWorker);
if (serviceWorkerInstance) {
aInit.mSource.SetValue().SetAsServiceWorker() = serviceWorkerInstance;
}
return true;
}
} // namespace dom
} // namespace mozilla
......@@ -15,6 +15,8 @@ class nsIGlobalWindow;
namespace mozilla {
namespace dom {
class ClientPostMessageArgs;
struct MessageEventInit;
class Promise;
struct RegistrationOptions;
class ServiceWorker;
......@@ -64,7 +66,19 @@ public:
IMPL_EVENT_HANDLER(controllerchange)
IMPL_EVENT_HANDLER(error)
IMPL_EVENT_HANDLER(message)
// Almost a manual expansion of IMPL_EVENT_HANDLER(message), but
// with the additional StartMessages() when setting the handler, as
// required by the spec.
inline mozilla::dom::EventHandlerNonNull* GetOnmessage()
{
return GetEventHandler(nsGkAtoms::onmessage);
}
inline void SetOnmessage(mozilla::dom::EventHandlerNonNull* aCallback)
{
SetEventHandler(nsGkAtoms::onmessage, aCallback);
StartMessages();
}
static bool IsEnabled(JSContext* aCx, JSObject* aGlobal);
......@@ -89,6 +103,9 @@ public:
already_AddRefed<Promise>
GetRegistrations(ErrorResult& aRv);
void
StartMessages();
Promise*
GetReady(ErrorResult& aRv);
......@@ -104,6 +121,9 @@ public:
void
ControllerChanged(ErrorResult& aRv);
void
ReceiveMessage(const ClientPostMessageArgs& aArgs);
private:
ServiceWorkerContainer(nsIGlobalObject* aGlobal,
already_AddRefed<ServiceWorkerContainer::Inner> aInner);
......@@ -121,6 +141,27 @@ private:
GetGlobalIfValid(ErrorResult& aRv,
const std::function<void(nsIDocument*)>&& aStorageFailureCB = nullptr) const;
struct ReceivedMessage;
// Dispatch a Runnable that dispatches the given message on this
// object. When the owner of this object is a Window, the Runnable
// is dispatched on the corresponding TabGroup.
void
EnqueueReceivedMessageDispatch(RefPtr<ReceivedMessage> aMessage);
template <typename F>
void
RunWithJSContext(F&& aCallable);
void
DispatchMessage(RefPtr<ReceivedMessage> aMessage);
static bool
FillInMessageEventInit(JSContext* aCx,
nsIGlobalObject* aGlobal,
ReceivedMessage& aMessage,
MessageEventInit& aInit);
RefPtr<Inner> mInner;
// This only changes when a worker hijacks everything in its scope by calling
......@@ -129,6 +170,13 @@ private:
RefPtr<Promise> mReadyPromise;
MozPromiseRequestHolder<ServiceWorkerRegistrationPromise> mReadyPromiseHolder;
// Set after StartMessages() has been called.
bool mMessagesStarted = false;
// Queue holding messages posted from service worker as long as
// StartMessages() hasn't been called.
nsTArray<RefPtr<ReceivedMessage>> mPendingMessages;
};
} // namespace dom
......
......@@ -4,7 +4,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/.
*
* The origin of this IDL file is
* http://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html
* https://w3c.github.io/ServiceWorker/#serviceworkercontainer
*
*/
......@@ -29,6 +29,8 @@ interface ServiceWorkerContainer : EventTarget {
[NewObject]
Promise<sequence<ServiceWorkerRegistration>> getRegistrations();
void startMessages();
attribute EventHandler oncontrollerchange;
attribute EventHandler onerror;
attribute EventHandler onmessage;
......
......@@ -21,7 +21,8 @@
}
promise_test(async t => {
const scope = SCOPE + "?q=aborted-not-intercepted";
const suffix = "?q=aborted-not-intercepted";
const scope = SCOPE + suffix;
await setupRegistration(t, scope);
const iframe = await with_iframe(scope);
add_completion_callback(_ => iframe.remove());
......@@ -33,8 +34,13 @@
const nextData = new Promise(resolve => {
w.navigator.serviceWorker.addEventListener('message', function once(event) {
w.navigator.serviceWorker.removeEventListener('message', once);
resolve(event.data);
// The message triggered by the iframe's document's fetch
// request cannot get dispatched by the time we add the event
// listener, so we have to guard against it.
if (!event.data.endsWith(suffix)) {
w.navigator.serviceWorker.removeEventListener('message', once);
resolve(event.data);
}
})
});
......
self.addEventListener('fetch', function(event) {
if (event.request.url.includes('dummy')) {
if (event.request.url.includes('dummy.html?')) {
event.waitUntil(async function() {
let destination = new URL(event.request.url).searchParams.get("dest");
var result = "FAIL";
......
......@@ -51,4 +51,208 @@ promise_test(t => {
})
.then(e => { assert_equals(e.data, 'quit'); });
}, 'postMessage from ServiceWorker to Client.');
// This function creates a message listener that captures all messages
// sent to this window and matches them with corresponding requests.
// This frees test code from having to use clunky constructs just to
// avoid race conditions, since the relative order of message and
// request arrival doesn't matter.
function create_message_listener(t) {
const listener = {
messages: new Set(),
requests: new Set(),
waitFor: function(predicate) {
for (const event of this.messages) {
// If a message satisfying the predicate has already
// arrived, it gets matched to this request.
if (predicate(event)) {
this.messages.delete(event);
return Promise.resolve(event);
}
}
// If no match was found, the request is stored and a
// promise is returned.
const request = { predicate };
const promise = new Promise(resolve => request.resolve = resolve);
this.requests.add(request);
return promise;
}
};
window.onmessage = t.step_func(event => {
for (const request of listener.requests) {
// If the new message matches a stored request's
// predicate, the request's promise is resolved with this
// message.
if (request.predicate(event)) {
listener.requests.delete(request);
request.resolve(event);
return;
}
};
// No outstanding request for this message, store it in case
// it's requested later.
listener.messages.add(event);
});
return listener;
}
async function service_worker_register_and_activate(t, script, scope) {
const registration = await service_worker_unregister_and_register(t, script, scope);