dom/serviceworkers/ServiceWorkerContainer.cpp
author Boris Zbarsky <bzbarsky@mit.edu>
Sat, 08 Jun 2019 21:26:12 +0000
changeset 537902 89b5e0496b75de48a77f44ab25953bb54b2e0a4b
parent 535374 fffc348e55232b77c9bf026d5b3bd00012961113
permissions -rw-r--r--
Bug 1557793 part 3. Change the signatures of various nsContentUtils localization methods to play nicer with the new stringbundle API. r=smaug Differential Revision: https://phabricator.services.mozilla.com/D34201

/* -*- 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 "ServiceWorkerContainer.h"

#include "nsContentPolicyUtils.h"
#include "nsContentSecurityManager.h"
#include "nsContentUtils.h"
#include "mozilla/dom/Document.h"
#include "nsIServiceWorkerManager.h"
#include "nsIScriptError.h"
#include "nsThreadUtils.h"
#include "nsIURL.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs.h"

#include "nsCycleCollectionParticipant.h"
#include "nsServiceManagerUtils.h"

#include "mozilla/LoadInfo.h"
#include "mozilla/StorageAccess.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"
#include "ServiceWorkerContainerImpl.h"
#include "ServiceWorkerRegistration.h"
#include "ServiceWorkerUtils.h"

// This is defined to something else on Windows
#ifdef DispatchMessage
#  undef DispatchMessage
#endif

namespace mozilla {
namespace dom {

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerContainer)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

NS_IMPL_ADDREF_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper)

NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper,
                                   mControllerWorker, mReadyPromise)

namespace {

bool IsInPrivateBrowsing(JSContext* const aCx) {
  if (const nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx)) {
    if (const nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull()) {
      return principal->GetPrivateBrowsingId() > 0;
    }
  }
  return false;
}

bool IsServiceWorkersTestingEnabledInWindow(JSObject* const aGlobal) {
  if (const nsCOMPtr<nsPIDOMWindowInner> innerWindow =
          Navigator::GetWindowFromGlobal(aGlobal)) {
    if (const nsCOMPtr<nsPIDOMWindowOuter> outerWindow =
            innerWindow->GetOuterWindow()) {
      return outerWindow->GetServiceWorkersTestingEnabled();
    }
  }
  return false;
}

}  // namespace

/* static */
bool ServiceWorkerContainer::IsEnabled(JSContext* aCx, JSObject* aGlobal) {
  MOZ_ASSERT(NS_IsMainThread());

  JS::Rooted<JSObject*> global(aCx, aGlobal);

  if (!StaticPrefs::dom_serviceWorkers_enabled()) {
    return false;
  }

  if (IsInPrivateBrowsing(aCx)) {
    return false;
  }

  if (IsSecureContextOrObjectIsFromSecureContext(aCx, global)) {
    return true;
  }

  const bool isTestingEnabledInWindow =
      IsServiceWorkersTestingEnabledInWindow(global);
  const bool isTestingEnabledByPref =
      StaticPrefs::dom_serviceWorkers_testing_enabled();
  const bool isTestingEnabled =
      isTestingEnabledByPref || isTestingEnabledInWindow;

  return isTestingEnabled;
}

// static
already_AddRefed<ServiceWorkerContainer> ServiceWorkerContainer::Create(
    nsIGlobalObject* aGlobal) {
  RefPtr<Inner> inner;
  if (ServiceWorkerParentInterceptEnabled()) {
    inner = new RemoteServiceWorkerContainerImpl();
  } else {
    inner = new ServiceWorkerContainerImpl();
  }
  NS_ENSURE_TRUE(inner, nullptr);

  RefPtr<ServiceWorkerContainer> ref =
      new ServiceWorkerContainer(aGlobal, inner.forget());
  return ref.forget();
}

ServiceWorkerContainer::ServiceWorkerContainer(
    nsIGlobalObject* aGlobal,
    already_AddRefed<ServiceWorkerContainer::Inner> aInner)
    : DOMEventTargetHelper(aGlobal), mInner(aInner) {
  mInner->AddContainer(this);
  Maybe<ServiceWorkerDescriptor> controller = aGlobal->GetController();
  if (controller.isSome()) {
    mControllerWorker = aGlobal->GetOrCreateServiceWorker(controller.ref());
  }
}

ServiceWorkerContainer::~ServiceWorkerContainer() {
  mInner->RemoveContainer(this);
}

void ServiceWorkerContainer::DisconnectFromOwner() {
  mControllerWorker = nullptr;
  mReadyPromise = nullptr;
  DOMEventTargetHelper::DisconnectFromOwner();
}

void ServiceWorkerContainer::ControllerChanged(ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> go = GetParentObject();
  if (!go) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }
  mControllerWorker = go->GetOrCreateServiceWorker(go->GetController().ref());
  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) {
  return ServiceWorkerContainer_Binding::Wrap(aCx, this, aGivenProto);
}

namespace {

already_AddRefed<nsIURI> GetBaseURIFromGlobal(nsIGlobalObject* aGlobal,
                                              ErrorResult& aRv) {
  // It would be nice not to require a window here, but right
  // now we don't have a great way to get the base URL just
  // from the nsIGlobalObject.
  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal);
  if (!window) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  Document* doc = window->GetExtantDoc();
  if (!doc) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  nsCOMPtr<nsIURI> baseURI = doc->GetDocBaseURI();
  if (!baseURI) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  return baseURI.forget();
}

}  // anonymous namespace

already_AddRefed<Promise> ServiceWorkerContainer::Register(
    const nsAString& aScriptURL, const RegistrationOptions& aOptions,
    ErrorResult& aRv) {
  // Note, we can't use GetGlobalIfValid() from the start here.  If we
  // hit a storage failure we want to log a message with the final
  // scope string we put together below.
  nsIGlobalObject* global = GetParentObject();
  if (!global) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  Maybe<ClientInfo> clientInfo = global->GetClientInfo();
  if (clientInfo.isNothing()) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  nsCOMPtr<nsIURI> baseURI = GetBaseURIFromGlobal(global, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  nsresult rv;
  nsCOMPtr<nsIURI> scriptURI;
  rv = NS_NewURI(getter_AddRefs(scriptURI), aScriptURL, nullptr, baseURI);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    aRv.ThrowTypeError<MSG_INVALID_URL>(aScriptURL);
    return nullptr;
  }

  // In ServiceWorkerContainer.register() the scope argument is parsed against
  // different base URLs depending on whether it was passed or not.
  nsCOMPtr<nsIURI> scopeURI;

  // Step 4. If none passed, parse against script's URL
  if (!aOptions.mScope.WasPassed()) {
    NS_NAMED_LITERAL_STRING(defaultScope, "./");
    rv = NS_NewURI(getter_AddRefs(scopeURI), defaultScope, nullptr, scriptURI);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      nsAutoCString spec;
      scriptURI->GetSpec(spec);
      NS_ConvertUTF8toUTF16 wSpec(spec);
      aRv.ThrowTypeError<MSG_INVALID_SCOPE>(defaultScope, wSpec);
      return nullptr;
    }
  } else {
    // Step 5. Parse against entry settings object's base URL.
    rv = NS_NewURI(getter_AddRefs(scopeURI), aOptions.mScope.Value(), nullptr,
                   baseURI);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      nsIURI* uri = baseURI ? baseURI : scriptURI;
      nsAutoCString spec;
      uri->GetSpec(spec);
      NS_ConvertUTF8toUTF16 wSpec(spec);
      aRv.ThrowTypeError<MSG_INVALID_SCOPE>(aOptions.mScope.Value(), wSpec);
      return nullptr;
    }
  }

  // Strip the any ref from both the script and scope URLs.
  nsCOMPtr<nsIURI> cloneWithoutRef;
  aRv = NS_GetURIWithoutRef(scriptURI, getter_AddRefs(cloneWithoutRef));
  if (aRv.Failed()) {
    return nullptr;
  }
  scriptURI = cloneWithoutRef.forget();

  aRv = NS_GetURIWithoutRef(scopeURI, getter_AddRefs(cloneWithoutRef));
  if (aRv.Failed()) {
    return nullptr;
  }
  scopeURI = cloneWithoutRef.forget();

  aRv = ServiceWorkerScopeAndScriptAreValid(clientInfo.ref(), scopeURI,
                                            scriptURI);
  if (aRv.Failed()) {
    return nullptr;
  }

  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global);
  if (!window) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  Document* doc = window->GetExtantDoc();
  if (!doc) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  // The next section of code executes an NS_CheckContentLoadPolicy()
  // check.  This is necessary to enforce the CSP of the calling client.
  // Currently this requires an Document.  Once bug 965637 lands we
  // should try to move this into ServiceWorkerScopeAndScriptAreValid()
  // using the ClientInfo instead of doing a window-specific check here.
  // See bug 1455077 for further investigation.
  nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new mozilla::net::LoadInfo(
      doc->NodePrincipal(),  // loading principal
      doc->NodePrincipal(),  // triggering principal
      doc,                   // loading node
      nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
      nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER);

  // Check content policy.
  int16_t decision = nsIContentPolicy::ACCEPT;
  rv = NS_CheckContentLoadPolicy(scriptURI, secCheckLoadInfo,
                                 NS_LITERAL_CSTRING("application/javascript"),
                                 &decision);
  if (NS_FAILED(rv)) {
    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
    return nullptr;
  }
  if (NS_WARN_IF(decision != nsIContentPolicy::ACCEPT)) {
    aRv.Throw(NS_ERROR_CONTENT_BLOCKED);
    return nullptr;
  }

  // Get the string representation for both the script and scope since
  // we sanitized them above.
  nsCString cleanedScopeURL;
  aRv = scopeURI->GetSpec(cleanedScopeURL);
  if (aRv.Failed()) {
    return nullptr;
  }

  nsCString cleanedScriptURL;
  aRv = scriptURI->GetSpec(cleanedScriptURL);
  if (aRv.Failed()) {
    return nullptr;
  }

  // Verify that the global is valid and has permission to store
  // data.  We perform this late so that we can report the final
  // scope URL in any error message.
  Unused << GetGlobalIfValid(aRv, [&](Document* aDoc) {
    AutoTArray<nsString, 1> param;
    CopyUTF8toUTF16(cleanedScopeURL, *param.AppendElement());
    nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
                                    NS_LITERAL_CSTRING("Service Workers"), aDoc,
                                    nsContentUtils::eDOM_PROPERTIES,
                                    "ServiceWorkerRegisterStorageError", param);
  });

  window->NoteCalledRegisterForServiceWorkerScope(cleanedScopeURL);

  RefPtr<Promise> outer =
      Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
  if (aRv.Failed()) {
    return nullptr;
  }

  RefPtr<ServiceWorkerContainer> self = this;

  mInner->Register(
      clientInfo.ref(), cleanedScopeURL, cleanedScriptURL,
      aOptions.mUpdateViaCache,
      [self, outer](const ServiceWorkerRegistrationDescriptor& aDesc) {
        ErrorResult rv;
        nsIGlobalObject* global = self->GetGlobalIfValid(rv);
        if (rv.Failed()) {
          outer->MaybeReject(rv);
          return;
        }
        RefPtr<ServiceWorkerRegistration> reg =
            global->GetOrCreateServiceWorkerRegistration(aDesc);
        outer->MaybeResolve(reg);
      },
      [outer](ErrorResult& aRv) { outer->MaybeReject(aRv); });

  return outer.forget();
}

already_AddRefed<ServiceWorker> ServiceWorkerContainer::GetController() {
  RefPtr<ServiceWorker> ref = mControllerWorker;
  return ref.forget();
}

already_AddRefed<Promise> ServiceWorkerContainer::GetRegistrations(
    ErrorResult& aRv) {
  nsIGlobalObject* global = GetGlobalIfValid(aRv, [](Document* aDoc) {
    nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
                                    NS_LITERAL_CSTRING("Service Workers"), aDoc,
                                    nsContentUtils::eDOM_PROPERTIES,
                                    "ServiceWorkerGetRegistrationStorageError");
  });
  if (aRv.Failed()) {
    return nullptr;
  }

  Maybe<ClientInfo> clientInfo = global->GetClientInfo();
  if (clientInfo.isNothing()) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  RefPtr<Promise> outer =
      Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
  if (aRv.Failed()) {
    return nullptr;
  }

  RefPtr<ServiceWorkerContainer> self = this;

  mInner->GetRegistrations(
      clientInfo.ref(),
      [self,
       outer](const nsTArray<ServiceWorkerRegistrationDescriptor>& aDescList) {
        ErrorResult rv;
        nsIGlobalObject* global = self->GetGlobalIfValid(rv);
        if (rv.Failed()) {
          outer->MaybeReject(rv);
          return;
        }
        nsTArray<RefPtr<ServiceWorkerRegistration>> regList;
        for (auto& desc : aDescList) {
          RefPtr<ServiceWorkerRegistration> reg =
              global->GetOrCreateServiceWorkerRegistration(desc);
          if (reg) {
            regList.AppendElement(std::move(reg));
          }
        }
        outer->MaybeResolve(regList);
      },
      [self, outer](ErrorResult& aRv) { outer->MaybeReject(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) {
  nsIGlobalObject* global = GetGlobalIfValid(aRv, [](Document* aDoc) {
    nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
                                    NS_LITERAL_CSTRING("Service Workers"), aDoc,
                                    nsContentUtils::eDOM_PROPERTIES,
                                    "ServiceWorkerGetRegistrationStorageError");
  });
  if (aRv.Failed()) {
    return nullptr;
  }

  Maybe<ClientInfo> clientInfo = global->GetClientInfo();
  if (clientInfo.isNothing()) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  nsCOMPtr<nsIURI> baseURI = GetBaseURIFromGlobal(global, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  nsCOMPtr<nsIURI> uri;
  aRv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, baseURI);
  if (aRv.Failed()) {
    return nullptr;
  }

  nsCString spec;
  aRv = uri->GetSpec(spec);
  if (aRv.Failed()) {
    return nullptr;
  }

  RefPtr<Promise> outer =
      Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
  if (aRv.Failed()) {
    return nullptr;
  }

  RefPtr<ServiceWorkerContainer> self = this;

  mInner->GetRegistration(
      clientInfo.ref(), spec,
      [self, outer](const ServiceWorkerRegistrationDescriptor& aDescriptor) {
        ErrorResult rv;
        nsIGlobalObject* global = self->GetGlobalIfValid(rv);
        if (rv.Failed()) {
          outer->MaybeReject(rv);
          return;
        }
        RefPtr<ServiceWorkerRegistration> reg =
            global->GetOrCreateServiceWorkerRegistration(aDescriptor);
        outer->MaybeResolve(reg);
      },
      [self, outer](ErrorResult& aRv) {
        if (!aRv.Failed()) {
          Unused << self->GetGlobalIfValid(aRv);
          if (!aRv.Failed()) {
            outer->MaybeResolveWithUndefined();
            return;
          }
        }
        outer->MaybeReject(aRv);
      });

  return outer.forget();
}

Promise* ServiceWorkerContainer::GetReady(ErrorResult& aRv) {
  if (mReadyPromise) {
    return mReadyPromise;
  }

  nsIGlobalObject* global = GetGlobalIfValid(aRv);
  if (aRv.Failed()) {
    return nullptr;
  }
  MOZ_DIAGNOSTIC_ASSERT(global);

  Maybe<ClientInfo> clientInfo(global->GetClientInfo());
  if (clientInfo.isNothing()) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  mReadyPromise =
      Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
  if (aRv.Failed()) {
    return nullptr;
  }

  RefPtr<ServiceWorkerContainer> self = this;
  RefPtr<Promise> outer = mReadyPromise;

  mInner->GetReady(
      clientInfo.ref(),
      [self, outer](const ServiceWorkerRegistrationDescriptor& aDescriptor) {
        ErrorResult rv;
        nsIGlobalObject* global = self->GetGlobalIfValid(rv);
        if (rv.Failed()) {
          outer->MaybeReject(rv);
          return;
        }
        RefPtr<ServiceWorkerRegistration> reg =
            global->GetOrCreateServiceWorkerRegistration(aDescriptor);
        NS_ENSURE_TRUE_VOID(reg);

        // Don't resolve the ready promise until the registration has
        // reached the right version.  This ensures that the active
        // worker property is set correctly on the registration.
        reg->WhenVersionReached(
            aDescriptor.Version(),
            [outer, reg](bool aResult) { outer->MaybeResolve(reg); });
      },
      [self, outer](ErrorResult& aRv) { outer->MaybeReject(aRv); });

  return mReadyPromise;
}

// Testing only.
void ServiceWorkerContainer::GetScopeForUrl(const nsAString& aUrl,
                                            nsString& aScope,
                                            ErrorResult& aRv) {
  nsCOMPtr<nsIServiceWorkerManager> swm =
      mozilla::services::GetServiceWorkerManager();
  if (!swm) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
  if (NS_WARN_IF(!window)) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }

  nsCOMPtr<Document> doc = window->GetExtantDoc();
  if (NS_WARN_IF(!doc)) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return;
  }

  aRv = swm->GetScopeForUrl(doc->NodePrincipal(), aUrl, aScope);
}

nsIGlobalObject* ServiceWorkerContainer::GetGlobalIfValid(
    ErrorResult& aRv,
    const std::function<void(Document*)>&& aStorageFailureCB) const {
  // For now we require a window since ServiceWorkerContainer is
  // not exposed on worker globals yet.  The main thing we need
  // to fix here to support that is the storage access check via
  // the nsIGlobalObject.
  nsPIDOMWindowInner* window = GetOwner();
  if (NS_WARN_IF(!window)) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  nsCOMPtr<Document> doc = window->GetExtantDoc();
  if (NS_WARN_IF(!doc)) {
    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
    return nullptr;
  }

  // Don't allow a service worker to access service worker registrations
  // from a window with storage disabled.  If these windows can access
  // the registration it increases the chance they can bypass the storage
  // block via postMessage(), etc.
  auto storageAllowed = StorageAllowedForWindow(window);
  if (NS_WARN_IF(storageAllowed != StorageAccess::eAllow)) {
    if (aStorageFailureCB) {
      aStorageFailureCB(doc);
    }
    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
    return nullptr;
  }

  // Don't allow service workers when the document is chrome.
  if (NS_WARN_IF(nsContentUtils::IsSystemPrincipal(doc->NodePrincipal()))) {
    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
    return nullptr;
  }

  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) {
    ErrorResult result;
    bool deserializationFailed = false;
    RootedDictionary<MessageEventInit> init(aCx);
    if (!FillInMessageEventInit(aCx, aGlobal, *message, init, result)) {
      deserializationFailed = result.ErrorCodeIs(NS_ERROR_DOM_DATA_CLONE_ERR);
      MOZ_ASSERT_IF(deserializationFailed, init.mData.isNull());
      MOZ_ASSERT_IF(deserializationFailed, init.mPorts.IsEmpty());
      MOZ_ASSERT_IF(deserializationFailed, !init.mOrigin.IsEmpty());
      MOZ_ASSERT_IF(deserializationFailed, !init.mSource.IsNull());

      if (!deserializationFailed && result.MaybeSetPendingException(aCx)) {
        return;
      }
    }

    RefPtr<MessageEvent> event = MessageEvent::Constructor(
        this,
        deserializationFailed ? NS_LITERAL_STRING("messageerror")
                              : NS_LITERAL_STRING("message"),
        init);
    event->SetTrusted(true);

    result = NS_OK;
    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();
}

}  // namespace

bool ServiceWorkerContainer::FillInMessageEventInit(
    JSContext* const aCx, nsIGlobalObject* const aGlobal,
    ReceivedMessage& aMessage, MessageEventInit& aInit, ErrorResult& aRv) {
  // Determining the source and origin should preceed attempting deserialization
  // because on a "messageerror" event (i.e. when deserialization fails), the
  // dispatched message needs to contain such an origin and source, per spec:
  //
  // "If this throws an exception, catch it, fire an event named messageerror
  //  at destination, using MessageEvent, with the origin attribute initialized
  //  to origin and the source attribute initialized to source, and then abort
  //  these steps." - 6.4 of postMessage
  //  See: https://w3c.github.io/ServiceWorker/#service-worker-postmessage
  const RefPtr<ServiceWorker> serviceWorkerInstance =
      GetOrCreateServiceWorkerWithoutWarnings(aGlobal, aMessage.mServiceWorker);
  if (serviceWorkerInstance) {
    aInit.mSource.SetValue().SetAsServiceWorker() = serviceWorkerInstance;
  }

  const nsresult rv =
      FillInOriginNoSuffix(aMessage.mServiceWorker, aInit.mOrigin);
  if (NS_FAILED(rv)) {
    return false;
  }

  JS::Rooted<JS::Value> messageData(aCx);
  aMessage.mClonedData.Read(aCx, &messageData, aRv);
  if (aRv.Failed()) {
    return false;
  }

  aInit.mData = messageData;

  if (!aMessage.mClonedData.TakeTransferredPortsAsSequence(aInit.mPorts)) {
    xpc::Throw(aCx, NS_ERROR_OUT_OF_MEMORY);
    return false;
  }

  return true;
}

}  // namespace dom
}  // namespace mozilla