diff options
Diffstat (limited to 'dom/workers/ServiceWorkerManager.cpp')
-rw-r--r-- | dom/workers/ServiceWorkerManager.cpp | 3969 |
1 files changed, 3969 insertions, 0 deletions
diff --git a/dom/workers/ServiceWorkerManager.cpp b/dom/workers/ServiceWorkerManager.cpp new file mode 100644 index 000000000..a66df0731 --- /dev/null +++ b/dom/workers/ServiceWorkerManager.cpp @@ -0,0 +1,3969 @@ +/* -*- 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 "ServiceWorkerManager.h" + +#include "nsAutoPtr.h" +#include "nsIConsoleService.h" +#include "nsIDOMEventTarget.h" +#include "nsIDocument.h" +#include "nsIScriptSecurityManager.h" +#include "nsIStreamLoader.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsINetworkInterceptController.h" +#include "nsIMutableArray.h" +#include "nsIScriptError.h" +#include "nsISimpleEnumerator.h" +#include "nsITimer.h" +#include "nsIUploadChannel2.h" +#include "nsPIDOMWindow.h" +#include "nsScriptLoader.h" +#include "nsServiceManagerUtils.h" +#include "nsDebug.h" +#include "nsISupportsPrimitives.h" + +#include "jsapi.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/LoadContext.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/DOMError.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/Headers.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/NotificationEvent.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/Unused.h" +#include "mozilla/EnumSet.h" + +#include "nsContentPolicyUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsTArray.h" + +#include "RuntimeService.h" +#include "ServiceWorker.h" +#include "ServiceWorkerClient.h" +#include "ServiceWorkerContainer.h" +#include "ServiceWorkerInfo.h" +#include "ServiceWorkerJobQueue.h" +#include "ServiceWorkerManagerChild.h" +#include "ServiceWorkerPrivate.h" +#include "ServiceWorkerRegisterJob.h" +#include "ServiceWorkerRegistrar.h" +#include "ServiceWorkerRegistration.h" +#include "ServiceWorkerScriptCache.h" +#include "ServiceWorkerEvents.h" +#include "ServiceWorkerUnregisterJob.h" +#include "ServiceWorkerUpdateJob.h" +#include "SharedWorker.h" +#include "WorkerInlines.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" + +#ifdef PostMessage +#undef PostMessage +#endif + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +BEGIN_WORKERS_NAMESPACE + +#define PURGE_DOMAIN_DATA "browser:purge-domain-data" +#define PURGE_SESSION_HISTORY "browser:purge-session-history" +#define CLEAR_ORIGIN_DATA "clear-origin-attributes-data" + +static_assert(nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN == static_cast<uint32_t>(RequestMode::Same_origin), + "RequestMode enumeration value should match Necko CORS mode value."); +static_assert(nsIHttpChannelInternal::CORS_MODE_NO_CORS == static_cast<uint32_t>(RequestMode::No_cors), + "RequestMode enumeration value should match Necko CORS mode value."); +static_assert(nsIHttpChannelInternal::CORS_MODE_CORS == static_cast<uint32_t>(RequestMode::Cors), + "RequestMode enumeration value should match Necko CORS mode value."); +static_assert(nsIHttpChannelInternal::CORS_MODE_NAVIGATE == static_cast<uint32_t>(RequestMode::Navigate), + "RequestMode enumeration value should match Necko CORS mode value."); + +static_assert(nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW == static_cast<uint32_t>(RequestRedirect::Follow), + "RequestRedirect enumeration value should make Necko Redirect mode value."); +static_assert(nsIHttpChannelInternal::REDIRECT_MODE_ERROR == static_cast<uint32_t>(RequestRedirect::Error), + "RequestRedirect enumeration value should make Necko Redirect mode value."); +static_assert(nsIHttpChannelInternal::REDIRECT_MODE_MANUAL == static_cast<uint32_t>(RequestRedirect::Manual), + "RequestRedirect enumeration value should make Necko Redirect mode value."); +static_assert(3 == static_cast<uint32_t>(RequestRedirect::EndGuard_), + "RequestRedirect enumeration value should make Necko Redirect mode value."); + +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT == static_cast<uint32_t>(RequestCache::Default), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE == static_cast<uint32_t>(RequestCache::No_store), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD == static_cast<uint32_t>(RequestCache::Reload), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE == static_cast<uint32_t>(RequestCache::No_cache), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE == static_cast<uint32_t>(RequestCache::Force_cache), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED == static_cast<uint32_t>(RequestCache::Only_if_cached), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(6 == static_cast<uint32_t>(RequestCache::EndGuard_), + "RequestCache enumeration value should match Necko Cache mode value."); + +static StaticRefPtr<ServiceWorkerManager> gInstance; + +struct ServiceWorkerManager::RegistrationDataPerPrincipal final +{ + // Ordered list of scopes for glob matching. + // Each entry is an absolute URL representing the scope. + // Each value of the hash table is an array of an absolute URLs representing + // the scopes. + // + // An array is used for now since the number of controlled scopes per + // domain is expected to be relatively low. If that assumption was proved + // wrong this should be replaced with a better structure to avoid the + // memmoves associated with inserting stuff in the middle of the array. + nsTArray<nsCString> mOrderedScopes; + + // Scope to registration. + // The scope should be a fully qualified valid URL. + nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerRegistrationInfo> mInfos; + + // Maps scopes to job queues. + nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerJobQueue> mJobQueues; + + // Map scopes to scheduled update timers. + nsInterfaceHashtable<nsCStringHashKey, nsITimer> mUpdateTimers; +}; + +namespace { + +nsresult +PopulateRegistrationData(nsIPrincipal* aPrincipal, + const ServiceWorkerRegistrationInfo* aRegistration, + ServiceWorkerRegistrationData& aData) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aRegistration); + + if (NS_WARN_IF(!BasePrincipal::Cast(aPrincipal)->IsCodebasePrincipal())) { + return NS_ERROR_FAILURE; + } + + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aData.principal()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aData.scope() = aRegistration->mScope; + + RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); + if (NS_WARN_IF(!newest)) { + return NS_ERROR_FAILURE; + } + + if (aRegistration->GetActive()) { + aData.currentWorkerURL() = aRegistration->GetActive()->ScriptSpec(); + aData.cacheName() = aRegistration->GetActive()->CacheName(); + } + + return NS_OK; +} + +class TeardownRunnable final : public Runnable +{ +public: + explicit TeardownRunnable(ServiceWorkerManagerChild* aActor) + : mActor(aActor) + { + MOZ_ASSERT(mActor); + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(mActor); + mActor->SendShutdown(); + return NS_OK; + } + +private: + ~TeardownRunnable() {} + + RefPtr<ServiceWorkerManagerChild> mActor; +}; + +} // namespace + +////////////////////////// +// ServiceWorkerManager // +////////////////////////// + +NS_IMPL_ADDREF(ServiceWorkerManager) +NS_IMPL_RELEASE(ServiceWorkerManager) + +NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager) + NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager) + NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIServiceWorkerManager) +NS_INTERFACE_MAP_END + +ServiceWorkerManager::ServiceWorkerManager() + : mActor(nullptr) + , mShuttingDown(false) +{ +} + +ServiceWorkerManager::~ServiceWorkerManager() +{ + // The map will assert if it is not empty when destroyed. + mRegistrationInfos.Clear(); + MOZ_ASSERT(!mActor); +} + +void +ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) +{ + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + DebugOnly<nsresult> rv; + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false /* ownsWeak */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (XRE_IsParentProcess()) { + MOZ_DIAGNOSTIC_ASSERT(aRegistrar); + + nsTArray<ServiceWorkerRegistrationData> data; + aRegistrar->GetRegistrations(data); + LoadRegistrations(data); + + if (obs) { + DebugOnly<nsresult> rv; + rv = obs->AddObserver(this, PURGE_SESSION_HISTORY, false /* ownsWeak */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = obs->AddObserver(this, PURGE_DOMAIN_DATA, false /* ownsWeak */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = obs->AddObserver(this, CLEAR_ORIGIN_DATA, false /* ownsWeak */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + if (!BackgroundChild::GetOrCreateForCurrentThread(this)) { + // Make sure to do this last as our failure cleanup expects Init() to have + // executed. + ActorFailed(); + } +} + +void +ServiceWorkerManager::MaybeStartShutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (mShuttingDown) { + return; + } + + mShuttingDown = true; + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + for (auto it2 = it1.UserData()->mUpdateTimers.Iter(); !it2.Done(); it2.Next()) { + nsCOMPtr<nsITimer> timer = it2.UserData(); + timer->Cancel(); + } + it1.UserData()->mUpdateTimers.Clear(); + + for (auto it2 = it1.UserData()->mJobQueues.Iter(); !it2.Done(); it2.Next()) { + RefPtr<ServiceWorkerJobQueue> queue = it2.UserData(); + queue->CancelAll(); + } + it1.UserData()->mJobQueues.Clear(); + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + + if (XRE_IsParentProcess()) { + obs->RemoveObserver(this, PURGE_SESSION_HISTORY); + obs->RemoveObserver(this, PURGE_DOMAIN_DATA); + obs->RemoveObserver(this, CLEAR_ORIGIN_DATA); + } + } + + mPendingOperations.Clear(); + + if (!mActor) { + return; + } + + mActor->ManagerShuttingDown(); + + RefPtr<TeardownRunnable> runnable = new TeardownRunnable(mActor); + nsresult rv = NS_DispatchToMainThread(runnable); + Unused << NS_WARN_IF(NS_FAILED(rv)); + mActor = nullptr; +} + +class ServiceWorkerResolveWindowPromiseOnRegisterCallback final : public ServiceWorkerJob::Callback +{ + RefPtr<nsPIDOMWindowInner> mWindow; + // The promise "returned" by the call to Update up to + // navigator.serviceWorker.register(). + RefPtr<Promise> mPromise; + + ~ServiceWorkerResolveWindowPromiseOnRegisterCallback() + {} + + virtual void + JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override + { + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + + if (aStatus.Failed()) { + mPromise->MaybeReject(aStatus); + return; + } + + MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Register); + RefPtr<ServiceWorkerRegisterJob> registerJob = + static_cast<ServiceWorkerRegisterJob*>(aJob); + RefPtr<ServiceWorkerRegistrationInfo> reg = registerJob->GetRegistration(); + + RefPtr<ServiceWorkerRegistration> swr = + mWindow->GetServiceWorkerRegistration(NS_ConvertUTF8toUTF16(reg->mScope)); + mPromise->MaybeResolve(swr); + } + +public: + ServiceWorkerResolveWindowPromiseOnRegisterCallback(nsPIDOMWindowInner* aWindow, + Promise* aPromise) + : mWindow(aWindow) + , mPromise(aPromise) + {} + + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerResolveWindowPromiseOnRegisterCallback, override) +}; + +namespace { + +class PropagateSoftUpdateRunnable final : public Runnable +{ +public: + PropagateSoftUpdateRunnable(const PrincipalOriginAttributes& aOriginAttributes, + const nsAString& aScope) + : mOriginAttributes(aOriginAttributes) + , mScope(aScope) + {} + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->PropagateSoftUpdate(mOriginAttributes, mScope); + } + + return NS_OK; + } + +private: + ~PropagateSoftUpdateRunnable() + {} + + const PrincipalOriginAttributes mOriginAttributes; + const nsString mScope; +}; + +class PropagateUnregisterRunnable final : public Runnable +{ +public: + PropagateUnregisterRunnable(nsIPrincipal* aPrincipal, + nsIServiceWorkerUnregisterCallback* aCallback, + const nsAString& aScope) + : mPrincipal(aPrincipal) + , mCallback(aCallback) + , mScope(aScope) + { + MOZ_ASSERT(aPrincipal); + } + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->PropagateUnregister(mPrincipal, mCallback, mScope); + } + + return NS_OK; + } + +private: + ~PropagateUnregisterRunnable() + {} + + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback; + const nsString mScope; +}; + +class RemoveRunnable final : public Runnable +{ +public: + explicit RemoveRunnable(const nsACString& aHost) + {} + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->Remove(mHost); + } + + return NS_OK; + } + +private: + ~RemoveRunnable() + {} + + const nsCString mHost; +}; + +class PropagateRemoveRunnable final : public Runnable +{ +public: + explicit PropagateRemoveRunnable(const nsACString& aHost) + {} + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->PropagateRemove(mHost); + } + + return NS_OK; + } + +private: + ~PropagateRemoveRunnable() + {} + + const nsCString mHost; +}; + +class PropagateRemoveAllRunnable final : public Runnable +{ +public: + PropagateRemoveAllRunnable() + {} + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->PropagateRemoveAll(); + } + + return NS_OK; + } + +private: + ~PropagateRemoveAllRunnable() + {} +}; + +} // namespace + +// This function implements parts of the step 3 of the following algorithm: +// https://w3c.github.io/webappsec/specs/powerfulfeatures/#settings-secure +static bool +IsFromAuthenticatedOrigin(nsIDocument* aDoc) +{ + MOZ_ASSERT(aDoc); + nsCOMPtr<nsIDocument> doc(aDoc); + nsCOMPtr<nsIContentSecurityManager> csm = do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID); + if (NS_WARN_IF(!csm)) { + return false; + } + + while (doc && !nsContentUtils::IsChromeDoc(doc)) { + bool trustworthyOrigin = false; + + // The origin of the document may be different from the document URI + // itself. Check the principal, not the document URI itself. + nsCOMPtr<nsIPrincipal> documentPrincipal = doc->NodePrincipal(); + + // The check for IsChromeDoc() above should mean we never see a system + // principal inside the loop. + MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(documentPrincipal)); + + csm->IsOriginPotentiallyTrustworthy(documentPrincipal, &trustworthyOrigin); + if (!trustworthyOrigin) { + return false; + } + + doc = doc->GetParentDocument(); + } + return true; +} + +// If we return an error code here, the ServiceWorkerContainer will +// automatically reject the Promise. +NS_IMETHODIMP +ServiceWorkerManager::Register(mozIDOMWindow* aWindow, + nsIURI* aScopeURI, + nsIURI* aScriptURI, + nsISupports** aPromise) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + auto* window = nsPIDOMWindowInner::From(aWindow); + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (!doc) { + return NS_ERROR_FAILURE; + } + + // Don't allow service workers to register when the *document* is chrome. + if (NS_WARN_IF(nsContentUtils::IsSystemPrincipal(doc->NodePrincipal()))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = window->GetOuterWindow(); + bool serviceWorkersTestingEnabled = + outerWindow->GetServiceWorkersTestingEnabled(); + + bool authenticatedOrigin; + if (Preferences::GetBool("dom.serviceWorkers.testing.enabled") || + serviceWorkersTestingEnabled) { + authenticatedOrigin = true; + } else { + authenticatedOrigin = IsFromAuthenticatedOrigin(doc); + } + + if (!authenticatedOrigin) { + NS_WARNING("ServiceWorker registration from insecure websites is not allowed."); + return NS_ERROR_DOM_SECURITY_ERR; + } + + // Data URLs are not allowed. + nsCOMPtr<nsIPrincipal> documentPrincipal = doc->NodePrincipal(); + + nsresult rv = documentPrincipal->CheckMayLoad(aScriptURI, true /* report */, + false /* allowIfInheritsPrincipal */); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // Check content policy. + int16_t decision = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER, + aScriptURI, + documentPrincipal, + doc, + EmptyCString(), + nullptr, + &decision); + NS_ENSURE_SUCCESS(rv, rv); + if (NS_WARN_IF(decision != nsIContentPolicy::ACCEPT)) { + return NS_ERROR_CONTENT_BLOCKED; + } + + + rv = documentPrincipal->CheckMayLoad(aScopeURI, true /* report */, + false /* allowIfInheritsPrinciple */); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // The IsOriginPotentiallyTrustworthy() check allows file:// and possibly other + // URI schemes. We need to explicitly only allows http and https schemes. + // Note, we just use the aScriptURI here for the check since its already + // been verified as same origin with the document principal. This also + // is a good block against accidentally allowing blob: script URIs which + // might inherit the origin. + bool isHttp = false; + bool isHttps = false; + aScriptURI->SchemeIs("http", &isHttp); + aScriptURI->SchemeIs("https", &isHttps); + if (NS_WARN_IF(!isHttp && !isHttps)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCString cleanedScope; + rv = aScopeURI->GetSpecIgnoringRef(cleanedScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + nsAutoCString spec; + rv = aScriptURI->GetSpecIgnoringRef(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(sgo, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(documentPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + AddRegisteringDocument(cleanedScope, doc); + + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, + cleanedScope); + + RefPtr<ServiceWorkerResolveWindowPromiseOnRegisterCallback> cb = + new ServiceWorkerResolveWindowPromiseOnRegisterCallback(window, promise); + + nsCOMPtr<nsILoadGroup> docLoadGroup = doc->GetDocumentLoadGroup(); + RefPtr<WorkerLoadInfo::InterfaceRequestor> ir = + new WorkerLoadInfo::InterfaceRequestor(documentPrincipal, docLoadGroup); + ir->MaybeAddTabChild(docLoadGroup); + + // Create a load group that is separate from, yet related to, the document's load group. + // This allows checks for interfaces like nsILoadContext to yield the values used by the + // the document, yet will not cancel the update job if the document's load group is cancelled. + nsCOMPtr<nsILoadGroup> loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); + MOZ_ALWAYS_SUCCEEDS(loadGroup->SetNotificationCallbacks(ir)); + + RefPtr<ServiceWorkerRegisterJob> job = + new ServiceWorkerRegisterJob(documentPrincipal, cleanedScope, spec, + loadGroup); + job->AppendResultCallback(cb); + queue->ScheduleJob(job); + + AssertIsOnMainThread(); + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_REGISTRATIONS, 1); + + promise.forget(aPromise); + return NS_OK; +} + +void +ServiceWorkerManager::AppendPendingOperation(nsIRunnable* aRunnable) +{ + MOZ_ASSERT(!mActor); + MOZ_ASSERT(aRunnable); + + if (!mShuttingDown) { + mPendingOperations.AppendElement(aRunnable); + } +} + +/* + * Implements the async aspects of the getRegistrations algorithm. + */ +class GetRegistrationsRunnable final : public Runnable +{ + nsCOMPtr<nsPIDOMWindowInner> mWindow; + RefPtr<Promise> mPromise; +public: + GetRegistrationsRunnable(nsPIDOMWindowInner* aWindow, Promise* aPromise) + : mWindow(aWindow), mPromise(aPromise) + {} + + NS_IMETHOD + Run() override + { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsIDocument* doc = mWindow->GetExtantDoc(); + if (!doc) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI(); + if (!docURI) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + if (!principal) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsTArray<RefPtr<ServiceWorkerRegistration>> array; + + if (NS_WARN_IF(!BasePrincipal::Cast(principal)->IsCodebasePrincipal())) { + return NS_OK; + } + + nsAutoCString scopeKey; + nsresult rv = swm->PrincipalToScopeKey(principal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ServiceWorkerManager::RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + mPromise->MaybeResolve(array); + return NS_OK; + } + + for (uint32_t i = 0; i < data->mOrderedScopes.Length(); ++i) { + RefPtr<ServiceWorkerRegistrationInfo> info = + data->mInfos.GetWeak(data->mOrderedScopes[i]); + if (info->mPendingUninstall) { + continue; + } + + NS_ConvertUTF8toUTF16 scope(data->mOrderedScopes[i]); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPromise->MaybeReject(rv); + break; + } + + rv = principal->CheckMayLoad(scopeURI, true /* report */, + false /* allowIfInheritsPrincipal */); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + RefPtr<ServiceWorkerRegistration> swr = + mWindow->GetServiceWorkerRegistration(scope); + + array.AppendElement(swr); + } + + mPromise->MaybeResolve(array); + return NS_OK; + } +}; + +// If we return an error code here, the ServiceWorkerContainer will +// automatically reject the Promise. +NS_IMETHODIMP +ServiceWorkerManager::GetRegistrations(mozIDOMWindow* aWindow, + nsISupports** aPromise) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + auto* window = nsPIDOMWindowInner::From(aWindow); + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + // Don't allow service workers to register when the *document* is chrome for + // now. + MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); + + nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(sgo, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + nsCOMPtr<nsIRunnable> runnable = + new GetRegistrationsRunnable(window, promise); + promise.forget(aPromise); + return NS_DispatchToCurrentThread(runnable); +} + +/* + * Implements the async aspects of the getRegistration algorithm. + */ +class GetRegistrationRunnable final : public Runnable +{ + nsCOMPtr<nsPIDOMWindowInner> mWindow; + RefPtr<Promise> mPromise; + nsString mDocumentURL; + +public: + GetRegistrationRunnable(nsPIDOMWindowInner* aWindow, Promise* aPromise, + const nsAString& aDocumentURL) + : mWindow(aWindow), mPromise(aPromise), mDocumentURL(aDocumentURL) + {} + + NS_IMETHOD + Run() override + { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsIDocument* doc = mWindow->GetExtantDoc(); + if (!doc) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI(); + if (!docURI) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mDocumentURL, nullptr, docURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPromise->MaybeReject(rv); + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + if (!principal) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + rv = principal->CheckMayLoad(uri, true /* report */, + false /* allowIfInheritsPrinciple */); + if (NS_FAILED(rv)) { + mPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return NS_OK; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetServiceWorkerRegistrationInfo(principal, uri); + + if (!registration) { + mPromise->MaybeResolveWithUndefined(); + return NS_OK; + } + + NS_ConvertUTF8toUTF16 scope(registration->mScope); + RefPtr<ServiceWorkerRegistration> swr = + mWindow->GetServiceWorkerRegistration(scope); + mPromise->MaybeResolve(swr); + + return NS_OK; + } +}; + +// If we return an error code here, the ServiceWorkerContainer will +// automatically reject the Promise. +NS_IMETHODIMP +ServiceWorkerManager::GetRegistration(mozIDOMWindow* aWindow, + const nsAString& aDocumentURL, + nsISupports** aPromise) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + auto* window = nsPIDOMWindowInner::From(aWindow); + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + // Don't allow service workers to register when the *document* is chrome for + // now. + MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); + + nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(sgo, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + nsCOMPtr<nsIRunnable> runnable = + new GetRegistrationRunnable(window, promise, aDocumentURL); + promise.forget(aPromise); + return NS_DispatchToCurrentThread(runnable); +} + +class GetReadyPromiseRunnable final : public Runnable +{ + nsCOMPtr<nsPIDOMWindowInner> mWindow; + RefPtr<Promise> mPromise; + +public: + GetReadyPromiseRunnable(nsPIDOMWindowInner* aWindow, Promise* aPromise) + : mWindow(aWindow), mPromise(aPromise) + {} + + NS_IMETHOD + Run() override + { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsIDocument* doc = mWindow->GetExtantDoc(); + if (!doc) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI(); + if (!docURI) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + if (!swm->CheckReadyPromise(mWindow, docURI, mPromise)) { + swm->StorePendingReadyPromise(mWindow, docURI, mPromise); + } + + return NS_OK; + } +}; + +NS_IMETHODIMP +ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes, + const nsACString& aScope, + uint32_t aDataLength, + uint8_t* aDataBytes, + uint8_t optional_argc) +{ + if (optional_argc == 2) { + nsTArray<uint8_t> data; + if (!data.InsertElementsAt(0, aDataBytes, aDataLength, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + return SendPushEvent(aOriginAttributes, aScope, EmptyString(), Some(data)); + } + MOZ_ASSERT(optional_argc == 0); + return SendPushEvent(aOriginAttributes, aScope, EmptyString(), Nothing()); +} + +nsresult +ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes, + const nsACString& aScope, + const nsAString& aMessageId, + const Maybe<nsTArray<uint8_t>>& aData) +{ + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromSuffix(aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + ServiceWorkerInfo* serviceWorker = GetActiveWorkerInfoForScope(attrs, aScope); + if (NS_WARN_IF(!serviceWorker)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(serviceWorker->GetPrincipal(), aScope); + MOZ_DIAGNOSTIC_ASSERT(registration); + + return serviceWorker->WorkerPrivate()->SendPushEvent(aMessageId, aData, + registration); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendPushSubscriptionChangeEvent(const nsACString& aOriginAttributes, + const nsACString& aScope) +{ + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromSuffix(aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); + if (!info) { + return NS_ERROR_FAILURE; + } + return info->WorkerPrivate()->SendPushSubscriptionChangeEvent(); +} + +nsresult +ServiceWorkerManager::SendNotificationEvent(const nsAString& aEventName, + const nsACString& aOriginSuffix, + const nsACString& aScope, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior) +{ + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromSuffix(aOriginSuffix)) { + return NS_ERROR_INVALID_ARG; + } + + ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); + if (!info) { + return NS_ERROR_FAILURE; + } + + ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate(); + return workerPrivate->SendNotificationEvent(aEventName, aID, aTitle, aDir, + aLang, aBody, aTag, + aIcon, aData, aBehavior, + NS_ConvertUTF8toUTF16(aScope)); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendNotificationClickEvent(const nsACString& aOriginSuffix, + const nsACString& aScope, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior) +{ + return SendNotificationEvent(NS_LITERAL_STRING(NOTIFICATION_CLICK_EVENT_NAME), + aOriginSuffix, aScope, aID, aTitle, aDir, aLang, + aBody, aTag, aIcon, aData, aBehavior); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendNotificationCloseEvent(const nsACString& aOriginSuffix, + const nsACString& aScope, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior) +{ + return SendNotificationEvent(NS_LITERAL_STRING(NOTIFICATION_CLOSE_EVENT_NAME), + aOriginSuffix, aScope, aID, aTitle, aDir, aLang, + aBody, aTag, aIcon, aData, aBehavior); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetReadyPromise(mozIDOMWindow* aWindow, + nsISupports** aPromise) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + auto* window = nsPIDOMWindowInner::From(aWindow); + + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + + // Don't allow service workers to register when the *document* is chrome for + // now. + MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); + + MOZ_ASSERT(!mPendingReadyPromises.Contains(window)); + + nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(sgo, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + nsCOMPtr<nsIRunnable> runnable = + new GetReadyPromiseRunnable(window, promise); + promise.forget(aPromise); + return NS_DispatchToCurrentThread(runnable); +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveReadyPromise(mozIDOMWindow* aWindow) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + if (!aWindow) { + return NS_ERROR_FAILURE; + } + + mPendingReadyPromises.Remove(aWindow); + return NS_OK; +} + +void +ServiceWorkerManager::StorePendingReadyPromise(nsPIDOMWindowInner* aWindow, + nsIURI* aURI, + Promise* aPromise) +{ + PendingReadyPromise* data; + + // We should not have 2 pending promises for the same window. + MOZ_ASSERT(!mPendingReadyPromises.Get(aWindow, &data)); + + data = new PendingReadyPromise(aURI, aPromise); + mPendingReadyPromises.Put(aWindow, data); +} + +void +ServiceWorkerManager::CheckPendingReadyPromises() +{ + for (auto iter = mPendingReadyPromises.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(iter.Key()); + MOZ_ASSERT(window); + + nsAutoPtr<PendingReadyPromise>& pendingReadyPromise = iter.Data(); + if (CheckReadyPromise(window, pendingReadyPromise->mURI, + pendingReadyPromise->mPromise)) { + iter.Remove(); + } + } +} + +bool +ServiceWorkerManager::CheckReadyPromise(nsPIDOMWindowInner* aWindow, + nsIURI* aURI, Promise* aPromise) +{ + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aURI); + + nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); + MOZ_ASSERT(doc); + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + MOZ_ASSERT(principal); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, aURI); + + if (registration && registration->GetActive()) { + NS_ConvertUTF8toUTF16 scope(registration->mScope); + RefPtr<ServiceWorkerRegistration> swr = + aWindow->GetServiceWorkerRegistration(scope); + aPromise->MaybeResolve(swr); + return true; + } + + return false; +} + +ServiceWorkerInfo* +ServiceWorkerManager::GetActiveWorkerInfoForScope(const PrincipalOriginAttributes& aOriginAttributes, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + if (NS_FAILED(rv)) { + return nullptr; + } + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(scopeURI, aOriginAttributes); + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, scopeURI); + if (!registration) { + return nullptr; + } + + return registration->GetActive(); +} + +ServiceWorkerInfo* +ServiceWorkerManager::GetActiveWorkerInfoForDocument(nsIDocument* aDocument) +{ + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerRegistrationInfo> registration; + GetDocumentRegistration(aDocument, getter_AddRefs(registration)); + + if (!registration) { + return nullptr; + } + + return registration->GetActive(); +} + +namespace { + +class UnregisterJobCallback final : public ServiceWorkerJob::Callback +{ + nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback; + + ~UnregisterJobCallback() + { + } + +public: + explicit UnregisterJobCallback(nsIServiceWorkerUnregisterCallback* aCallback) + : mCallback(aCallback) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCallback); + } + + void + JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + + if (aStatus.Failed()) { + mCallback->UnregisterFailed(); + return; + } + + MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Unregister); + RefPtr<ServiceWorkerUnregisterJob> unregisterJob = + static_cast<ServiceWorkerUnregisterJob*>(aJob); + mCallback->UnregisterSucceeded(unregisterJob->GetResult()); + } + + NS_INLINE_DECL_REFCOUNTING(UnregisterJobCallback) +}; + +} // anonymous namespace + +NS_IMETHODIMP +ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal, + nsIServiceWorkerUnregisterCallback* aCallback, + const nsAString& aScope) +{ + AssertIsOnMainThread(); + + if (!aPrincipal) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + +// This is not accessible by content, and callers should always ensure scope is +// a correct URI, so this is wrapped in DEBUG +#ifdef DEBUG + nsCOMPtr<nsIURI> scopeURI; + rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } +#endif + + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_ConvertUTF16toUTF8 scope(aScope); + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope); + + RefPtr<ServiceWorkerUnregisterJob> job = + new ServiceWorkerUnregisterJob(aPrincipal, scope, true /* send to parent */); + + if (aCallback) { + RefPtr<UnregisterJobCallback> cb = new UnregisterJobCallback(aCallback); + job->AppendResultCallback(cb); + } + + queue->ScheduleJob(job); + return NS_OK; +} + +nsresult +ServiceWorkerManager::NotifyUnregister(nsIPrincipal* aPrincipal, + const nsAString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + nsresult rv; + +// This is not accessible by content, and callers should always ensure scope is +// a correct URI, so this is wrapped in DEBUG +#ifdef DEBUG + nsCOMPtr<nsIURI> scopeURI; + rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } +#endif + + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_ConvertUTF16toUTF8 scope(aScope); + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope); + + RefPtr<ServiceWorkerUnregisterJob> job = + new ServiceWorkerUnregisterJob(aPrincipal, scope, + false /* send to parent */); + + queue->ScheduleJob(job); + return NS_OK; +} + +void +ServiceWorkerManager::WorkerIsIdle(ServiceWorkerInfo* aWorker) +{ + AssertIsOnMainThread(); + MOZ_DIAGNOSTIC_ASSERT(aWorker); + + RefPtr<ServiceWorkerRegistrationInfo> reg = + GetRegistration(aWorker->GetPrincipal(), aWorker->Scope()); + if (!reg) { + return; + } + + if (reg->GetActive() != aWorker) { + return; + } + + if (!reg->IsControllingDocuments() && reg->mPendingUninstall) { + RemoveRegistration(reg); + return; + } + + reg->TryToActivateAsync(); +} + +already_AddRefed<ServiceWorkerJobQueue> +ServiceWorkerManager::GetOrCreateJobQueue(const nsACString& aKey, + const nsACString& aScope) +{ + MOZ_ASSERT(!aKey.IsEmpty()); + ServiceWorkerManager::RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(aKey, &data)) { + data = new RegistrationDataPerPrincipal(); + mRegistrationInfos.Put(aKey, data); + } + + RefPtr<ServiceWorkerJobQueue> queue; + if (!data->mJobQueues.Get(aScope, getter_AddRefs(queue))) { + RefPtr<ServiceWorkerJobQueue> newQueue = new ServiceWorkerJobQueue(); + queue = newQueue; + data->mJobQueues.Put(aScope, newQueue.forget()); + } + + return queue.forget(); +} + +/* static */ +already_AddRefed<ServiceWorkerManager> +ServiceWorkerManager::GetInstance() +{ + // Note: We don't simply check gInstance for null-ness here, since otherwise + // this can resurrect the ServiceWorkerManager pretty late during shutdown. + static bool firstTime = true; + if (firstTime) { + RefPtr<ServiceWorkerRegistrar> swr; + + // Don't create the ServiceWorkerManager until the ServiceWorkerRegistrar is + // initialized. + if (XRE_IsParentProcess()) { + swr = ServiceWorkerRegistrar::Get(); + if (!swr) { + return nullptr; + } + } + + firstTime = false; + + AssertIsOnMainThread(); + + gInstance = new ServiceWorkerManager(); + gInstance->Init(swr); + ClearOnShutdown(&gInstance); + } + RefPtr<ServiceWorkerManager> copy = gInstance.get(); + return copy.forget(); +} + +void +ServiceWorkerManager::FinishFetch(ServiceWorkerRegistrationInfo* aRegistration) +{ +} + +void +ServiceWorkerManager::ReportToAllClients(const nsCString& aScope, + const nsString& aMessage, + const nsString& aFilename, + const nsString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags) +{ + nsCOMPtr<nsIURI> uri; + nsresult rv; + + if (!aFilename.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(uri), aFilename); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + AutoTArray<uint64_t, 16> windows; + + // Report errors to every controlled document. + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* reg = iter.UserData(); + MOZ_ASSERT(reg); + if (!reg->mScope.Equals(aScope)) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + if (!doc || !doc->IsCurrentActiveDocument() || !doc->GetWindow()) { + continue; + } + + windows.AppendElement(doc->InnerWindowID()); + + nsContentUtils::ReportToConsoleNonLocalized(aMessage, + aFlags, + NS_LITERAL_CSTRING("Service Workers"), + doc, + uri, + aLine, + aLineNumber, + aColumnNumber, + nsContentUtils::eOMIT_LOCATION); + } + + // Report to any documents that have called .register() for this scope. They + // may not be controlled, but will still want to see error reports. + WeakDocumentList* regList = mRegisteringDocuments.Get(aScope); + if (regList) { + for (int32_t i = regList->Length() - 1; i >= 0; --i) { + nsCOMPtr<nsIDocument> doc = do_QueryReferent(regList->ElementAt(i)); + if (!doc) { + regList->RemoveElementAt(i); + continue; + } + + if (!doc->IsCurrentActiveDocument()) { + continue; + } + + uint64_t innerWindowId = doc->InnerWindowID(); + if (windows.Contains(innerWindowId)) { + continue; + } + + windows.AppendElement(innerWindowId); + + nsContentUtils::ReportToConsoleNonLocalized(aMessage, + aFlags, + NS_LITERAL_CSTRING("Service Workers"), + doc, + uri, + aLine, + aLineNumber, + aColumnNumber, + nsContentUtils::eOMIT_LOCATION); + } + + if (regList->IsEmpty()) { + regList = nullptr; + nsAutoPtr<WeakDocumentList> doomed; + mRegisteringDocuments.RemoveAndForget(aScope, doomed); + } + } + + InterceptionList* intList = mNavigationInterceptions.Get(aScope); + if (intList) { + nsIConsoleService* consoleService = nullptr; + for (uint32_t i = 0; i < intList->Length(); ++i) { + nsCOMPtr<nsIInterceptedChannel> channel = intList->ElementAt(i); + + nsCOMPtr<nsIChannel> inner; + rv = channel->GetChannel(getter_AddRefs(inner)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + uint64_t innerWindowId = nsContentUtils::GetInnerWindowID(inner); + if (innerWindowId == 0 || windows.Contains(innerWindowId)) { + continue; + } + + windows.AppendElement(innerWindowId); + + // Unfortunately the nsContentUtils helpers don't provide a convenient + // way to log to a window ID without a document. Use console service + // directly. + nsCOMPtr<nsIScriptError> errorObject = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = errorObject->InitWithWindowID(aMessage, + aFilename, + aLine, + aLineNumber, + aColumnNumber, + aFlags, + NS_LITERAL_CSTRING("Service Workers"), + innerWindowId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (!consoleService) { + rv = CallGetService(NS_CONSOLESERVICE_CONTRACTID, &consoleService); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + consoleService->LogMessage(errorObject); + } + } + + // If there are no documents to report to, at least report something to the + // browser console. + if (windows.IsEmpty()) { + nsContentUtils::ReportToConsoleNonLocalized(aMessage, + aFlags, + NS_LITERAL_CSTRING("Service Workers"), + nullptr, // document + uri, + aLine, + aLineNumber, + aColumnNumber, + nsContentUtils::eOMIT_LOCATION); + return; + } +} + +/* static */ +void +ServiceWorkerManager::LocalizeAndReportToAllClients( + const nsCString& aScope, + const char* aStringKey, + const nsTArray<nsString>& aParamArray, + uint32_t aFlags, + const nsString& aFilename, + const nsString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber) +{ + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return; + } + + nsresult rv; + nsXPIDLString message; + rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, + aStringKey, aParamArray, message); + if (NS_SUCCEEDED(rv)) { + swm->ReportToAllClients(aScope, message, + aFilename, aLine, aLineNumber, aColumnNumber, + aFlags); + } else { + NS_WARNING("Failed to format and therefore report localized error."); + } +} + +void +ServiceWorkerManager::FlushReportsToAllClients(const nsACString& aScope, + nsIConsoleReportCollector* aReporter) +{ + AutoTArray<uint64_t, 16> windows; + + // Report errors to every controlled document. + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* reg = iter.UserData(); + MOZ_ASSERT(reg); + if (!reg->mScope.Equals(aScope)) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + if (!doc || !doc->IsCurrentActiveDocument() || !doc->GetWindow()) { + continue; + } + + windows.AppendElement(doc->InnerWindowID()); + + aReporter->FlushConsoleReports(doc, + nsIConsoleReportCollector::ReportAction::Save); + } + + // Report to any documents that have called .register() for this scope. They + // may not be controlled, but will still want to see error reports. + WeakDocumentList* regList = mRegisteringDocuments.Get(aScope); + if (regList) { + for (int32_t i = regList->Length() - 1; i >= 0; --i) { + nsCOMPtr<nsIDocument> doc = do_QueryReferent(regList->ElementAt(i)); + if (!doc) { + regList->RemoveElementAt(i); + continue; + } + + if (!doc->IsCurrentActiveDocument()) { + continue; + } + + uint64_t innerWindowId = doc->InnerWindowID(); + if (windows.Contains(innerWindowId)) { + continue; + } + + windows.AppendElement(innerWindowId); + + aReporter->FlushConsoleReports(doc, + nsIConsoleReportCollector::ReportAction::Save); + } + + if (regList->IsEmpty()) { + regList = nullptr; + nsAutoPtr<WeakDocumentList> doomed; + mRegisteringDocuments.RemoveAndForget(aScope, doomed); + } + } + + nsresult rv; + InterceptionList* intList = mNavigationInterceptions.Get(aScope); + if (intList) { + for (uint32_t i = 0; i < intList->Length(); ++i) { + nsCOMPtr<nsIInterceptedChannel> channel = intList->ElementAt(i); + + nsCOMPtr<nsIChannel> inner; + rv = channel->GetChannel(getter_AddRefs(inner)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + uint64_t innerWindowId = nsContentUtils::GetInnerWindowID(inner); + if (innerWindowId == 0 || windows.Contains(innerWindowId)) { + continue; + } + + windows.AppendElement(innerWindowId); + + aReporter->FlushReportsByWindowId(innerWindowId, + nsIConsoleReportCollector::ReportAction::Save); + } + } + + // If there are no documents to report to, at least report something to the + // browser console. + if (windows.IsEmpty()) { + aReporter->FlushConsoleReports((nsIDocument*)nullptr); + return; + } + + aReporter->ClearConsoleReports(); +} + +void +ServiceWorkerManager::HandleError(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsCString& aScope, + const nsString& aWorkerURL, + const nsString& aMessage, + const nsString& aFilename, + const nsString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags, + JSExnType aExnType) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + ServiceWorkerManager::RegistrationDataPerPrincipal* data; + if (NS_WARN_IF(!mRegistrationInfos.Get(scopeKey, &data))) { + return; + } + + // Always report any uncaught exceptions or errors to the console of + // each client. + ReportToAllClients(aScope, aMessage, aFilename, aLine, aLineNumber, + aColumnNumber, aFlags); +} + +void +ServiceWorkerManager::LoadRegistration( + const ServiceWorkerRegistrationData& aRegistration) +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIPrincipal> principal = + PrincipalInfoToPrincipal(aRegistration.principal()); + if (!principal) { + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(principal, aRegistration.scope()); + if (!registration) { + registration = CreateNewRegistration(aRegistration.scope(), principal); + } else { + // If active worker script matches our expectations for a "current worker", + // then we are done. + if (registration->GetActive() && + registration->GetActive()->ScriptSpec() == aRegistration.currentWorkerURL()) { + // No needs for updates. + return; + } + } + + const nsCString& currentWorkerURL = aRegistration.currentWorkerURL(); + if (!currentWorkerURL.IsEmpty()) { + registration->SetActive( + new ServiceWorkerInfo(registration->mPrincipal, registration->mScope, + currentWorkerURL, aRegistration.cacheName())); + registration->GetActive()->SetActivateStateUncheckedWithoutEvent(ServiceWorkerState::Activated); + } +} + +void +ServiceWorkerManager::LoadRegistrations( + const nsTArray<ServiceWorkerRegistrationData>& aRegistrations) +{ + AssertIsOnMainThread(); + + for (uint32_t i = 0, len = aRegistrations.Length(); i < len; ++i) { + LoadRegistration(aRegistrations[i]); + } +} + +void +ServiceWorkerManager::ActorFailed() +{ + MOZ_DIAGNOSTIC_ASSERT(!mActor); + MaybeStartShutdown(); +} + +void +ServiceWorkerManager::ActorCreated(mozilla::ipc::PBackgroundChild* aActor) +{ + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + if (mShuttingDown) { + MOZ_DIAGNOSTIC_ASSERT(mPendingOperations.IsEmpty()); + return; + } + + PServiceWorkerManagerChild* actor = + aActor->SendPServiceWorkerManagerConstructor(); + if (!actor) { + ActorFailed(); + return; + } + + mActor = static_cast<ServiceWorkerManagerChild*>(actor); + + // Flush the pending requests. + for (uint32_t i = 0, len = mPendingOperations.Length(); i < len; ++i) { + MOZ_ASSERT(mPendingOperations[i]); + nsresult rv = NS_DispatchToCurrentThread(mPendingOperations[i].forget()); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch a runnable."); + } + } + + mPendingOperations.Clear(); +} + +void +ServiceWorkerManager::StoreRegistration( + nsIPrincipal* aPrincipal, + ServiceWorkerRegistrationInfo* aRegistration) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aRegistration); + + if (mShuttingDown) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mActor); + if (!mActor) { + return; + } + + ServiceWorkerRegistrationData data; + nsresult rv = PopulateRegistrationData(aPrincipal, aRegistration, data); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + PrincipalInfo principalInfo; + if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, + &principalInfo)))) { + return; + } + + mActor->SendRegister(data); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsPIDOMWindowInner* aWindow) +{ + MOZ_ASSERT(aWindow); + nsCOMPtr<nsIDocument> document = aWindow->GetExtantDoc(); + return GetServiceWorkerRegistrationInfo(document); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIDocument* aDoc) +{ + MOZ_ASSERT(aDoc); + nsCOMPtr<nsIURI> documentURI = aDoc->GetDocumentURI(); + nsCOMPtr<nsIPrincipal> principal = aDoc->NodePrincipal(); + return GetServiceWorkerRegistrationInfo(principal, documentURI); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal, + nsIURI* aURI) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aURI); + + //XXXnsm Temporary fix until Bug 1171432 is fixed. + if (NS_WARN_IF(BasePrincipal::Cast(aPrincipal)->AppId() == nsIScriptSecurityManager::UNKNOWN_APP_ID)) { + return nullptr; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_FAILED(rv)) { + return nullptr; + } + + return GetServiceWorkerRegistrationInfo(scopeKey, aURI); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo(const nsACString& aScopeKey, + nsIURI* aURI) +{ + MOZ_ASSERT(aURI); + + nsAutoCString spec; + nsresult rv = aURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsAutoCString scope; + RegistrationDataPerPrincipal* data; + if (!FindScopeForPath(aScopeKey, spec, &data, scope)) { + return nullptr; + } + + MOZ_ASSERT(data); + + RefPtr<ServiceWorkerRegistrationInfo> registration; + data->mInfos.Get(scope, getter_AddRefs(registration)); + // ordered scopes and registrations better be in sync. + MOZ_ASSERT(registration); + +#ifdef DEBUG + nsAutoCString origin; + rv = registration->mPrincipal->GetOrigin(origin); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(origin.Equals(aScopeKey)); +#endif + + if (registration->mPendingUninstall) { + return nullptr; + } + return registration.forget(); +} + +/* static */ nsresult +ServiceWorkerManager::PrincipalToScopeKey(nsIPrincipal* aPrincipal, + nsACString& aKey) +{ + MOZ_ASSERT(aPrincipal); + + if (!BasePrincipal::Cast(aPrincipal)->IsCodebasePrincipal()) { + return NS_ERROR_FAILURE; + } + + nsresult rv = aPrincipal->GetOrigin(aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +/* static */ void +ServiceWorkerManager::AddScopeAndRegistration(const nsACString& aScope, + ServiceWorkerRegistrationInfo* aInfo) +{ + MOZ_ASSERT(aInfo); + MOZ_ASSERT(aInfo->mPrincipal); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return; + } + + nsAutoCString scopeKey; + nsresult rv = swm->PrincipalToScopeKey(aInfo->mPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + MOZ_ASSERT(!scopeKey.IsEmpty()); + + RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + data = new RegistrationDataPerPrincipal(); + swm->mRegistrationInfos.Put(scopeKey, data); + } + + for (uint32_t i = 0; i < data->mOrderedScopes.Length(); ++i) { + const nsCString& current = data->mOrderedScopes[i]; + + // Perfect match! + if (aScope.Equals(current)) { + data->mInfos.Put(aScope, aInfo); + swm->NotifyListenersOnRegister(aInfo); + return; + } + + // Sort by length, with longest match first. + // /foo/bar should be before /foo/ + // Similarly /foo/b is between the two. + if (StringBeginsWith(aScope, current)) { + data->mOrderedScopes.InsertElementAt(i, aScope); + data->mInfos.Put(aScope, aInfo); + swm->NotifyListenersOnRegister(aInfo); + return; + } + } + + data->mOrderedScopes.AppendElement(aScope); + data->mInfos.Put(aScope, aInfo); + swm->NotifyListenersOnRegister(aInfo); +} + +/* static */ bool +ServiceWorkerManager::FindScopeForPath(const nsACString& aScopeKey, + const nsACString& aPath, + RegistrationDataPerPrincipal** aData, + nsACString& aMatch) +{ + MOZ_ASSERT(aData); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + + if (!swm || !swm->mRegistrationInfos.Get(aScopeKey, aData)) { + return false; + } + + for (uint32_t i = 0; i < (*aData)->mOrderedScopes.Length(); ++i) { + const nsCString& current = (*aData)->mOrderedScopes[i]; + if (StringBeginsWith(aPath, current)) { + aMatch = current; + return true; + } + } + + return false; +} + +/* static */ bool +ServiceWorkerManager::HasScope(nsIPrincipal* aPrincipal, + const nsACString& aScope) +{ + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return false; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + return false; + } + + return data->mOrderedScopes.Contains(aScope); +} + +/* static */ void +ServiceWorkerManager::RemoveScopeAndRegistration(ServiceWorkerRegistrationInfo* aRegistration) +{ + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return; + } + + nsAutoCString scopeKey; + nsresult rv = swm->PrincipalToScopeKey(aRegistration->mPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + nsCOMPtr<nsITimer> timer = data->mUpdateTimers.Get(aRegistration->mScope); + if (timer) { + timer->Cancel(); + data->mUpdateTimers.Remove(aRegistration->mScope); + } + + // The registration should generally only be removed if there are no controlled + // documents, but mControlledDocuments can contain references to potentially + // controlled docs. This happens when the service worker is not active yet. + // We must purge these references since we are evicting the registration. + for (auto iter = swm->mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* reg = iter.UserData(); + MOZ_ASSERT(reg); + if (reg->mScope.Equals(aRegistration->mScope)) { + iter.Remove(); + } + } + + RefPtr<ServiceWorkerRegistrationInfo> info; + data->mInfos.Get(aRegistration->mScope, getter_AddRefs(info)); + + data->mInfos.Remove(aRegistration->mScope); + data->mOrderedScopes.RemoveElement(aRegistration->mScope); + swm->NotifyListenersOnUnregister(info); + + swm->MaybeRemoveRegistrationInfo(scopeKey); + swm->NotifyServiceWorkerRegistrationRemoved(aRegistration); +} + +void +ServiceWorkerManager::MaybeRemoveRegistrationInfo(const nsACString& aScopeKey) +{ + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(aScopeKey, &data)) { + return; + } + + if (data->mOrderedScopes.IsEmpty() && data->mJobQueues.Count() == 0) { + mRegistrationInfos.Remove(aScopeKey); + } +} + +void +ServiceWorkerManager::MaybeStartControlling(nsIDocument* aDoc, + const nsAString& aDocumentId) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aDoc); + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(aDoc); + if (registration) { + MOZ_ASSERT(!mControlledDocuments.Contains(aDoc)); + StartControllingADocument(registration, aDoc, aDocumentId); + } +} + +void +ServiceWorkerManager::MaybeStopControlling(nsIDocument* aDoc) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aDoc); + RefPtr<ServiceWorkerRegistrationInfo> registration; + mControlledDocuments.Remove(aDoc, getter_AddRefs(registration)); + // A document which was uncontrolled does not maintain that state itself, so + // it will always call MaybeStopControlling() even if there isn't an + // associated registration. So this check is required. + if (registration) { + StopControllingADocument(registration); + } +} + +void +ServiceWorkerManager::MaybeCheckNavigationUpdate(nsIDocument* aDoc) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aDoc); + // We perform these success path navigation update steps when the + // document tells us its more or less done loading. This avoids + // slowing down page load and also lets pages consistently get + // updatefound events when they fire. + // + // 9.8.20 If respondWithEntered is false, then: + // 9.8.22 Else: (respondWith was entered and succeeded) + // If request is a non-subresource request, then: Invoke Soft Update + // algorithm. + RefPtr<ServiceWorkerRegistrationInfo> registration; + mControlledDocuments.Get(aDoc, getter_AddRefs(registration)); + if (registration) { + registration->MaybeScheduleUpdate(); + } +} + +void +ServiceWorkerManager::StartControllingADocument(ServiceWorkerRegistrationInfo* aRegistration, + nsIDocument* aDoc, + const nsAString& aDocumentId) +{ + MOZ_ASSERT(aRegistration); + MOZ_ASSERT(aDoc); + + aRegistration->StartControllingADocument(); + mControlledDocuments.Put(aDoc, aRegistration); + if (!aDocumentId.IsEmpty()) { + aDoc->SetId(aDocumentId); + } + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS, 1); +} + +void +ServiceWorkerManager::StopControllingADocument(ServiceWorkerRegistrationInfo* aRegistration) +{ + aRegistration->StopControllingADocument(); + if (aRegistration->IsControllingDocuments() || !aRegistration->IsIdle()) { + return; + } + + if (aRegistration->mPendingUninstall) { + RemoveRegistration(aRegistration); + return; + } + + // We use to aggressively terminate the worker at this point, but it + // caused problems. There are more uses for a service worker than actively + // controlled documents. We need to let the worker naturally terminate + // in case its handling push events, message events, etc. + aRegistration->TryToActivateAsync(); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetScopeForUrl(nsIPrincipal* aPrincipal, + const nsAString& aUrl, nsAString& aScope) +{ + MOZ_ASSERT(aPrincipal); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> r = + GetServiceWorkerRegistrationInfo(aPrincipal, uri); + if (!r) { + return NS_ERROR_FAILURE; + } + + aScope = NS_ConvertUTF8toUTF16(r->mScope); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::AddRegistrationEventListener(const nsAString& aScope, + ServiceWorkerRegistrationListener* aListener) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aListener); +#ifdef DEBUG + // Ensure a registration is only listening for it's own scope. + nsAutoString regScope; + aListener->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + MOZ_ASSERT(aScope.Equals(regScope)); +#endif + + MOZ_ASSERT(!mServiceWorkerRegistrationListeners.Contains(aListener)); + mServiceWorkerRegistrationListeners.AppendElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveRegistrationEventListener(const nsAString& aScope, + ServiceWorkerRegistrationListener* aListener) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aListener); +#ifdef DEBUG + // Ensure a registration is unregistering for it's own scope. + nsAutoString regScope; + aListener->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + MOZ_ASSERT(aScope.Equals(regScope)); +#endif + + MOZ_ASSERT(mServiceWorkerRegistrationListeners.Contains(aListener)); + mServiceWorkerRegistrationListeners.RemoveElement(aListener); + return NS_OK; +} + +void +ServiceWorkerManager::FireUpdateFoundOnServiceWorkerRegistrations( + ServiceWorkerRegistrationInfo* aRegistration) +{ + AssertIsOnMainThread(); + + nsTObserverArray<ServiceWorkerRegistrationListener*>::ForwardIterator it(mServiceWorkerRegistrationListeners); + while (it.HasMore()) { + RefPtr<ServiceWorkerRegistrationListener> target = it.GetNext(); + nsAutoString regScope; + target->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + + NS_ConvertUTF16toUTF8 utf8Scope(regScope); + if (utf8Scope.Equals(aRegistration->mScope)) { + target->UpdateFound(); + } + } +} + +/* + * This is used for installing, waiting and active. + */ +nsresult +ServiceWorkerManager::GetServiceWorkerForScope(nsPIDOMWindowInner* aWindow, + const nsAString& aScope, + WhichServiceWorker aWhichWorker, + nsISupports** aServiceWorker) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); + MOZ_ASSERT(doc); + + /////////////////////////////////////////// + // Security check + nsAutoCString scope = NS_ConvertUTF16toUTF8(aScope); + nsCOMPtr<nsIURI> scopeURI; + // We pass nullptr as the base URI since scopes obtained from + // ServiceWorkerRegistrations MUST be fully qualified URIs. + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<nsIPrincipal> documentPrincipal = doc->NodePrincipal(); + rv = documentPrincipal->CheckMayLoad(scopeURI, true /* report */, + false /* allowIfInheritsPrinciple */); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + //////////////////////////////////////////// + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(documentPrincipal, scope); + if (NS_WARN_IF(!registration)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerInfo> info; + if (aWhichWorker == WhichServiceWorker::INSTALLING_WORKER) { + info = registration->GetInstalling(); + } else if (aWhichWorker == WhichServiceWorker::WAITING_WORKER) { + info = registration->GetWaiting(); + } else if (aWhichWorker == WhichServiceWorker::ACTIVE_WORKER) { + info = registration->GetActive(); + } else { + MOZ_CRASH("Invalid worker type"); + } + + if (NS_WARN_IF(!info)) { + return NS_ERROR_DOM_NOT_FOUND_ERR; + } + + RefPtr<ServiceWorker> serviceWorker = info->GetOrCreateInstance(aWindow); + + serviceWorker->SetState(info->State()); + serviceWorker.forget(aServiceWorker); + return NS_OK; +} + +namespace { + +class ContinueDispatchFetchEventRunnable : public Runnable +{ + RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; + nsCOMPtr<nsIInterceptedChannel> mChannel; + nsCOMPtr<nsILoadGroup> mLoadGroup; + nsString mDocumentId; + bool mIsReload; +public: + ContinueDispatchFetchEventRunnable(ServiceWorkerPrivate* aServiceWorkerPrivate, + nsIInterceptedChannel* aChannel, + nsILoadGroup* aLoadGroup, + const nsAString& aDocumentId, + bool aIsReload) + : mServiceWorkerPrivate(aServiceWorkerPrivate) + , mChannel(aChannel) + , mLoadGroup(aLoadGroup) + , mDocumentId(aDocumentId) + , mIsReload(aIsReload) + { + MOZ_ASSERT(aServiceWorkerPrivate); + MOZ_ASSERT(aChannel); + } + + void + HandleError() + { + AssertIsOnMainThread(); + NS_WARNING("Unexpected error while dispatching fetch event!"); + DebugOnly<nsresult> rv = mChannel->ResetInterception(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Failed to resume intercepted network request"); + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + nsCOMPtr<nsIChannel> channel; + nsresult rv = mChannel->GetChannel(getter_AddRefs(channel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleError(); + return NS_OK; + } + + // The channel might have encountered an unexpected error while ensuring + // the upload stream is cloneable. Check here and reset the interception + // if that happens. + nsresult status; + rv = channel->GetStatus(&status); + if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(status))) { + HandleError(); + return NS_OK; + } + + rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, + mDocumentId, mIsReload); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleError(); + } + + return NS_OK; + } +}; + +} // anonymous namespace + +void +ServiceWorkerManager::DispatchFetchEvent(const PrincipalOriginAttributes& aOriginAttributes, + nsIDocument* aDoc, + const nsAString& aDocumentIdForTopLevelNavigation, + nsIInterceptedChannel* aChannel, + bool aIsReload, + bool aIsSubresourceLoad, + ErrorResult& aRv) +{ + MOZ_ASSERT(aChannel); + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerInfo> serviceWorker; + nsCOMPtr<nsILoadGroup> loadGroup; + nsAutoString documentId; + + if (aIsSubresourceLoad) { + MOZ_ASSERT(aDoc); + + serviceWorker = GetActiveWorkerInfoForDocument(aDoc); + if (!serviceWorker) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + loadGroup = aDoc->GetDocumentLoadGroup(); + nsresult rv = aDoc->GetOrCreateId(documentId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } else { + nsCOMPtr<nsIChannel> internalChannel; + aRv = aChannel->GetChannel(getter_AddRefs(internalChannel)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + internalChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + + // TODO: Use aDocumentIdForTopLevelNavigation for potentialClientId, pending + // the spec change. + + nsCOMPtr<nsIURI> uri; + aRv = aChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // non-subresource request means the URI contains the principal + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(uri, aOriginAttributes); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, uri); + if (!registration) { + NS_WARNING("No registration found when dispatching the fetch event"); + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // While we only enter this method if IsAvailable() previously saw + // an active worker, it is possible for that worker to be removed + // before we get to this point. Therefore we must handle a nullptr + // active worker here. + serviceWorker = registration->GetActive(); + if (!serviceWorker) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + AddNavigationInterception(serviceWorker->Scope(), aChannel); + } + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(serviceWorker); + + nsCOMPtr<nsIRunnable> continueRunnable = + new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(), + aChannel, loadGroup, + documentId, aIsReload); + + nsCOMPtr<nsIChannel> innerChannel; + aRv = aChannel->GetChannel(getter_AddRefs(innerChannel)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(innerChannel); + + // If there is no upload stream, then continue immediately + if (!uploadChannel) { + MOZ_ALWAYS_SUCCEEDS(continueRunnable->Run()); + return; + } + // Otherwise, ensure the upload stream can be cloned directly. This may + // require some async copying, so provide a callback. + aRv = uploadChannel->EnsureUploadStreamIsCloneable(continueRunnable); +} + +bool +ServiceWorkerManager::IsAvailable(nsIPrincipal* aPrincipal, + nsIURI* aURI) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aURI); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(aPrincipal, aURI); + return registration && registration->GetActive(); +} + +bool +ServiceWorkerManager::IsControlled(nsIDocument* aDoc, ErrorResult& aRv) +{ + MOZ_ASSERT(aDoc); + + if (nsContentUtils::IsInPrivateBrowsing(aDoc)) { + // Handle the case where a service worker was previously registered in + // a non-private window (bug 1255621). + return false; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration; + nsresult rv = GetDocumentRegistration(aDoc, getter_AddRefs(registration)); + if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE)) { + // It's OK to ignore the case where we don't have a registration. + aRv.Throw(rv); + return false; + } + + return !!registration; +} + +nsresult +ServiceWorkerManager::GetDocumentRegistration(nsIDocument* aDoc, + ServiceWorkerRegistrationInfo** aRegistrationInfo) +{ + RefPtr<ServiceWorkerRegistrationInfo> registration; + if (!mControlledDocuments.Get(aDoc, getter_AddRefs(registration))) { + return NS_ERROR_NOT_AVAILABLE; + } + + // If the document is controlled, the current worker MUST be non-null. + if (!registration->GetActive()) { + return NS_ERROR_NOT_AVAILABLE; + } + + registration.forget(aRegistrationInfo); + return NS_OK; +} + +/* + * The .controller is for the registration associated with the document when + * the document was loaded. + */ +NS_IMETHODIMP +ServiceWorkerManager::GetDocumentController(nsPIDOMWindowInner* aWindow, + nsISupports** aServiceWorker) +{ + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); + if (!doc) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration; + nsresult rv = GetDocumentRegistration(doc, getter_AddRefs(registration)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(registration->GetActive()); + RefPtr<ServiceWorker> serviceWorker = + registration->GetActive()->GetOrCreateInstance(aWindow); + + serviceWorker.forget(aServiceWorker); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::GetInstalling(nsPIDOMWindowInner* aWindow, + const nsAString& aScope, + nsISupports** aServiceWorker) +{ + return GetServiceWorkerForScope(aWindow, aScope, + WhichServiceWorker::INSTALLING_WORKER, + aServiceWorker); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetWaiting(nsPIDOMWindowInner* aWindow, + const nsAString& aScope, + nsISupports** aServiceWorker) +{ + return GetServiceWorkerForScope(aWindow, aScope, + WhichServiceWorker::WAITING_WORKER, + aServiceWorker); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetActive(nsPIDOMWindowInner* aWindow, + const nsAString& aScope, + nsISupports** aServiceWorker) +{ + return GetServiceWorkerForScope(aWindow, aScope, + WhichServiceWorker::ACTIVE_WORKER, + aServiceWorker); +} + +void +ServiceWorkerManager::InvalidateServiceWorkerRegistrationWorker(ServiceWorkerRegistrationInfo* aRegistration, + WhichServiceWorker aWhichOnes) +{ + AssertIsOnMainThread(); + nsTObserverArray<ServiceWorkerRegistrationListener*>::ForwardIterator it(mServiceWorkerRegistrationListeners); + while (it.HasMore()) { + RefPtr<ServiceWorkerRegistrationListener> target = it.GetNext(); + nsAutoString regScope; + target->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + + NS_ConvertUTF16toUTF8 utf8Scope(regScope); + + if (utf8Scope.Equals(aRegistration->mScope)) { + target->InvalidateWorkers(aWhichOnes); + } + } +} + +void +ServiceWorkerManager::NotifyServiceWorkerRegistrationRemoved(ServiceWorkerRegistrationInfo* aRegistration) +{ + AssertIsOnMainThread(); + nsTObserverArray<ServiceWorkerRegistrationListener*>::ForwardIterator it(mServiceWorkerRegistrationListeners); + while (it.HasMore()) { + RefPtr<ServiceWorkerRegistrationListener> target = it.GetNext(); + nsAutoString regScope; + target->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + + NS_ConvertUTF16toUTF8 utf8Scope(regScope); + + if (utf8Scope.Equals(aRegistration->mScope)) { + target->RegistrationRemoved(); + } + } +} + +void +ServiceWorkerManager::SoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + + if (mShuttingDown) { + return; + } + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(scopeURI, aOriginAttributes); + if (NS_WARN_IF(!principal)) { + return; + } + + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(principal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(scopeKey, aScope); + if (NS_WARN_IF(!registration)) { + return; + } + + // "If registration's uninstalling flag is set, abort these steps." + if (registration->mPendingUninstall) { + return; + } + + // "If registration's installing worker is not null, abort these steps." + if (registration->GetInstalling()) { + return; + } + + // "Let newestWorker be the result of running Get Newest Worker algorithm + // passing registration as its argument. + // If newestWorker is null, abort these steps." + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + if (!newest) { + return; + } + + // "If the registration queue for registration is empty, invoke Update algorithm, + // or its equivalent, with client, registration as its argument." + // TODO(catalinb): We don't implement the force bypass cache flag. + // See: https://github.com/slightlyoff/ServiceWorker/issues/759 + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, + aScope); + + RefPtr<ServiceWorkerUpdateJob> job = + new ServiceWorkerUpdateJob(principal, registration->mScope, + newest->ScriptSpec(), nullptr); + queue->ScheduleJob(job); +} + +namespace { + +class UpdateJobCallback final : public ServiceWorkerJob::Callback +{ + RefPtr<ServiceWorkerUpdateFinishCallback> mCallback; + + ~UpdateJobCallback() + { + } + +public: + explicit UpdateJobCallback(ServiceWorkerUpdateFinishCallback* aCallback) + : mCallback(aCallback) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCallback); + } + + void + JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + + if (aStatus.Failed()) { + mCallback->UpdateFailed(aStatus); + return; + } + + MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Update); + RefPtr<ServiceWorkerUpdateJob> updateJob = + static_cast<ServiceWorkerUpdateJob*>(aJob); + RefPtr<ServiceWorkerRegistrationInfo> reg = updateJob->GetRegistration(); + mCallback->UpdateSucceeded(reg); + } + + NS_INLINE_DECL_REFCOUNTING(UpdateJobCallback) +}; +} // anonymous namespace + +void +ServiceWorkerManager::Update(nsIPrincipal* aPrincipal, + const nsACString& aScope, + ServiceWorkerUpdateFinishCallback* aCallback) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCallback); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(scopeKey, aScope); + if (NS_WARN_IF(!registration)) { + return; + } + + // "Let newestWorker be the result of running Get Newest Worker algorithm + // passing registration as its argument. + // If newestWorker is null, return a promise rejected with "InvalidStateError" + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + if (!newest) { + ErrorResult error(NS_ERROR_DOM_INVALID_STATE_ERR); + aCallback->UpdateFailed(error); + + // In case the callback does not consume the exception + error.SuppressException(); + + return; + } + + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope); + + // "Invoke Update algorithm, or its equivalent, with client, registration as + // its argument." + RefPtr<ServiceWorkerUpdateJob> job = + new ServiceWorkerUpdateJob(aPrincipal, registration->mScope, + newest->ScriptSpec(), nullptr); + + RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback); + job->AppendResultCallback(cb); + + queue->ScheduleJob(job); +} + +namespace { + +static void +FireControllerChangeOnDocument(nsIDocument* aDocument) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aDocument); + + nsCOMPtr<nsPIDOMWindowInner> w = aDocument->GetInnerWindow(); + if (!w) { + NS_WARNING("Failed to dispatch controllerchange event"); + return; + } + + auto* window = nsGlobalWindow::Cast(w.get()); + ErrorResult result; + dom::Navigator* navigator = window->GetNavigator(result); + if (NS_WARN_IF(result.Failed())) { + result.SuppressException(); + return; + } + + RefPtr<ServiceWorkerContainer> container = navigator->ServiceWorker(); + container->ControllerChanged(result); + if (result.Failed()) { + NS_WARNING("Failed to dispatch controllerchange event"); + } +} + +} // anonymous namespace + +UniquePtr<ServiceWorkerClientInfo> +ServiceWorkerManager::GetClient(nsIPrincipal* aPrincipal, + const nsAString& aClientId, + ErrorResult& aRv) +{ + UniquePtr<ServiceWorkerClientInfo> clientInfo; + nsCOMPtr<nsISupportsInterfacePointer> ifptr = + do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID); + if (NS_WARN_IF(!ifptr)) { + return clientInfo; + } + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return clientInfo; + } + + nsresult rv = obs->NotifyObservers(ifptr, "service-worker-get-client", + PromiseFlatString(aClientId).get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return clientInfo; + } + + nsCOMPtr<nsISupports> ptr; + ifptr->GetData(getter_AddRefs(ptr)); + nsCOMPtr<nsIDocument> doc = do_QueryInterface(ptr); + if (NS_WARN_IF(!doc)) { + return clientInfo; + } + + bool equals = false; + aPrincipal->Equals(doc->NodePrincipal(), &equals); + if (!equals) { + return clientInfo; + } + + if (!IsFromAuthenticatedOrigin(doc)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return clientInfo; + } + + clientInfo.reset(new ServiceWorkerClientInfo(doc)); + return clientInfo; +} + +void +ServiceWorkerManager::GetAllClients(nsIPrincipal* aPrincipal, + const nsCString& aScope, + bool aIncludeUncontrolled, + nsTArray<ServiceWorkerClientInfo>& aDocuments) +{ + MOZ_ASSERT(aPrincipal); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(aPrincipal, aScope); + + if (!registration) { + // The registration was removed, leave the array empty. + return; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return; + } + + nsCOMPtr<nsISimpleEnumerator> enumerator; + nsresult rv = obs->EnumerateObservers("service-worker-get-client", + getter_AddRefs(enumerator)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + auto ProcessDocument = [&aDocuments](nsIPrincipal* aPrincipal, nsIDocument* aDoc) { + if (!aDoc || !aDoc->GetWindow()) { + return; + } + + bool equals = false; + aPrincipal->Equals(aDoc->NodePrincipal(), &equals); + if (!equals) { + return; + } + + // Treat http windows with devtools opened as secure if the correct devtools + // setting is enabled. + if (!aDoc->GetWindow()->GetServiceWorkersTestingEnabled() && + !Preferences::GetBool("dom.serviceWorkers.testing.enabled") && + !IsFromAuthenticatedOrigin(aDoc)) { + return; + } + + ServiceWorkerClientInfo clientInfo(aDoc); + aDocuments.AppendElement(aDoc); + }; + + // Since it's not simple to check whether a document is in + // mControlledDocuments, we take different code paths depending on whether we + // need to look at all documents. The common parts of the two loops are + // factored out into the ProcessDocument lambda. + if (aIncludeUncontrolled) { + bool loop = true; + while (NS_SUCCEEDED(enumerator->HasMoreElements(&loop)) && loop) { + nsCOMPtr<nsISupports> ptr; + rv = enumerator->GetNext(getter_AddRefs(ptr)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(ptr); + ProcessDocument(aPrincipal, doc); + } + } else { + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* thisRegistration = iter.UserData(); + MOZ_ASSERT(thisRegistration); + if (!registration->mScope.Equals(thisRegistration->mScope)) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + + // All controlled documents must have an outer window. + MOZ_ASSERT(doc->GetWindow()); + + ProcessDocument(aPrincipal, doc); + } + } +} + +void +ServiceWorkerManager::MaybeClaimClient(nsIDocument* aDocument, + ServiceWorkerRegistrationInfo* aWorkerRegistration) +{ + MOZ_ASSERT(aWorkerRegistration); + MOZ_ASSERT(aWorkerRegistration->GetActive()); + + // Same origin check + if (!aWorkerRegistration->mPrincipal->Equals(aDocument->NodePrincipal())) { + return; + } + + // The registration that should be controlling the client + RefPtr<ServiceWorkerRegistrationInfo> matchingRegistration = + GetServiceWorkerRegistrationInfo(aDocument); + + // The registration currently controlling the client + RefPtr<ServiceWorkerRegistrationInfo> controllingRegistration; + GetDocumentRegistration(aDocument, getter_AddRefs(controllingRegistration)); + + if (aWorkerRegistration != matchingRegistration || + aWorkerRegistration == controllingRegistration) { + return; + } + + if (controllingRegistration) { + StopControllingADocument(controllingRegistration); + } + + StartControllingADocument(aWorkerRegistration, aDocument, NS_LITERAL_STRING("")); + FireControllerChangeOnDocument(aDocument); +} + +nsresult +ServiceWorkerManager::ClaimClients(nsIPrincipal* aPrincipal, + const nsCString& aScope, uint64_t aId) +{ + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(aPrincipal, aScope); + + if (!registration || !registration->GetActive() || + !(registration->GetActive()->ID() == aId)) { + // The worker is not active. + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISimpleEnumerator> enumerator; + nsresult rv = obs->EnumerateObservers("service-worker-get-client", + getter_AddRefs(enumerator)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool loop = true; + while (NS_SUCCEEDED(enumerator->HasMoreElements(&loop)) && loop) { + nsCOMPtr<nsISupports> ptr; + rv = enumerator->GetNext(getter_AddRefs(ptr)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(ptr); + MaybeClaimClient(doc, registration); + } + + return NS_OK; +} + +void +ServiceWorkerManager::SetSkipWaitingFlag(nsIPrincipal* aPrincipal, + const nsCString& aScope, + uint64_t aServiceWorkerID) +{ + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(aPrincipal, aScope); + if (NS_WARN_IF(!registration)) { + return; + } + + RefPtr<ServiceWorkerInfo> worker = + registration->GetServiceWorkerInfoById(aServiceWorkerID); + + if (NS_WARN_IF(!worker)) { + return; + } + + worker->SetSkipWaitingFlag(); + + if (worker->State() == ServiceWorkerState::Installed) { + registration->TryToActivateAsync(); + } +} + +void +ServiceWorkerManager::FireControllerChange(ServiceWorkerRegistrationInfo* aRegistration) +{ + AssertIsOnMainThread(); + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + if (iter.UserData() != aRegistration) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + if (NS_WARN_IF(!doc)) { + continue; + } + + FireControllerChangeOnDocument(doc); + } +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetRegistration(nsIPrincipal* aPrincipal, + const nsACString& aScope) const +{ + MOZ_ASSERT(aPrincipal); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return GetRegistration(scopeKey, aScope); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetRegistrationByPrincipal(nsIPrincipal* aPrincipal, + const nsAString& aScope, + nsIServiceWorkerRegistrationInfo** aInfo) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aInfo); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> info = + GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI); + if (!info) { + return NS_ERROR_FAILURE; + } + info.forget(aInfo); + + return NS_OK; +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetRegistration(const nsACString& aScopeKey, + const nsACString& aScope) const +{ + RefPtr<ServiceWorkerRegistrationInfo> reg; + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(aScopeKey, &data)) { + return reg.forget(); + } + + data->mInfos.Get(aScope, getter_AddRefs(reg)); + return reg.forget(); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::CreateNewRegistration(const nsCString& aScope, + nsIPrincipal* aPrincipal) +{ +#ifdef DEBUG + AssertIsOnMainThread(); + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + RefPtr<ServiceWorkerRegistrationInfo> tmp = + GetRegistration(aPrincipal, aScope); + MOZ_ASSERT(!tmp); +#endif + + RefPtr<ServiceWorkerRegistrationInfo> registration = + new ServiceWorkerRegistrationInfo(aScope, aPrincipal); + // From now on ownership of registration is with + // mServiceWorkerRegistrationInfos. + AddScopeAndRegistration(aScope, registration); + return registration.forget(); +} + +void +ServiceWorkerManager::MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration) +{ + MOZ_ASSERT(aRegistration); + RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); + if (!newest && HasScope(aRegistration->mPrincipal, aRegistration->mScope)) { + RemoveRegistration(aRegistration); + } +} + +void +ServiceWorkerManager::RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration) +{ + // Note, we do not need to call mActor->SendUnregister() here. There are a few + // ways we can get here: + // 1) Through a normal unregister which calls SendUnregister() in the unregister + // job Start() method. + // 2) Through origin storage being purged. These result in ForceUnregister() + // starting unregister jobs which in turn call SendUnregister(). + // 3) Through the failure to install a new service worker. Since we don't store + // the registration until install succeeds, we do not need to call + // SendUnregister here. + // Assert these conditions by testing for pending uninstall (cases 1 and 2) or + // null workers (case 3). +#ifdef DEBUG + RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); + MOZ_ASSERT(aRegistration->mPendingUninstall || !newest); +#endif + + MOZ_ASSERT(HasScope(aRegistration->mPrincipal, aRegistration->mScope)); + + // When a registration is removed, we must clear its contents since the DOM + // object may be held by content script. + aRegistration->Clear(); + + RemoveScopeAndRegistration(aRegistration); +} + +namespace { +/** + * See toolkit/modules/sessionstore/Utils.jsm function hasRootDomain(). + * + * Returns true if the |url| passed in is part of the given root |domain|. + * For example, if |url| is "www.mozilla.org", and we pass in |domain| as + * "mozilla.org", this will return true. It would return false the other way + * around. + */ +bool +HasRootDomain(nsIURI* aURI, const nsACString& aDomain) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aURI); + + nsAutoCString host; + nsresult rv = aURI->GetHost(host); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsACString::const_iterator start, end; + host.BeginReading(start); + host.EndReading(end); + if (!FindInReadable(aDomain, start, end)) { + return false; + } + + if (host.Equals(aDomain)) { + return true; + } + + // Beginning of the string matches, can't look at the previous char. + if (start.get() == host.BeginReading()) { + // Equals failed so this is fine. + return false; + } + + char prevChar = *(--start); + return prevChar == '.'; +} + +} // namespace + +NS_IMETHODIMP +ServiceWorkerManager::GetAllRegistrations(nsIArray** aResult) +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIMutableArray> array(do_CreateInstance(NS_ARRAY_CONTRACTID)); + if (!array) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + for (auto it2 = it1.UserData()->mInfos.Iter(); !it2.Done(); it2.Next()) { + ServiceWorkerRegistrationInfo* reg = it2.UserData(); + MOZ_ASSERT(reg); + + if (reg->mPendingUninstall) { + continue; + } + + array->AppendElement(reg, false); + } + } + + array.forget(aResult); + return NS_OK; +} + +// MUST ONLY BE CALLED FROM Remove(), RemoveAll() and RemoveAllRegistrations()! +void +ServiceWorkerManager::ForceUnregister(RegistrationDataPerPrincipal* aRegistrationData, + ServiceWorkerRegistrationInfo* aRegistration) +{ + MOZ_ASSERT(aRegistrationData); + MOZ_ASSERT(aRegistration); + + RefPtr<ServiceWorkerJobQueue> queue; + aRegistrationData->mJobQueues.Get(aRegistration->mScope, getter_AddRefs(queue)); + if (queue) { + queue->CancelAll(); + } + + nsCOMPtr<nsITimer> timer = + aRegistrationData->mUpdateTimers.Get(aRegistration->mScope); + if (timer) { + timer->Cancel(); + aRegistrationData->mUpdateTimers.Remove(aRegistration->mScope); + } + + // Since Unregister is async, it is ok to call it in an enumeration. + Unregister(aRegistration->mPrincipal, nullptr, NS_ConvertUTF8toUTF16(aRegistration->mScope)); +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveAndPropagate(const nsACString& aHost) +{ + Remove(aHost); + PropagateRemove(aHost); + return NS_OK; +} + +void +ServiceWorkerManager::Remove(const nsACString& aHost) +{ + AssertIsOnMainThread(); + + // We need to postpone this operation in case we don't have an actor because + // this is needed by the ForceUnregister. + if (!mActor) { + RefPtr<nsIRunnable> runnable = new RemoveRunnable(aHost); + AppendPendingOperation(runnable); + return; + } + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); + for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { + ServiceWorkerRegistrationInfo* reg = it2.UserData(); + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), it2.Key(), + nullptr, nullptr); + // This way subdomains are also cleared. + if (NS_SUCCEEDED(rv) && HasRootDomain(scopeURI, aHost)) { + ForceUnregister(data, reg); + } + } + } +} + +void +ServiceWorkerManager::PropagateRemove(const nsACString& aHost) +{ + AssertIsOnMainThread(); + + if (!mActor) { + RefPtr<nsIRunnable> runnable = new PropagateRemoveRunnable(aHost); + AppendPendingOperation(runnable); + return; + } + + mActor->SendPropagateRemove(nsCString(aHost)); +} + +void +ServiceWorkerManager::RemoveAll() +{ + AssertIsOnMainThread(); + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); + for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { + ServiceWorkerRegistrationInfo* reg = it2.UserData(); + ForceUnregister(data, reg); + } + } +} + +void +ServiceWorkerManager::PropagateRemoveAll() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!mActor) { + RefPtr<nsIRunnable> runnable = new PropagateRemoveAllRunnable(); + AppendPendingOperation(runnable); + return; + } + + mActor->SendPropagateRemoveAll(); +} + +void +ServiceWorkerManager::RemoveAllRegistrations(OriginAttributesPattern* aPattern) +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(aPattern); + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); + + // We can use iteration because ForceUnregister (and Unregister) are + // async. Otherwise doing some R/W operations on an hashtable during + // iteration will crash. + for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { + ServiceWorkerRegistrationInfo* reg = it2.UserData(); + + MOZ_ASSERT(reg); + MOZ_ASSERT(reg->mPrincipal); + + bool matches = + aPattern->Matches(BasePrincipal::Cast(reg->mPrincipal)->OriginAttributesRef()); + if (!matches) { + continue; + } + + ForceUnregister(data, reg); + } + } +} + +NS_IMETHODIMP +ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener) +{ + AssertIsOnMainThread(); + + if (!aListener || mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.AppendElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveListener(nsIServiceWorkerManagerListener* aListener) +{ + AssertIsOnMainThread(); + + if (!aListener || !mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::ShouldReportToWindow(mozIDOMWindowProxy* aWindow, + const nsACString& aScope, + bool* aResult) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aResult); + + *aResult = false; + + // Get the inner window ID to compare to our document windows below. + nsCOMPtr<nsPIDOMWindowOuter> targetWin = nsPIDOMWindowOuter::From(aWindow); + if (NS_WARN_IF(!targetWin)) { + return NS_OK; + } + + targetWin = targetWin->GetScriptableTop(); + uint64_t winId = targetWin->WindowID(); + + // Check our weak registering document references first. This way we clear + // out as many dead weak references as possible when this method is called. + WeakDocumentList* list = mRegisteringDocuments.Get(aScope); + if (list) { + for (int32_t i = list->Length() - 1; i >= 0; --i) { + nsCOMPtr<nsIDocument> doc = do_QueryReferent(list->ElementAt(i)); + if (!doc) { + list->RemoveElementAt(i); + continue; + } + + if (!doc->IsCurrentActiveDocument()) { + continue; + } + + nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow(); + if (!win) { + continue; + } + + win = win->GetScriptableTop(); + + // Match. We should report to this window. + if (win && winId == win->WindowID()) { + *aResult = true; + return NS_OK; + } + } + + if (list->IsEmpty()) { + list = nullptr; + nsAutoPtr<WeakDocumentList> doomed; + mRegisteringDocuments.RemoveAndForget(aScope, doomed); + } + } + + // Examine any windows performing a navigation that we are currently + // intercepting. + InterceptionList* intList = mNavigationInterceptions.Get(aScope); + if (intList) { + for (uint32_t i = 0; i < intList->Length(); ++i) { + nsCOMPtr<nsIInterceptedChannel> channel = intList->ElementAt(i); + + nsCOMPtr<nsIChannel> inner; + nsresult rv = channel->GetChannel(getter_AddRefs(inner)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + uint64_t id = nsContentUtils::GetInnerWindowID(inner); + if (id == 0) { + continue; + } + + nsCOMPtr<nsPIDOMWindowInner> win = nsGlobalWindow::GetInnerWindowWithId(id)->AsInner(); + if (!win) { + continue; + } + + nsCOMPtr<nsPIDOMWindowOuter> outer = win->GetScriptableTop(); + + // Match. We should report to this window. + if (outer && winId == outer->WindowID()) { + *aResult = true; + return NS_OK; + } + } + } + + // Next examine controlled documents to see if the windows match. + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* reg = iter.UserData(); + MOZ_ASSERT(reg); + if (!reg->mScope.Equals(aScope)) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + if (!doc || !doc->IsCurrentActiveDocument()) { + continue; + } + + nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow(); + if (!win) { + continue; + } + + win = win->GetScriptableTop(); + + // Match. We should report to this window. + if (win && winId == win->WindowID()) { + *aResult = true; + return NS_OK; + } + } + + // No match. We should not report to this window. + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + if (strcmp(aTopic, PURGE_SESSION_HISTORY) == 0) { + MOZ_ASSERT(XRE_IsParentProcess()); + RemoveAll(); + PropagateRemoveAll(); + return NS_OK; + } + + if (strcmp(aTopic, PURGE_DOMAIN_DATA) == 0) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsAutoString domain(aData); + RemoveAndPropagate(NS_ConvertUTF16toUTF8(domain)); + return NS_OK; + } + + if (strcmp(aTopic, CLEAR_ORIGIN_DATA) == 0) { + MOZ_ASSERT(XRE_IsParentProcess()); + OriginAttributesPattern pattern; + MOZ_ALWAYS_TRUE(pattern.Init(nsAutoString(aData))); + + RemoveAllRegistrations(&pattern); + return NS_OK; + } + + if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { + MaybeStartShutdown(); + return NS_OK; + } + + MOZ_CRASH("Received message we aren't supposed to be registered for!"); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::PropagateSoftUpdate(JS::Handle<JS::Value> aOriginAttributes, + const nsAString& aScope, + JSContext* aCx) +{ + AssertIsOnMainThread(); + + PrincipalOriginAttributes attrs; + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + PropagateSoftUpdate(attrs, aScope); + return NS_OK; +} + +void +ServiceWorkerManager::PropagateSoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, + const nsAString& aScope) +{ + AssertIsOnMainThread(); + + if (!mActor) { + RefPtr<nsIRunnable> runnable = + new PropagateSoftUpdateRunnable(aOriginAttributes, aScope); + AppendPendingOperation(runnable); + return; + } + + mActor->SendPropagateSoftUpdate(aOriginAttributes, nsString(aScope)); +} + +NS_IMETHODIMP +ServiceWorkerManager::PropagateUnregister(nsIPrincipal* aPrincipal, + nsIServiceWorkerUnregisterCallback* aCallback, + const nsAString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + if (!mActor) { + RefPtr<nsIRunnable> runnable = + new PropagateUnregisterRunnable(aPrincipal, aCallback, aScope); + AppendPendingOperation(runnable); + return NS_OK; + } + + PrincipalInfo principalInfo; + if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, + &principalInfo)))) { + return NS_ERROR_FAILURE; + } + + mActor->SendPropagateUnregister(principalInfo, nsString(aScope)); + + nsresult rv = Unregister(aPrincipal, aCallback, aScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +ServiceWorkerManager::NotifyListenersOnRegister( + nsIServiceWorkerRegistrationInfo* aInfo) +{ + nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(mListeners); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnRegister(aInfo); + } +} + +void +ServiceWorkerManager::NotifyListenersOnUnregister( + nsIServiceWorkerRegistrationInfo* aInfo) +{ + nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(mListeners); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnUnregister(aInfo); + } +} + +void +ServiceWorkerManager::AddRegisteringDocument(const nsACString& aScope, + nsIDocument* aDoc) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!aScope.IsEmpty()); + MOZ_ASSERT(aDoc); + + WeakDocumentList* list = mRegisteringDocuments.LookupOrAdd(aScope); + MOZ_ASSERT(list); + + for (int32_t i = list->Length() - 1; i >= 0; --i) { + nsCOMPtr<nsIDocument> existing = do_QueryReferent(list->ElementAt(i)); + if (!existing) { + list->RemoveElementAt(i); + continue; + } + if (existing == aDoc) { + return; + } + } + + list->AppendElement(do_GetWeakReference(aDoc)); +} + +class ServiceWorkerManager::InterceptionReleaseHandle final : public nsISupports +{ + const nsCString mScope; + + // Weak reference to channel is safe, because the channel holds a + // reference to this object. Also, the pointer is only used for + // comparison purposes. + nsIInterceptedChannel* mChannel; + + ~InterceptionReleaseHandle() + { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->RemoveNavigationInterception(mScope, mChannel); + } + } + +public: + InterceptionReleaseHandle(const nsACString& aScope, + nsIInterceptedChannel* aChannel) + : mScope(aScope) + , mChannel(aChannel) + { + AssertIsOnMainThread(); + MOZ_ASSERT(!aScope.IsEmpty()); + MOZ_ASSERT(mChannel); + } + + NS_DECL_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS0(ServiceWorkerManager::InterceptionReleaseHandle); + +void +ServiceWorkerManager::AddNavigationInterception(const nsACString& aScope, + nsIInterceptedChannel* aChannel) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!aScope.IsEmpty()); + MOZ_ASSERT(aChannel); + + InterceptionList* list = + mNavigationInterceptions.LookupOrAdd(aScope); + MOZ_ASSERT(list); + MOZ_ASSERT(!list->Contains(aChannel)); + + nsCOMPtr<nsISupports> releaseHandle = + new InterceptionReleaseHandle(aScope, aChannel); + aChannel->SetReleaseHandle(releaseHandle); + + list->AppendElement(aChannel); +} + +void +ServiceWorkerManager::RemoveNavigationInterception(const nsACString& aScope, + nsIInterceptedChannel* aChannel) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aChannel); + InterceptionList* list = + mNavigationInterceptions.Get(aScope); + if (list) { + MOZ_ALWAYS_TRUE(list->RemoveElement(aChannel)); + MOZ_ASSERT(!list->Contains(aChannel)); + if (list->IsEmpty()) { + list = nullptr; + nsAutoPtr<InterceptionList> doomed; + mNavigationInterceptions.RemoveAndForget(aScope, doomed); + } + } +} + +class UpdateTimerCallback final : public nsITimerCallback +{ + nsCOMPtr<nsIPrincipal> mPrincipal; + const nsCString mScope; + + ~UpdateTimerCallback() + { + } + +public: + UpdateTimerCallback(nsIPrincipal* aPrincipal, const nsACString& aScope) + : mPrincipal(aPrincipal) + , mScope(aScope) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mPrincipal); + MOZ_ASSERT(!mScope.IsEmpty()); + } + + NS_IMETHOD + Notify(nsITimer* aTimer) override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // shutting down, do nothing + return NS_OK; + } + + swm->UpdateTimerFired(mPrincipal, mScope); + return NS_OK; + } + + NS_DECL_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS(UpdateTimerCallback, nsITimerCallback) + +bool +ServiceWorkerManager::MayHaveActiveServiceWorkerInstance(ContentParent* aContent, + nsIPrincipal* aPrincipal) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + if (mShuttingDown) { + return false; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return false; + } + + return true; +} + +void +ServiceWorkerManager::ScheduleUpdateTimer(nsIPrincipal* aPrincipal, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aScope.IsEmpty()); + + if (mShuttingDown) { + return; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + nsCOMPtr<nsITimer> timer = data->mUpdateTimers.Get(aScope); + if (timer) { + // There is already a timer scheduled. In this case just use the original + // schedule time. We don't want to push it out to a later time since that + // could allow updates to be starved forever if events are continuously + // fired. + return; + } + + timer = do_CreateInstance("@mozilla.org/timer;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsITimerCallback> callback = new UpdateTimerCallback(aPrincipal, + aScope); + + const uint32_t UPDATE_DELAY_MS = 1000; + + rv = timer->InitWithCallback(callback, UPDATE_DELAY_MS, + nsITimer::TYPE_ONE_SHOT); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + data->mUpdateTimers.Put(aScope, timer); +} + +void +ServiceWorkerManager::UpdateTimerFired(nsIPrincipal* aPrincipal, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aScope.IsEmpty()); + + if (mShuttingDown) { + return; + } + + // First cleanup the timer. + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + nsCOMPtr<nsITimer> timer = data->mUpdateTimers.Get(aScope); + if (timer) { + timer->Cancel(); + data->mUpdateTimers.Remove(aScope); + } + + RefPtr<ServiceWorkerRegistrationInfo> registration; + data->mInfos.Get(aScope, getter_AddRefs(registration)); + if (!registration) { + return; + } + + if (!registration->CheckAndClearIfUpdateNeeded()) { + return; + } + + PrincipalOriginAttributes attrs = + BasePrincipal::Cast(aPrincipal)->OriginAttributesRef(); + + SoftUpdate(attrs, aScope); +} + +void +ServiceWorkerManager::MaybeSendUnregister(nsIPrincipal* aPrincipal, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aScope.IsEmpty()); + + if (!mActor) { + return; + } + + PrincipalInfo principalInfo; + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + Unused << mActor->SendUnregister(principalInfo, NS_ConvertUTF8toUTF16(aScope)); +} + +END_WORKERS_NAMESPACE |