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
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -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();
  }
+8 −116
Original line number Diff line number Diff line
@@ -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();
  }

  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();
  // 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();
  }

  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>
+202 −0
Original line number Diff line number Diff line
@@ -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
+49 −1
Original line number Diff line number Diff line
@@ -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
+3 −1
Original line number Diff line number Diff line
@@ -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;
Loading