gecko-dev/dom/serviceworkers/ServiceWorkerManager.cpp

3365 lines
108 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- 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 <algorithm>
#include "nsCOMPtr.h"
#include "nsICookieJarSettings.h"
#include "nsIHttpChannel.h"
#include "nsIHttpChannelInternal.h"
#include "nsINamed.h"
#include "nsINetworkInterceptController.h"
#include "nsIMutableArray.h"
#include "nsIPrincipal.h"
#include "nsITimer.h"
#include "nsIUploadChannel2.h"
#include "nsServiceManagerUtils.h"
#include "nsDebug.h"
#include "nsIPermissionManager.h"
#include "nsXULAppAPI.h"
#include "jsapi.h"
#include "mozilla/AppShutdown.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/LoadContext.h"
#include "mozilla/MozPromise.h"
#include "mozilla/Result.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/Telemetry.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/ClientHandle.h"
#include "mozilla/dom/ClientManager.h"
#include "mozilla/dom/ClientSource.h"
#include "mozilla/dom/ConsoleUtils.h"
#include "mozilla/dom/ContentParent.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/Promise.h"
#include "mozilla/dom/PromiseNativeHandler.h"
#include "mozilla/dom/Request.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/dom/TypedArray.h"
#include "mozilla/dom/SharedWorker.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerRunnable.h"
#include "mozilla/dom/WorkerScope.h"
#include "mozilla/extensions/WebExtensionPolicy.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozilla/dom/ScriptLoader.h"
#include "mozilla/PermissionManager.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_extensions.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/StoragePrincipalHelper.h"
#include "mozilla/Unused.h"
#include "mozilla/EnumSet.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsIDUtils.h"
#include "nsNetUtil.h"
#include "nsProxyRelease.h"
#include "nsQueryObject.h"
#include "nsTArray.h"
#include "ServiceWorker.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 "ServiceWorkerShutdownBlocker.h"
#include "ServiceWorkerEvents.h"
#include "ServiceWorkerUnregisterJob.h"
#include "ServiceWorkerUpdateJob.h"
#include "ServiceWorkerUtils.h"
#include "ServiceWorkerQuotaUtils.h"
#ifdef PostMessage
# undef PostMessage
#endif
mozilla::LazyLogModule sWorkerTelemetryLog("WorkerTelemetry");
#ifdef LOG
# undef LOG
#endif
#define LOG(_args) MOZ_LOG(sWorkerTelemetryLog, LogLevel::Debug, _args);
using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::ipc;
namespace mozilla::dom {
// Counts the number of registered ServiceWorkers, and the number that
// handle Fetch, for reporting in Telemetry
uint32_t gServiceWorkersRegistered = 0;
uint32_t gServiceWorkersRegisteredFetch = 0;
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 == RequestRedirectValues::Count,
"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 == RequestCacheValues::Count,
"RequestCache enumeration value should match Necko Cache mode value.");
static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::Imports) ==
nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
"nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*"
" should match ServiceWorkerUpdateViaCache enumeration.");
static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::All) ==
nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL,
"nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*"
" should match ServiceWorkerUpdateViaCache enumeration.");
static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::None) ==
nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE,
"nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*"
" should match ServiceWorkerUpdateViaCache enumeration.");
static StaticRefPtr<ServiceWorkerManager> gInstance;
namespace {
nsresult PopulateRegistrationData(
nsIPrincipal* aPrincipal,
const ServiceWorkerRegistrationInfo* aRegistration,
ServiceWorkerRegistrationData& aData) {
MOZ_ASSERT(aPrincipal);
MOZ_ASSERT(aRegistration);
if (NS_WARN_IF(!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal())) {
return NS_ERROR_FAILURE;
}
nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aData.principal());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
aData.scope() = aRegistration->Scope();
// TODO: When bug 1426401 is implemented we will need to handle more
// than just the active worker here.
RefPtr<ServiceWorkerInfo> active = aRegistration->GetActive();
MOZ_ASSERT(active);
if (NS_WARN_IF(!active)) {
return NS_ERROR_FAILURE;
}
aData.currentWorkerURL() = active->ScriptSpec();
aData.cacheName() = active->CacheName();
aData.currentWorkerHandlesFetch() = active->HandlesFetch();
aData.currentWorkerInstalledTime() = active->GetInstalledTime();
aData.currentWorkerActivatedTime() = active->GetActivatedTime();
aData.updateViaCache() =
static_cast<uint32_t>(aRegistration->GetUpdateViaCache());
aData.lastUpdateTime() = aRegistration->GetLastUpdateTime();
aData.navigationPreloadState() = aRegistration->GetNavigationPreloadState();
MOZ_ASSERT(ServiceWorkerRegistrationDataIsValid(aData));
return NS_OK;
}
class TeardownRunnable final : public Runnable {
public:
explicit TeardownRunnable(ServiceWorkerManagerChild* aActor)
: Runnable("dom::ServiceWorkerManager::TeardownRunnable"),
mActor(aActor) {
MOZ_ASSERT(mActor);
}
NS_IMETHOD Run() override {
MOZ_ASSERT(mActor);
PServiceWorkerManagerChild::Send__delete__(mActor);
return NS_OK;
}
private:
~TeardownRunnable() = default;
RefPtr<ServiceWorkerManagerChild> mActor;
};
constexpr char kFinishShutdownTopic[] = "profile-before-change-qm";
already_AddRefed<nsIAsyncShutdownClient> GetAsyncShutdownBarrier() {
AssertIsOnMainThread();
nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
MOZ_ASSERT(svc);
nsCOMPtr<nsIAsyncShutdownClient> barrier;
DebugOnly<nsresult> rv =
svc->GetProfileChangeTeardown(getter_AddRefs(barrier));
MOZ_ASSERT(NS_SUCCEEDED(rv));
return barrier.forget();
}
Result<nsCOMPtr<nsIPrincipal>, nsresult> ScopeToPrincipal(
nsIURI* aScopeURI, const OriginAttributes& aOriginAttributes) {
MOZ_ASSERT(aScopeURI);
nsCOMPtr<nsIPrincipal> principal =
BasePrincipal::CreateContentPrincipal(aScopeURI, aOriginAttributes);
if (NS_WARN_IF(!principal)) {
return Err(NS_ERROR_FAILURE);
}
return principal;
}
Result<nsCOMPtr<nsIPrincipal>, nsresult> ScopeToPrincipal(
const nsACString& aScope, const OriginAttributes& aOriginAttributes) {
MOZ_ASSERT(nsContentUtils::IsAbsoluteURL(aScope));
nsCOMPtr<nsIURI> scopeURI;
MOZ_TRY(NS_NewURI(getter_AddRefs(scopeURI), aScope));
return ScopeToPrincipal(scopeURI, aOriginAttributes);
}
} // namespace
struct ServiceWorkerManager::RegistrationDataPerPrincipal final {
// Implements a container of keys for the "scope to registration map":
// https://w3c.github.io/ServiceWorker/#dfn-scope-to-registration-map
//
// where each key is an absolute URL.
//
// The properties of this map that the spec uses are
// 1) insertion,
// 2) removal,
// 3) iteration of scopes in FIFO order (excluding removed scopes),
// 4) and finding, for a given path, the maximal length scope which is a
// prefix of the path.
//
// Additionally, because this is a container of keys for a map, there
// shouldn't be duplicate scopes.
//
// The current implementation uses a dynamic array as the underlying
// container, which is not optimal for unbounded container sizes (all
// supported operations are in linear time) but may be superior for small
// container sizes.
//
// If this is proven to be too slow, the underlying storage should be replaced
// with a linked list of scopes in combination with an ordered map that maps
// scopes to linked list elements/iterators. This would reduce all of the
// above operations besides iteration (necessarily linear) to logarithmic
// time.
class ScopeContainer final : private nsTArray<nsCString> {
using Base = nsTArray<nsCString>;
public:
using Base::Contains;
using Base::IsEmpty;
using Base::Length;
// No using-declaration to avoid importing the non-const overload.
decltype(auto) operator[](Base::index_type aIndex) const {
return Base::operator[](aIndex);
}
void InsertScope(const nsACString& aScope) {
MOZ_DIAGNOSTIC_ASSERT(nsContentUtils::IsAbsoluteURL(aScope));
if (Contains(aScope)) {
return;
}
AppendElement(aScope);
}
void RemoveScope(const nsACString& aScope) {
MOZ_ALWAYS_TRUE(RemoveElement(aScope));
}
// Implements most of "Match Service Worker Registration":
// https://w3c.github.io/ServiceWorker/#scope-match-algorithm
Maybe<nsCString> MatchScope(const nsACString& aClientUrl) const {
Maybe<nsCString> match;
for (const nsCString& scope : *this) {
if (StringBeginsWith(aClientUrl, scope)) {
if (!match || scope.Length() > match->Length()) {
match = Some(scope);
}
}
}
// Step 7.2:
// "Assert: matchingScopes origin and clientURLs origin are same
// origin."
MOZ_DIAGNOSTIC_ASSERT_IF(match, IsSameOrigin(*match, aClientUrl));
return match;
}
private:
bool IsSameOrigin(const nsACString& aMatchingScope,
const nsACString& aClientUrl) const {
auto parseResult = ScopeToPrincipal(aMatchingScope, OriginAttributes());
if (NS_WARN_IF(parseResult.isErr())) {
return false;
}
auto scopePrincipal = parseResult.unwrap();
parseResult = ScopeToPrincipal(aClientUrl, OriginAttributes());
if (NS_WARN_IF(parseResult.isErr())) {
return false;
}
auto clientPrincipal = parseResult.unwrap();
bool equals = false;
if (NS_WARN_IF(
NS_FAILED(scopePrincipal->Equals(clientPrincipal, &equals)))) {
return false;
}
return equals;
}
};
ScopeContainer mScopeContainer;
// 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;
// The number of times we have done a quota usage check for this origin for
// mitigation purposes. See the docs on nsIServiceWorkerRegistrationInfo,
// where this value is exposed.
int32_t mQuotaUsageCheckCount = 0;
};
//////////////////////////
// ServiceWorkerManager //
//////////////////////////
NS_IMPL_ADDREF(ServiceWorkerManager)
NS_IMPL_RELEASE(ServiceWorkerManager)
NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager)
NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager)
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();
// This can happen if the browser is started up in ProfileManager mode, in
// which case XPCOM will startup and shutdown, but there won't be any
// profile-* topic notifications. The shutdown blocker expects to be in a
// NotAcceptingPromises state when it's destroyed, and this transition
// normally happens in the "profile-change-teardown" notification callback
// (which won't be called in ProfileManager mode).
if (!mShuttingDown && mShutdownBlocker) {
mShutdownBlocker->StopAcceptingPromises();
}
}
void ServiceWorkerManager::BlockShutdownOn(GenericNonExclusivePromise* aPromise,
uint32_t aShutdownStateId) {
AssertIsOnMainThread();
MOZ_ASSERT(mShutdownBlocker);
MOZ_ASSERT(aPromise);
mShutdownBlocker->WaitOnPromise(aPromise, aShutdownStateId);
}
void ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) {
// ServiceWorkers now only support parent intercept. In parent intercept
// mode, only the parent process ServiceWorkerManager has any state or does
// anything.
//
// It is our goal to completely eliminate support for content process
// ServiceWorkerManager instances and make getting a SWM instance trigger a
// fatal assertion. But until we've reached that point, we make
// initialization a no-op so that content process ServiceWorkerManager
// instances will simply have no state and no registrations.
if (!XRE_IsParentProcess()) {
return;
}
nsCOMPtr<nsIAsyncShutdownClient> shutdownBarrier = GetAsyncShutdownBarrier();
if (shutdownBarrier) {
mShutdownBlocker = ServiceWorkerShutdownBlocker::CreateAndRegisterOn(
*shutdownBarrier, *this);
MOZ_ASSERT(mShutdownBlocker);
}
MOZ_DIAGNOSTIC_ASSERT(aRegistrar);
nsTArray<ServiceWorkerRegistrationData> data;
aRegistrar->GetRegistrations(data);
LoadRegistrations(data);
PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!actorChild)) {
MaybeStartShutdown();
return;
}
PServiceWorkerManagerChild* actor =
actorChild->SendPServiceWorkerManagerConstructor();
if (!actor) {
MaybeStartShutdown();
return;
}
mActor = static_cast<ServiceWorkerManagerChild*>(actor);
mTelemetryLastChange = TimeStamp::Now();
}
void ServiceWorkerManager::RecordTelemetry(uint32_t aNumber, uint32_t aFetch) {
// Submit N value pairs to Telemetry for the time we were at those values
auto now = TimeStamp::Now();
// round down, with a minimum of 1 repeat. In theory this gives
// inaccuracy if there are frequent changes, but that's uncommon.
uint32_t repeats = (uint32_t)((now - mTelemetryLastChange).ToMilliseconds()) /
mTelemetryPeriodMs;
mTelemetryLastChange = now;
if (repeats == 0) {
repeats = 1;
}
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
"ServiceWorkerTelemetryRunnable", [aNumber, aFetch, repeats]() {
LOG(("ServiceWorkers running: %u samples of %u/%u", repeats, aNumber,
aFetch));
// Don't allocate infinitely huge arrays if someone visits a SW site
// after a few months running. 1 month is about 500K repeats @ 5s
// sampling
uint32_t num_repeats = std::min(repeats, 1000000U); // 4MB max
nsTArray<uint32_t> values;
uint32_t* array = values.AppendElements(num_repeats);
for (uint32_t i = 0; i < num_repeats; i++) {
array[i] = aNumber;
}
Telemetry::Accumulate(Telemetry::SERVICE_WORKER_RUNNING, "All"_ns,
values);
for (uint32_t i = 0; i < num_repeats; i++) {
array[i] = aFetch;
}
Telemetry::Accumulate(Telemetry::SERVICE_WORKER_RUNNING, "Fetch"_ns,
values);
});
NS_DispatchBackgroundTask(runnable.forget(), nsIEventTarget::DISPATCH_NORMAL);
}
RefPtr<GenericErrorResultPromise> ServiceWorkerManager::StartControllingClient(
const ClientInfo& aClientInfo,
ServiceWorkerRegistrationInfo* aRegistrationInfo,
bool aControlClientHandle) {
MOZ_DIAGNOSTIC_ASSERT(aRegistrationInfo->GetActive());
// XXX We can't use a generic lambda (accepting auto&& entry) like elsewhere
// with WithEntryHandle, since we get linker errors then using clang+lld. This
// might be a toolchain issue?
return mControlledClients.WithEntryHandle(
aClientInfo.Id(),
[&](decltype(mControlledClients)::EntryHandle&& entry)
-> RefPtr<GenericErrorResultPromise> {
const RefPtr<ServiceWorkerManager> self = this;
const ServiceWorkerDescriptor& active =
aRegistrationInfo->GetActive()->Descriptor();
if (entry) {
const RefPtr<ServiceWorkerRegistrationInfo> old =
std::move(entry.Data()->mRegistrationInfo);
const RefPtr<GenericErrorResultPromise> promise =
aControlClientHandle
? entry.Data()->mClientHandle->Control(active)
: GenericErrorResultPromise::CreateAndResolve(false,
__func__);
entry.Data()->mRegistrationInfo = aRegistrationInfo;
if (old != aRegistrationInfo) {
StopControllingRegistration(old);
aRegistrationInfo->StartControllingClient();
}
Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS,
1);
// Always check to see if we failed to actually control the client. In
// that case remove the client from our list of controlled clients.
return promise->Then(
GetMainThreadSerialEventTarget(), __func__,
[](bool) {
// do nothing on success
return GenericErrorResultPromise::CreateAndResolve(true,
__func__);
},
[self, aClientInfo](const CopyableErrorResult& aRv) {
// failed to control, forget about this client
self->StopControllingClient(aClientInfo);
return GenericErrorResultPromise::CreateAndReject(aRv,
__func__);
});
}
RefPtr<ClientHandle> clientHandle = ClientManager::CreateHandle(
aClientInfo, GetMainThreadSerialEventTarget());
const RefPtr<GenericErrorResultPromise> promise =
aControlClientHandle
? clientHandle->Control(active)
: GenericErrorResultPromise::CreateAndResolve(false, __func__);
aRegistrationInfo->StartControllingClient();
entry.Insert(
MakeUnique<ControlledClientData>(clientHandle, aRegistrationInfo));
clientHandle->OnDetach()->Then(
GetMainThreadSerialEventTarget(), __func__,
[self, aClientInfo] { self->StopControllingClient(aClientInfo); });
Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS,
1);
// Always check to see if we failed to actually control the client. In
// that case removed the client from our list of controlled clients.
return promise->Then(
GetMainThreadSerialEventTarget(), __func__,
[](bool) {
// do nothing on success
return GenericErrorResultPromise::CreateAndResolve(true,
__func__);
},
[self, aClientInfo](const CopyableErrorResult& aRv) {
// failed to control, forget about this client
self->StopControllingClient(aClientInfo);
return GenericErrorResultPromise::CreateAndReject(aRv, __func__);
});
});
}
void ServiceWorkerManager::StopControllingClient(
const ClientInfo& aClientInfo) {
auto entry = mControlledClients.Lookup(aClientInfo.Id());
if (!entry) {
return;
}
RefPtr<ServiceWorkerRegistrationInfo> reg =
std::move(entry.Data()->mRegistrationInfo);
entry.Remove();
StopControllingRegistration(reg);
}
void ServiceWorkerManager::MaybeStartShutdown() {
MOZ_ASSERT(NS_IsMainThread());
if (mShuttingDown) {
return;
}
mShuttingDown = true;
for (const auto& dataPtr : mRegistrationInfos.Values()) {
for (const auto& timerEntry : dataPtr->mUpdateTimers.Values()) {
timerEntry->Cancel();
}
dataPtr->mUpdateTimers.Clear();
for (const auto& queueEntry : dataPtr->mJobQueues.Values()) {
queueEntry->CancelAll();
}
dataPtr->mJobQueues.Clear();
for (const auto& registrationEntry : dataPtr->mInfos.Values()) {
registrationEntry->ShutdownWorkers();
}
// ServiceWorkerCleanup may try to unregister registrations, so don't clear
// mInfos.
}
for (const auto& entry : mControlledClients.Values()) {
entry->mRegistrationInfo->ShutdownWorkers();
}
for (auto iter = mOrphanedRegistrations.iter(); !iter.done(); iter.next()) {
iter.get()->ShutdownWorkers();
}
if (mShutdownBlocker) {
mShutdownBlocker->StopAcceptingPromises();
}
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
obs->AddObserver(this, kFinishShutdownTopic, false);
return;
}
MaybeFinishShutdown();
}
void ServiceWorkerManager::MaybeFinishShutdown() {
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
obs->RemoveObserver(this, kFinishShutdownTopic);
}
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;
// This also submits final telemetry
ServiceWorkerPrivate::RunningShutdown();
}
class ServiceWorkerResolveWindowPromiseOnRegisterCallback final
: public ServiceWorkerJob::Callback {
public:
NS_INLINE_DECL_REFCOUNTING(
ServiceWorkerResolveWindowPromiseOnRegisterCallback, override)
virtual void JobFinished(ServiceWorkerJob* aJob,
ErrorResult& aStatus) override {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aJob);
if (aStatus.Failed()) {
mPromiseHolder.Reject(CopyableErrorResult(aStatus), __func__);
return;
}
MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Register);
RefPtr<ServiceWorkerRegisterJob> registerJob =
static_cast<ServiceWorkerRegisterJob*>(aJob);
RefPtr<ServiceWorkerRegistrationInfo> reg = registerJob->GetRegistration();
mPromiseHolder.Resolve(reg->Descriptor(), __func__);
}
virtual void JobDiscarded(ErrorResult& aStatus) override {
MOZ_ASSERT(NS_IsMainThread());
mPromiseHolder.Reject(CopyableErrorResult(aStatus), __func__);
}
RefPtr<ServiceWorkerRegistrationPromise> Promise() {
MOZ_ASSERT(NS_IsMainThread());
return mPromiseHolder.Ensure(__func__);
}
private:
~ServiceWorkerResolveWindowPromiseOnRegisterCallback() = default;
MozPromiseHolder<ServiceWorkerRegistrationPromise> mPromiseHolder;
};
NS_IMETHODIMP
ServiceWorkerManager::RegisterForTest(nsIPrincipal* aPrincipal,
const nsAString& aScopeURL,
const nsAString& aScriptURL,
JSContext* aCx,
mozilla::dom::Promise** aPromise) {
nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
if (NS_WARN_IF(!global)) {
return NS_ERROR_FAILURE;
}
ErrorResult erv;
RefPtr<Promise> outer = Promise::Create(global, erv);
if (NS_WARN_IF(erv.Failed())) {
return erv.StealNSResult();
}
if (!StaticPrefs::dom_serviceWorkers_testing_enabled()) {
outer->MaybeRejectWithAbortError(
"registerForTest only allowed when dom.serviceWorkers.testing.enabled "
"is true");
outer.forget(aPromise);
return NS_OK;
}
if (aPrincipal == nullptr) {
outer->MaybeRejectWithAbortError("Missing principal");
outer.forget(aPromise);
return NS_OK;
}
if (aScriptURL.IsEmpty()) {
outer->MaybeRejectWithAbortError("Missing script url");
outer.forget(aPromise);
return NS_OK;
}
if (aScopeURL.IsEmpty()) {
outer->MaybeRejectWithAbortError("Missing scope url");
outer.forget(aPromise);
return NS_OK;
}
// The ClientType isn't really used here, but ClientType::Window
// is the least bad choice since this is happening on the main thread.
Maybe<ClientInfo> clientInfo =
dom::ClientManager::CreateInfo(ClientType::Window, aPrincipal);
if (!clientInfo.isSome()) {
outer->MaybeRejectWithUnknownError("Error creating clientInfo");
outer.forget(aPromise);
return NS_OK;
}
auto scope = NS_ConvertUTF16toUTF8(aScopeURL);
auto scriptURL = NS_ConvertUTF16toUTF8(aScriptURL);
auto regPromise = Register(clientInfo.ref(), scope, scriptURL,
dom::ServiceWorkerUpdateViaCache::Imports);
const RefPtr<ServiceWorkerManager> self(this);
const nsCOMPtr<nsIPrincipal> principal(aPrincipal);
regPromise->Then(
GetMainThreadSerialEventTarget(), __func__,
[self, outer, principal,
scope](const ServiceWorkerRegistrationDescriptor& regDesc) {
RefPtr<ServiceWorkerRegistrationInfo> registration =
self->GetRegistration(principal, NS_ConvertUTF16toUTF8(scope));
if (registration) {
outer->MaybeResolve(registration);
} else {
outer->MaybeRejectWithUnknownError(
"Failed to retrieve ServiceWorkerRegistrationInfo");
}
},
[outer](const mozilla::CopyableErrorResult& err) {
CopyableErrorResult result(err);
outer->MaybeReject(std::move(result));
});
outer.forget(aPromise);
return NS_OK;
}
RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::Register(
const ClientInfo& aClientInfo, const nsACString& aScopeURL,
const nsACString& aScriptURL, ServiceWorkerUpdateViaCache aUpdateViaCache) {
nsCOMPtr<nsIURI> scopeURI;
nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScopeURL);
if (NS_FAILED(rv)) {
// Odd, since it was serialiazed from an nsIURI.
CopyableErrorResult err;
err.ThrowInvalidStateError("Scope URL cannot be parsed");
return ServiceWorkerRegistrationPromise::CreateAndReject(err, __func__);
}
nsCOMPtr<nsIURI> scriptURI;
rv = NS_NewURI(getter_AddRefs(scriptURI), aScriptURL);
if (NS_FAILED(rv)) {
// Odd, since it was serialiazed from an nsIURI.
CopyableErrorResult err;
err.ThrowInvalidStateError("Script URL cannot be parsed");
return ServiceWorkerRegistrationPromise::CreateAndReject(err, __func__);
}
IgnoredErrorResult err;
ServiceWorkerScopeAndScriptAreValid(aClientInfo, scopeURI, scriptURI, err);
if (err.Failed()) {
return ServiceWorkerRegistrationPromise::CreateAndReject(
CopyableErrorResult(std::move(err)), __func__);
}
// If the previous validation step passed then we must have a principal.
auto principalOrErr = aClientInfo.GetPrincipal();
if (NS_WARN_IF(principalOrErr.isErr())) {
return ServiceWorkerRegistrationPromise::CreateAndReject(
CopyableErrorResult(principalOrErr.unwrapErr()), __func__);
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
nsAutoCString scopeKey;
rv = PrincipalToScopeKey(principal, scopeKey);
if (NS_WARN_IF(NS_FAILED(rv))) {
return ServiceWorkerRegistrationPromise::CreateAndReject(
CopyableErrorResult(rv), __func__);
}
RefPtr<ServiceWorkerJobQueue> queue =
GetOrCreateJobQueue(scopeKey, aScopeURL);
RefPtr<ServiceWorkerResolveWindowPromiseOnRegisterCallback> cb =
new ServiceWorkerResolveWindowPromiseOnRegisterCallback();
RefPtr<ServiceWorkerRegisterJob> job = new ServiceWorkerRegisterJob(
principal, aScopeURL, aScriptURL,
static_cast<ServiceWorkerUpdateViaCache>(aUpdateViaCache));
job->AppendResultCallback(cb);
queue->ScheduleJob(job);
MOZ_ASSERT(NS_IsMainThread());
return cb->Promise();
}
/*
* Implements the async aspects of the getRegistrations algorithm.
*/
class GetRegistrationsRunnable final : public Runnable {
const ClientInfo mClientInfo;
RefPtr<ServiceWorkerRegistrationListPromise::Private> mPromise;
public:
explicit GetRegistrationsRunnable(const ClientInfo& aClientInfo)
: Runnable("dom::ServiceWorkerManager::GetRegistrationsRunnable"),
mClientInfo(aClientInfo),
mPromise(new ServiceWorkerRegistrationListPromise::Private(__func__)) {}
RefPtr<ServiceWorkerRegistrationListPromise> Promise() const {
return mPromise;
}
NS_IMETHOD
Run() override {
auto scopeExit = MakeScopeExit(
[&] { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); });
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
if (!swm) {
return NS_OK;
}
auto principalOrErr = mClientInfo.GetPrincipal();
if (NS_WARN_IF(principalOrErr.isErr())) {
return NS_OK;
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
nsTArray<ServiceWorkerRegistrationDescriptor> array;
if (NS_WARN_IF(!BasePrincipal::Cast(principal)->IsContentPrincipal())) {
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)) {
scopeExit.release();
mPromise->Resolve(array, __func__);
return NS_OK;
}
for (uint32_t i = 0; i < data->mScopeContainer.Length(); ++i) {
RefPtr<ServiceWorkerRegistrationInfo> info =
data->mInfos.GetWeak(data->mScopeContainer[i]);
NS_ConvertUTF8toUTF16 scope(data->mScopeContainer[i]);
nsCOMPtr<nsIURI> scopeURI;
nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope);
if (NS_WARN_IF(NS_FAILED(rv))) {
break;
}
// Unfortunately we don't seem to have an obvious window id here; in
// particular ClientInfo does not have one, and neither do service worker
// registrations, as far as I can tell.
rv = principal->CheckMayLoadWithReporting(
scopeURI, false /* allowIfInheritsPrincipal */,
0 /* innerWindowID */);
if (NS_WARN_IF(NS_FAILED(rv))) {
continue;
}
array.AppendElement(info->Descriptor());
}
scopeExit.release();
mPromise->Resolve(array, __func__);
return NS_OK;
}
};
RefPtr<ServiceWorkerRegistrationListPromise>
ServiceWorkerManager::GetRegistrations(const ClientInfo& aClientInfo) const {
RefPtr<GetRegistrationsRunnable> runnable =
new GetRegistrationsRunnable(aClientInfo);
MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable));
return runnable->Promise();
}
/*
* Implements the async aspects of the getRegistration algorithm.
*/
class GetRegistrationRunnable final : public Runnable {
const ClientInfo mClientInfo;
RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise;
nsCString mURL;
public:
GetRegistrationRunnable(const ClientInfo& aClientInfo, const nsACString& aURL)
: Runnable("dom::ServiceWorkerManager::GetRegistrationRunnable"),
mClientInfo(aClientInfo),
mPromise(new ServiceWorkerRegistrationPromise::Private(__func__)),
mURL(aURL) {}
RefPtr<ServiceWorkerRegistrationPromise> Promise() const { return mPromise; }
NS_IMETHOD
Run() override {
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
if (!swm) {
mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__);
return NS_OK;
}
auto principalOrErr = mClientInfo.GetPrincipal();
if (NS_WARN_IF(principalOrErr.isErr())) {
mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__);
return NS_OK;
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL);
if (NS_WARN_IF(NS_FAILED(rv))) {
mPromise->Reject(rv, __func__);
return NS_OK;
}
// Unfortunately we don't seem to have an obvious window id here; in
// particular ClientInfo does not have one, and neither do service worker
// registrations, as far as I can tell.
rv = principal->CheckMayLoadWithReporting(
uri, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */);
if (NS_FAILED(rv)) {
mPromise->Reject(NS_ERROR_DOM_SECURITY_ERR, __func__);
return NS_OK;
}
RefPtr<ServiceWorkerRegistrationInfo> registration =
swm->GetServiceWorkerRegistrationInfo(principal, uri);
if (!registration) {
// Reject with NS_OK means "not found".
mPromise->Reject(NS_OK, __func__);
return NS_OK;
}
mPromise->Resolve(registration->Descriptor(), __func__);
return NS_OK;
}
};
RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::GetRegistration(
const ClientInfo& aClientInfo, const nsACString& aURL) const {
MOZ_ASSERT(NS_IsMainThread());
RefPtr<GetRegistrationRunnable> runnable =
new GetRegistrationRunnable(aClientInfo, aURL);
MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable));
return runnable->Promise();
}
NS_IMETHODIMP
ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes,
const nsACString& aScope,
const nsTArray<uint8_t>& aDataBytes,
uint8_t optional_argc) {
if (optional_argc == 1) {
// This does one copy here (while constructing the Maybe) and another when
// we end up copying into the SendPushEventRunnable. We could fix that to
// only do one copy by making things between here and there take
// Maybe<nsTArray<uint8_t>>&&, but then we'd need to copy before we know
// whether we really need to in PushMessageDispatcher::NotifyWorkers. Since
// in practice this only affects JS callers that pass data, and we don't
// have any right now, let's not worry about it.
return SendPushEvent(aOriginAttributes, aScope, u""_ns,
Some(aDataBytes.Clone()));
}
MOZ_ASSERT(optional_argc == 0);
return SendPushEvent(aOriginAttributes, aScope, u""_ns, Nothing());
}
nsresult ServiceWorkerManager::SendPushEvent(
const nsACString& aOriginAttributes, const nsACString& aScope,
const nsAString& aMessageId, const Maybe<nsTArray<uint8_t>>& aData) {
OriginAttributes attrs;
if (!attrs.PopulateFromSuffix(aOriginAttributes)) {
return NS_ERROR_INVALID_ARG;
}
nsCOMPtr<nsIPrincipal> principal;
MOZ_TRY_VAR(principal, ScopeToPrincipal(aScope, attrs));
// The registration handling a push notification must have an exact scope
// match. This will try to find an exact match, unlike how fetch may find the
// registration with the longest scope that's a prefix of the fetched URL.
RefPtr<ServiceWorkerRegistrationInfo> registration =
GetRegistration(principal, aScope);
if (NS_WARN_IF(!registration)) {
return NS_ERROR_FAILURE;
}
MOZ_DIAGNOSTIC_ASSERT(registration->Scope().Equals(aScope));
ServiceWorkerInfo* serviceWorker = registration->GetActive();
if (NS_WARN_IF(!serviceWorker)) {
return NS_ERROR_FAILURE;
}
return serviceWorker->WorkerPrivate()->SendPushEvent(aMessageId, aData,
registration);
}
NS_IMETHODIMP
ServiceWorkerManager::SendPushSubscriptionChangeEvent(
const nsACString& aOriginAttributes, const nsACString& aScope) {
OriginAttributes 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) {
OriginAttributes 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(nsLiteralString(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(nsLiteralString(NOTIFICATION_CLOSE_EVENT_NAME),
aOriginSuffix, aScope, aID, aTitle, aDir, aLang,
aBody, aTag, aIcon, aData, aBehavior);
}
RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::WhenReady(
const ClientInfo& aClientInfo) {
AssertIsOnMainThread();
for (auto& prd : mPendingReadyList) {
if (prd->mClientHandle->Info().Id() == aClientInfo.Id() &&
prd->mClientHandle->Info().PrincipalInfo() ==
aClientInfo.PrincipalInfo()) {
return prd->mPromise;
}
}
RefPtr<ServiceWorkerRegistrationInfo> reg =
GetServiceWorkerRegistrationInfo(aClientInfo);
if (reg && reg->GetActive()) {
return ServiceWorkerRegistrationPromise::CreateAndResolve(reg->Descriptor(),
__func__);
}
nsCOMPtr<nsISerialEventTarget> target = GetMainThreadSerialEventTarget();
RefPtr<ClientHandle> handle =
ClientManager::CreateHandle(aClientInfo, target);
mPendingReadyList.AppendElement(MakeUnique<PendingReadyData>(handle));
RefPtr<ServiceWorkerManager> self(this);
handle->OnDetach()->Then(target, __func__,
[self = std::move(self), aClientInfo] {
self->RemovePendingReadyPromise(aClientInfo);
});
return mPendingReadyList.LastElement()->mPromise;
}
void ServiceWorkerManager::CheckPendingReadyPromises() {
nsTArray<UniquePtr<PendingReadyData>> pendingReadyList =
std::move(mPendingReadyList);
for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) {
UniquePtr<PendingReadyData> prd(std::move(pendingReadyList[i]));
RefPtr<ServiceWorkerRegistrationInfo> reg =
GetServiceWorkerRegistrationInfo(prd->mClientHandle->Info());
if (reg && reg->GetActive()) {
prd->mPromise->Resolve(reg->Descriptor(), __func__);
} else {
mPendingReadyList.AppendElement(std::move(prd));
}
}
}
void ServiceWorkerManager::RemovePendingReadyPromise(
const ClientInfo& aClientInfo) {
nsTArray<UniquePtr<PendingReadyData>> pendingReadyList =
std::move(mPendingReadyList);
for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) {
UniquePtr<PendingReadyData> prd(std::move(pendingReadyList[i]));
if (prd->mClientHandle->Info().Id() == aClientInfo.Id() &&
prd->mClientHandle->Info().PrincipalInfo() ==
aClientInfo.PrincipalInfo()) {
prd->mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__);
} else {
mPendingReadyList.AppendElement(std::move(prd));
}
}
}
void ServiceWorkerManager::NoteInheritedController(
const ClientInfo& aClientInfo, const ServiceWorkerDescriptor& aController) {
MOZ_ASSERT(NS_IsMainThread());
auto principalOrErr = PrincipalInfoToPrincipal(aController.PrincipalInfo());
if (NS_WARN_IF(principalOrErr.isErr())) {
return;
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
nsCOMPtr<nsIURI> scope;
nsresult rv = NS_NewURI(getter_AddRefs(scope), aController.Scope());
NS_ENSURE_SUCCESS_VOID(rv);
RefPtr<ServiceWorkerRegistrationInfo> registration =
GetServiceWorkerRegistrationInfo(principal, scope);
NS_ENSURE_TRUE_VOID(registration);
NS_ENSURE_TRUE_VOID(registration->GetActive());
StartControllingClient(aClientInfo, registration,
false /* aControlClientHandle */);
}
ServiceWorkerInfo* ServiceWorkerManager::GetActiveWorkerInfoForScope(
const OriginAttributes& aOriginAttributes, const nsACString& aScope) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIURI> scopeURI;
nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
if (NS_FAILED(rv)) {
return nullptr;
}
auto result = ScopeToPrincipal(scopeURI, aOriginAttributes);
if (NS_WARN_IF(result.isErr())) {
return nullptr;
}
auto principal = result.unwrap();
RefPtr<ServiceWorkerRegistrationInfo> registration =
GetServiceWorkerRegistrationInfo(principal, scopeURI);
if (!registration) {
return nullptr;
}
return registration->GetActive();
}
namespace {
class UnregisterJobCallback final : public ServiceWorkerJob::Callback {
nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback;
~UnregisterJobCallback() { MOZ_ASSERT(!mCallback); }
public:
explicit UnregisterJobCallback(nsIServiceWorkerUnregisterCallback* aCallback)
: mCallback(aCallback) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mCallback);
}
void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aJob);
MOZ_ASSERT(mCallback);
auto scopeExit = MakeScopeExit([&]() { mCallback = nullptr; });
if (aStatus.Failed()) {
mCallback->UnregisterFailed();
return;
}
MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Unregister);
RefPtr<ServiceWorkerUnregisterJob> unregisterJob =
static_cast<ServiceWorkerUnregisterJob*>(aJob);
mCallback->UnregisterSucceeded(unregisterJob->GetResult());
}
void JobDiscarded(ErrorResult&) override {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mCallback);
mCallback->UnregisterFailed();
mCallback = nullptr;
}
NS_INLINE_DECL_REFCOUNTING(UnregisterJobCallback, override)
};
} // anonymous namespace
NS_IMETHODIMP
ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal,
nsIServiceWorkerUnregisterCallback* aCallback,
const nsAString& aScope) {
MOZ_ASSERT(NS_IsMainThread());
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);
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;
}
void ServiceWorkerManager::WorkerIsIdle(ServiceWorkerInfo* aWorker) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_DIAGNOSTIC_ASSERT(aWorker);
RefPtr<ServiceWorkerRegistrationInfo> reg =
GetRegistration(aWorker->Principal(), aWorker->Scope());
if (!reg) {
return;
}
if (reg->GetActive() != aWorker) {
return;
}
reg->TryToActivateAsync();
}
already_AddRefed<ServiceWorkerJobQueue>
ServiceWorkerManager::GetOrCreateJobQueue(const nsACString& aKey,
const nsACString& aScope) {
MOZ_ASSERT(!aKey.IsEmpty());
ServiceWorkerManager::RegistrationDataPerPrincipal* data;
// XXX we could use WithEntryHandle here to avoid a hashtable lookup, except
// that leads to a false positive assertion, see bug 1370674 comment 7.
if (!mRegistrationInfos.Get(aKey, &data)) {
data = mRegistrationInfos
.InsertOrUpdate(aKey, MakeUnique<RegistrationDataPerPrincipal>())
.get();
}
RefPtr queue = data->mJobQueues.GetOrInsertNew(aScope);
return queue.forget();
}
/* static */
already_AddRefed<ServiceWorkerManager> ServiceWorkerManager::GetInstance() {
if (!gInstance) {
RefPtr<ServiceWorkerRegistrar> swr;
// XXX: Substitute this with an assertion. See comment in Init.
if (XRE_IsParentProcess()) {
// Don't (re-)create the ServiceWorkerManager if we are already shutting
// down.
if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
return nullptr;
}
// Don't create the ServiceWorkerManager until the ServiceWorkerRegistrar
// is initialized.
swr = ServiceWorkerRegistrar::Get();
if (!swr) {
return nullptr;
}
}
MOZ_ASSERT(NS_IsMainThread());
gInstance = new ServiceWorkerManager();
gInstance->Init(swr);
ClearOnShutdown(&gInstance);
}
RefPtr<ServiceWorkerManager> copy = gInstance.get();
return copy.forget();
}
void ServiceWorkerManager::ReportToAllClients(
const nsCString& aScope, const nsString& aMessage,
const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber,
uint32_t aColumnNumber, uint32_t aFlags) {
ConsoleUtils::ReportForServiceWorkerScope(
NS_ConvertUTF8toUTF16(aScope), aMessage, aFilename, aLineNumber,
aColumnNumber, ConsoleUtils::eError);
}
/* 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;
nsAutoString 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::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) {
MOZ_ASSERT(NS_IsMainThread());
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) {
MOZ_ASSERT(NS_IsMainThread());
auto principalOrErr = PrincipalInfoToPrincipal(aRegistration.principal());
if (NS_WARN_IF(principalOrErr.isErr())) {
return;
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
// Purge extensions registrations if they are disabled by prefs.
if (!StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) {
nsCOMPtr<nsIURI> uri = principal->GetURI();
// We do check the URI scheme here because when this is going to run
// the extension may not have been loaded yet and the WebExtensionPolicy
// may not exist yet.
if (uri->SchemeIs("moz-extension")) {
const auto& cacheName = aRegistration.cacheName();
serviceWorkerScriptCache::PurgeCache(principal, cacheName);
return;
}
}
RefPtr<ServiceWorkerRegistrationInfo> registration =
GetRegistration(principal, aRegistration.scope());
if (!registration) {
registration =
CreateNewRegistration(aRegistration.scope(), principal,
static_cast<ServiceWorkerUpdateViaCache>(
aRegistration.updateViaCache()),
aRegistration.navigationPreloadState());
} else {
// If active worker script matches our expectations for a "current worker",
// then we are done. Since scripts with the same URL might have different
// contents such as updated scripts or scripts with different LoadFlags, we
// use the CacheName to judge whether the two scripts are identical, where
// the CacheName is an UUID generated when a new script is found.
if (registration->GetActive() &&
registration->GetActive()->CacheName() == aRegistration.cacheName()) {
// No needs for updates.
return;
}
}
registration->SetLastUpdateTime(aRegistration.lastUpdateTime());
nsLoadFlags importsLoadFlags = nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
if (aRegistration.updateViaCache() !=
static_cast<uint16_t>(ServiceWorkerUpdateViaCache::None)) {
importsLoadFlags |= nsIRequest::VALIDATE_ALWAYS;
}
const nsCString& currentWorkerURL = aRegistration.currentWorkerURL();
if (!currentWorkerURL.IsEmpty()) {
registration->SetActive(new ServiceWorkerInfo(
registration->Principal(), registration->Scope(), registration->Id(),
registration->Version(), currentWorkerURL, aRegistration.cacheName(),
importsLoadFlags));
registration->GetActive()->SetHandlesFetch(
aRegistration.currentWorkerHandlesFetch());
registration->GetActive()->SetInstalledTime(
aRegistration.currentWorkerInstalledTime());
registration->GetActive()->SetActivatedTime(
aRegistration.currentWorkerActivatedTime());
}
}
void ServiceWorkerManager::LoadRegistrations(
const nsTArray<ServiceWorkerRegistrationData>& aRegistrations) {
MOZ_ASSERT(NS_IsMainThread());
uint32_t fetch = 0;
for (uint32_t i = 0, len = aRegistrations.Length(); i < len; ++i) {
LoadRegistration(aRegistrations[i]);
if (aRegistrations[i].currentWorkerHandlesFetch()) {
fetch++;
}
}
gServiceWorkersRegistered = aRegistrations.Length();
gServiceWorkersRegisteredFetch = fetch;
Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
u"All"_ns, gServiceWorkersRegistered);
Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
u"Fetch"_ns, gServiceWorkersRegisteredFetch);
LOG(("LoadRegistrations: %u, fetch %u\n", gServiceWorkersRegistered,
gServiceWorkersRegisteredFetch));
}
void ServiceWorkerManager::StoreRegistration(
nsIPrincipal* aPrincipal, ServiceWorkerRegistrationInfo* aRegistration) {
MOZ_ASSERT(aPrincipal);
MOZ_ASSERT(aRegistration);
if (mShuttingDown) {
return;
}
// Do not store a registration for addons that are not installed, not enabled
// or installed temporarily.
//
// If the dom.serviceWorkers.testing.persistTemporaryInstalledAddons is set
// to true, the registration for a temporary installed addon will still be
// persisted (only meant to be used to make it easier to test some particular
// scenario with a temporary installed addon which doesn't need to be signed
// to be installed on release channel builds).
if (aPrincipal->SchemeIs("moz-extension")) {
RefPtr<extensions::WebExtensionPolicy> addonPolicy =
BasePrincipal::Cast(aPrincipal)->AddonPolicy();
if (!addonPolicy || !addonPolicy->Active() ||
(addonPolicy->TemporarilyInstalled() &&
!StaticPrefs::
dom_serviceWorkers_testing_persistTemporarilyInstalledAddons())) {
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(
const ClientInfo& aClientInfo) const {
auto principalOrErr = aClientInfo.GetPrincipal();
if (NS_WARN_IF(principalOrErr.isErr())) {
return nullptr;
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aClientInfo.URL());
NS_ENSURE_SUCCESS(rv, nullptr);
return GetServiceWorkerRegistrationInfo(principal, uri);
}
already_AddRefed<ServiceWorkerRegistrationInfo>
ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal,
nsIURI* aURI) const {
MOZ_ASSERT(aPrincipal);
MOZ_ASSERT(aURI);
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) const {
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->Principal()->GetOrigin(origin);
MOZ_ASSERT(NS_SUCCEEDED(rv));
MOZ_ASSERT(origin.Equals(aScopeKey));
#endif
return registration.forget();
}
/* static */
nsresult ServiceWorkerManager::PrincipalToScopeKey(nsIPrincipal* aPrincipal,
nsACString& aKey) {
MOZ_ASSERT(aPrincipal);
if (!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal()) {
return NS_ERROR_FAILURE;
}
nsresult rv = aPrincipal->GetOrigin(aKey);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
/* static */
nsresult ServiceWorkerManager::PrincipalInfoToScopeKey(
const PrincipalInfo& aPrincipalInfo, nsACString& aKey) {
if (aPrincipalInfo.type() != PrincipalInfo::TContentPrincipalInfo) {
return NS_ERROR_FAILURE;
}
auto content = aPrincipalInfo.get_ContentPrincipalInfo();
nsAutoCString suffix;
content.attrs().CreateSuffix(suffix);
aKey = content.originNoSuffix();
aKey.Append(suffix);
return NS_OK;
}
/* static */
void ServiceWorkerManager::AddScopeAndRegistration(
const nsACString& aScope, ServiceWorkerRegistrationInfo* aInfo) {
MOZ_ASSERT(aInfo);
MOZ_ASSERT(aInfo->Principal());
MOZ_ASSERT(!aInfo->IsUnregistered());
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
if (!swm) {
// browser shutdown
return;
}
nsAutoCString scopeKey;
nsresult rv = swm->PrincipalToScopeKey(aInfo->Principal(), scopeKey);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
MOZ_ASSERT(!scopeKey.IsEmpty());
auto* const data = swm->mRegistrationInfos.GetOrInsertNew(scopeKey);
data->mScopeContainer.InsertScope(aScope);
data->mInfos.InsertOrUpdate(aScope, RefPtr{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;
}
Maybe<nsCString> scope = (*aData)->mScopeContainer.MatchScope(aPath);
if (scope) {
// scope.isSome() will still truen true after this; we are just moving the
// string inside the Maybe, so the Maybe will contain an empty string.
aMatch = std::move(*scope);
}
return scope.isSome();
}
/* 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->mScopeContainer.Contains(aScope);
}
/* static */
void ServiceWorkerManager::RemoveScopeAndRegistration(
ServiceWorkerRegistrationInfo* aRegistration) {
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
if (!swm) {
return;
}
nsAutoCString scopeKey;
nsresult rv = swm->PrincipalToScopeKey(aRegistration->Principal(), scopeKey);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
RegistrationDataPerPrincipal* data;
if (!swm->mRegistrationInfos.Get(scopeKey, &data)) {
return;
}
if (auto entry = data->mUpdateTimers.Lookup(aRegistration->Scope())) {
entry.Data()->Cancel();
entry.Remove();
}
// Verify there are no controlled clients for the purged registration.
for (auto iter = swm->mControlledClients.Iter(); !iter.Done(); iter.Next()) {
auto& reg = iter.UserData()->mRegistrationInfo;
if (reg->Scope().Equals(aRegistration->Scope()) &&
reg->Principal()->Equals(aRegistration->Principal()) &&
reg->IsCorrupt()) {
iter.Remove();
}
}
RefPtr<ServiceWorkerRegistrationInfo> info;
data->mInfos.Remove(aRegistration->Scope(), getter_AddRefs(info));
aRegistration->SetUnregistered();
data->mScopeContainer.RemoveScope(aRegistration->Scope());
swm->NotifyListenersOnUnregister(info);
swm->MaybeRemoveRegistrationInfo(scopeKey);
}
void ServiceWorkerManager::MaybeRemoveRegistrationInfo(
const nsACString& aScopeKey) {
if (auto entry = mRegistrationInfos.Lookup(aScopeKey)) {
if (entry.Data()->mScopeContainer.IsEmpty() &&
entry.Data()->mJobQueues.Count() == 0) {
entry.Remove();
// Need to reset the mQuotaUsageCheckCount, if
// RegistrationDataPerPrincipal:: mScopeContainer is empty. This
// RegistrationDataPerPrincipal might be reused, such that quota usage
// mitigation can be triggered for the new added registration.
} else if (entry.Data()->mScopeContainer.IsEmpty() &&
entry.Data()->mQuotaUsageCheckCount) {
entry.Data()->mQuotaUsageCheckCount = 0;
}
}
}
bool ServiceWorkerManager::StartControlling(
const ClientInfo& aClientInfo,
const ServiceWorkerDescriptor& aServiceWorker) {
MOZ_ASSERT(NS_IsMainThread());
auto principalOrErr =
PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo());
if (NS_WARN_IF(principalOrErr.isErr())) {
return false;
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
nsCOMPtr<nsIURI> scope;
nsresult rv = NS_NewURI(getter_AddRefs(scope), aServiceWorker.Scope());
NS_ENSURE_SUCCESS(rv, false);
RefPtr<ServiceWorkerRegistrationInfo> registration =
GetServiceWorkerRegistrationInfo(principal, scope);
NS_ENSURE_TRUE(registration, false);
NS_ENSURE_TRUE(registration->GetActive(), false);
StartControllingClient(aClientInfo, registration);
return true;
}
void ServiceWorkerManager::MaybeCheckNavigationUpdate(
const ClientInfo& aClientInfo) {
MOZ_ASSERT(NS_IsMainThread());
// 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.
ControlledClientData* data = mControlledClients.Get(aClientInfo.Id());
if (data && data->mRegistrationInfo) {
data->mRegistrationInfo->MaybeScheduleUpdate();
}
}
void ServiceWorkerManager::StopControllingRegistration(
ServiceWorkerRegistrationInfo* aRegistration) {
aRegistration->StopControllingClient();
if (aRegistration->IsControllingClients()) {
return;
}
if (aRegistration->IsUnregistered()) {
if (aRegistration->IsIdle()) {
aRegistration->Clear();
} else {
aRegistration->ClearWhenIdle();
}
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);
if (NS_WARN_IF(NS_FAILED(rv))) {
return NS_ERROR_FAILURE;
}
RefPtr<ServiceWorkerRegistrationInfo> r =
GetServiceWorkerRegistrationInfo(aPrincipal, uri);
if (!r) {
return NS_ERROR_FAILURE;
}
CopyUTF8toUTF16(r->Scope(), aScope);
return NS_OK;
}
namespace {
class ContinueDispatchFetchEventRunnable : public Runnable {
RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate;
nsCOMPtr<nsIInterceptedChannel> mChannel;
nsCOMPtr<nsILoadGroup> mLoadGroup;
public:
ContinueDispatchFetchEventRunnable(
ServiceWorkerPrivate* aServiceWorkerPrivate,
nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup)
: Runnable(
"dom::ServiceWorkerManager::ContinueDispatchFetchEventRunnable"),
mServiceWorkerPrivate(aServiceWorkerPrivate),
mChannel(aChannel),
mLoadGroup(aLoadGroup) {
MOZ_ASSERT(aServiceWorkerPrivate);
MOZ_ASSERT(aChannel);
}
void HandleError() {
MOZ_ASSERT(NS_IsMainThread());
NS_WARNING("Unexpected error while dispatching fetch event!");
nsresult rv = mChannel->ResetInterception(false);
if (NS_FAILED(rv)) {
NS_WARNING("Failed to resume intercepted network request");
mChannel->CancelInterception(rv);
}
}
NS_IMETHOD
Run() override {
MOZ_ASSERT(NS_IsMainThread());
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;
}
nsString clientId;
nsString resultingClientId;
nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo();
if (clientInfo.isSome()) {
clientId = NSID_TrimBracketsUTF16(clientInfo->Id());
}
// Having an initial or reserved client are mutually exclusive events:
// either an initial client is used upon navigating an about:blank
// iframe, or a new, reserved environment/client is created (e.g.
// upon a top-level navigation). See step 4 of
// https://html.spec.whatwg.org/#process-a-navigate-fetch as well as
// https://github.com/w3c/ServiceWorker/issues/1228#issuecomment-345132444
Maybe<ClientInfo> resulting = loadInfo->GetInitialClientInfo();
if (resulting.isNothing()) {
resulting = loadInfo->GetReservedClientInfo();
} else {
MOZ_ASSERT(loadInfo->GetReservedClientInfo().isNothing());
}
if (resulting.isSome()) {
resultingClientId = NSID_TrimBracketsUTF16(resulting->Id());
}
rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, clientId,
resultingClientId);
if (NS_WARN_IF(NS_FAILED(rv))) {
HandleError();
}
return NS_OK;
}
};
} // anonymous namespace
void ServiceWorkerManager::DispatchFetchEvent(nsIInterceptedChannel* aChannel,
ErrorResult& aRv) {
MOZ_ASSERT(aChannel);
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(XRE_IsParentProcess());
nsCOMPtr<nsIChannel> internalChannel;
aRv = aChannel->GetChannel(getter_AddRefs(internalChannel));
if (NS_WARN_IF(aRv.Failed())) {
return;
}
nsCOMPtr<nsILoadGroup> loadGroup;
aRv = internalChannel->GetLoadGroup(getter_AddRefs(loadGroup));
if (NS_WARN_IF(aRv.Failed())) {
return;
}
nsCOMPtr<nsILoadInfo> loadInfo = internalChannel->LoadInfo();
RefPtr<ServiceWorkerInfo> serviceWorker;
if (!nsContentUtils::IsNonSubresourceRequest(internalChannel)) {
const Maybe<ServiceWorkerDescriptor>& controller =
loadInfo->GetController();
if (NS_WARN_IF(controller.isNothing())) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
RefPtr<ServiceWorkerRegistrationInfo> registration;
nsresult rv = GetClientRegistration(loadInfo->GetClientInfo().ref(),
getter_AddRefs(registration));
if (NS_WARN_IF(NS_FAILED(rv))) {
aRv.Throw(rv);
return;
}
serviceWorker = registration->GetActive();
if (NS_WARN_IF(!serviceWorker) ||
NS_WARN_IF(serviceWorker->Descriptor().Id() != controller.ref().Id())) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
} else {
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
OriginAttributes attrs = loadInfo->GetOriginAttributes();
if (StaticPrefs::privacy_partition_serviceWorkers()) {
StoragePrincipalHelper::GetOriginAttributes(
internalChannel, attrs,
StoragePrincipalHelper::eForeignPartitionedPrincipal);
}
nsCOMPtr<nsIPrincipal> principal =
BasePrincipal::CreateContentPrincipal(uri, attrs);
RefPtr<ServiceWorkerRegistrationInfo> registration =
GetServiceWorkerRegistrationInfo(principal, uri);
if (NS_WARN_IF(!registration)) {
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 (NS_WARN_IF(!serviceWorker)) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
// If there is a reserved client it should be marked as controlled before
// the FetchEvent is dispatched.
Maybe<ClientInfo> clientInfo = loadInfo->GetReservedClientInfo();
// Also override the initial about:blank controller since the real
// network load may be intercepted by a different service worker. If
// the intial about:blank has a controller here its simply been
// inherited from its parent.
if (clientInfo.isNothing()) {
clientInfo = loadInfo->GetInitialClientInfo();
// TODO: We need to handle the case where the initial about:blank is
// controlled, but the final document load is not. Right now
// the spec does not really say what to do. There currently
// is no way for the controller to be cleared from a client in
// the spec or our implementation. We may want to force a
// new inner window to be created instead of reusing the
// initial about:blank global. See bug 1419620 and the spec
// issue here: https://github.com/w3c/ServiceWorker/issues/1232
}
if (clientInfo.isSome()) {
// ClientChannelHelper is not called for STS upgrades that get
// intercepted by a service worker when interception occurs in
// the content process. Therefore the reserved client is not
// properly cleared in that case leading to a situation where
// a ClientSource with an http:// principal is controlled by
// a ServiceWorker with an https:// principal.
//
// This does not occur when interception is handled by the
// simpler InterceptedHttpChannel approach in the parent.
//
// As a temporary work around check for this principal mismatch
// here and perform the ClientChannelHelper's replacement of
// reserved client automatically.
if (!XRE_IsParentProcess()) {
auto clientPrincipalOrErr = clientInfo.ref().GetPrincipal();
nsCOMPtr<nsIPrincipal> clientPrincipal;
if (clientPrincipalOrErr.isOk()) {
clientPrincipal = clientPrincipalOrErr.unwrap();
}
if (!clientPrincipal || !clientPrincipal->Equals(principal)) {
UniquePtr<ClientSource> reservedClient =
loadInfo->TakeReservedClientSource();
nsCOMPtr<nsISerialEventTarget> target =
reservedClient ? reservedClient->EventTarget()
: GetMainThreadSerialEventTarget();
reservedClient.reset();
reservedClient = ClientManager::CreateSource(ClientType::Window,
target, principal);
loadInfo->GiveReservedClientSource(std::move(reservedClient));
clientInfo = loadInfo->GetReservedClientInfo();
}
}
// First, attempt to mark the reserved client controlled directly. This
// will update the controlled status in the ClientManagerService in the
// parent. It will also eventually propagate back to the ClientSource.
StartControllingClient(clientInfo.ref(), registration);
}
uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL;
nsCOMPtr<nsIHttpChannelInternal> http = do_QueryInterface(internalChannel);
MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode));
// Synthetic redirects for non-subresource requests with a "follow"
// redirect mode may switch controllers. This is basically worker
// scripts right now. In this case we need to explicitly clear the
// controller to avoid assertions on the SetController() below.
if (redirectMode == nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) {
loadInfo->ClearController();
}
// But we also note the reserved state on the LoadInfo. This allows the
// ClientSource to be updated immediately after the nsIChannel starts.
// This is necessary to have the correct controller in place for immediate
// follow-on requests.
loadInfo->SetController(serviceWorker->Descriptor());
}
MOZ_DIAGNOSTIC_ASSERT(serviceWorker);
RefPtr<ContinueDispatchFetchEventRunnable> continueRunnable =
new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(),
aChannel, loadGroup);
// When this service worker was registered, we also sent down the permissions
// for the runnable. They should have arrived by now, but we still need to
// wait for them if they have not.
RefPtr<PermissionManager> permMgr = PermissionManager::GetInstance();
if (permMgr) {
permMgr->WhenPermissionsAvailable(serviceWorker->Principal(),
continueRunnable);
} else {
continueRunnable->HandleError();
}
}
bool ServiceWorkerManager::IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI,
nsIChannel* aChannel) {
MOZ_ASSERT(aPrincipal);
MOZ_ASSERT(aURI);
MOZ_ASSERT(aChannel);
RefPtr<ServiceWorkerRegistrationInfo> registration =
GetServiceWorkerRegistrationInfo(aPrincipal, aURI);
if (!registration || !registration->GetActive()) {
return false;
}
// Checking if the matched service worker handles fetch events or not.
// If it does, directly return true and handle the client controlling logic
// in DispatchFetchEvent(). otherwise, do followings then return false.
// 1. Set the matched service worker as the controller of LoadInfo and
// correspoinding ClinetInfo
// 2. Maybe schedule a soft update
if (!registration->GetActive()->HandlesFetch()) {
// Checkin if the channel is not allowed for the service worker.
auto storageAccess = StorageAllowedForChannel(aChannel);
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
if (storageAccess != StorageAccess::eAllow) {
if (!StaticPrefs::privacy_partition_serviceWorkers()) {
return false;
}
nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
if (!StoragePartitioningEnabled(storageAccess, cookieJarSettings)) {
return false;
}
}
// ServiceWorkerInterceptController::ShouldPrepareForIntercept() handles the
// subresource cases. Must be non-subresource case here.
MOZ_ASSERT(nsContentUtils::IsNonSubresourceRequest(aChannel));
Maybe<ClientInfo> clientInfo = loadInfo->GetReservedClientInfo();
if (clientInfo.isNothing()) {
clientInfo = loadInfo->GetInitialClientInfo();
}
if (clientInfo.isSome()) {
StartControllingClient(clientInfo.ref(), registration);
}
uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL;
nsCOMPtr<nsIHttpChannelInternal> http = do_QueryInterface(aChannel);
MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode));
// Synthetic redirects for non-subresource requests with a "follow"
// redirect mode may switch controllers. This is basically worker
// scripts right now. In this case we need to explicitly clear the
// controller to avoid assertions on the SetController() below.
if (redirectMode == nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) {
loadInfo->ClearController();
}
loadInfo->SetController(registration->GetActive()->Descriptor());
// https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm 17.1
// try schedule a soft-update for non-subresource case.
registration->MaybeScheduleUpdate();
return false;
}
// Found a matching service worker which handles fetch events, return true.
return true;
}
nsresult ServiceWorkerManager::GetClientRegistration(
const ClientInfo& aClientInfo,
ServiceWorkerRegistrationInfo** aRegistrationInfo) {
ControlledClientData* data = mControlledClients.Get(aClientInfo.Id());
if (!data || !data->mRegistrationInfo) {
return NS_ERROR_NOT_AVAILABLE;
}
// If the document is controlled, the current worker MUST be non-null.
if (!data->mRegistrationInfo->GetActive()) {
return NS_ERROR_NOT_AVAILABLE;
}
RefPtr<ServiceWorkerRegistrationInfo> ref = data->mRegistrationInfo;
ref.forget(aRegistrationInfo);
return NS_OK;
}
int32_t ServiceWorkerManager::GetPrincipalQuotaUsageCheckCount(
nsIPrincipal* aPrincipal) {
nsAutoCString scopeKey;
nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
if (NS_WARN_IF(NS_FAILED(rv))) {
return -1;
}
RegistrationDataPerPrincipal* data;
if (!mRegistrationInfos.Get(scopeKey, &data)) {
return -1;
}
return data->mQuotaUsageCheckCount;
}
void ServiceWorkerManager::CheckPrincipalQuotaUsage(nsIPrincipal* aPrincipal,
const nsACString& aScope) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aPrincipal);
nsAutoCString scopeKey;
nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
RegistrationDataPerPrincipal* data;
if (!mRegistrationInfos.Get(scopeKey, &data)) {
return;
}
// Had already schedule a quota usage check.
if (data->mQuotaUsageCheckCount != 0) {
return;
}
++data->mQuotaUsageCheckCount;
// Get the corresponding ServiceWorkerRegistrationInfo here. Unregisteration
// might be triggered later, should get it here before it be removed from
// data.mInfos, such that NotifyListenersOnQuotaCheckFinish() can notify the
// corresponding ServiceWorkerRegistrationInfo after asynchronous quota
// checking finish.
RefPtr<ServiceWorkerRegistrationInfo> info;
data->mInfos.Get(aScope, getter_AddRefs(info));
MOZ_ASSERT(info);
RefPtr<ServiceWorkerManager> self = this;
ClearQuotaUsageIfNeeded(aPrincipal, [self, info](bool aResult) {
MOZ_ASSERT(NS_IsMainThread());
self->NotifyListenersOnQuotaUsageCheckFinish(info);
});
}
void ServiceWorkerManager::SoftUpdate(const OriginAttributes& aOriginAttributes,
const nsACString& aScope) {
MOZ_ASSERT(NS_IsMainThread());
if (mShuttingDown) {
return;
}
SoftUpdateInternal(aOriginAttributes, aScope, nullptr);
}
namespace {
class UpdateJobCallback final : public ServiceWorkerJob::Callback {
RefPtr<ServiceWorkerUpdateFinishCallback> mCallback;
~UpdateJobCallback() { MOZ_ASSERT(!mCallback); }
public:
explicit UpdateJobCallback(ServiceWorkerUpdateFinishCallback* aCallback)
: mCallback(aCallback) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mCallback);
}
void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aJob);
MOZ_ASSERT(mCallback);
auto scopeExit = MakeScopeExit([&]() { mCallback = nullptr; });
if (aStatus.Failed()) {
mCallback->UpdateFailed(aStatus);
return;
}
MOZ_DIAGNOSTIC_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Update);
RefPtr<ServiceWorkerUpdateJob> updateJob =
static_cast<ServiceWorkerUpdateJob*>(aJob);
RefPtr<ServiceWorkerRegistrationInfo> reg = updateJob->GetRegistration();
mCallback->UpdateSucceeded(reg);
}
void JobDiscarded(ErrorResult& aStatus) override {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mCallback);
mCallback->UpdateFailed(aStatus);
mCallback = nullptr;
}
NS_INLINE_DECL_REFCOUNTING(UpdateJobCallback, override)
};
} // anonymous namespace
void ServiceWorkerManager::SoftUpdateInternal(
const OriginAttributes& aOriginAttributes, const nsACString& aScope,
ServiceWorkerUpdateFinishCallback* aCallback) {
MOZ_ASSERT(NS_IsMainThread());
if (mShuttingDown) {
return;
}
auto result = ScopeToPrincipal(aScope, aOriginAttributes);
if (NS_WARN_IF(result.isErr())) {
return;
}
auto principal = result.unwrap();
nsAutoCString scopeKey;
nsresult 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 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->Scope(), newest->ScriptSpec(),
registration->GetUpdateViaCache());
if (aCallback) {
RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback);
job->AppendResultCallback(cb);
}
queue->ScheduleJob(job);
}
void ServiceWorkerManager::Update(
nsIPrincipal* aPrincipal, const nsACString& aScope,
nsCString aNewestWorkerScriptUrl,
ServiceWorkerUpdateFinishCallback* aCallback) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!aNewestWorkerScriptUrl.IsEmpty());
UpdateInternal(aPrincipal, aScope, std::move(aNewestWorkerScriptUrl),
aCallback);
}
void ServiceWorkerManager::UpdateInternal(
nsIPrincipal* aPrincipal, const nsACString& aScope,
nsCString&& aNewestWorkerScriptUrl,
ServiceWorkerUpdateFinishCallback* aCallback) {
MOZ_ASSERT(aPrincipal);
MOZ_ASSERT(aCallback);
MOZ_ASSERT(!aNewestWorkerScriptUrl.IsEmpty());
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)) {
ErrorResult error;
error.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(aScope, "uninstalled");
aCallback->UpdateFailed(error);
// In case the callback does not consume the exception
error.SuppressException();
return;
}
RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope);
// "Let job be the result of running Create Job with update, registrations
// scope url, newestWorkers script url, promise, and the context objects
// relevant settings object."
RefPtr<ServiceWorkerUpdateJob> job = new ServiceWorkerUpdateJob(
aPrincipal, registration->Scope(), std::move(aNewestWorkerScriptUrl),
registration->GetUpdateViaCache());
RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback);
job->AppendResultCallback(cb);
// "Invoke Schedule Job with job."
queue->ScheduleJob(job);
}
RefPtr<GenericErrorResultPromise> ServiceWorkerManager::MaybeClaimClient(
const ClientInfo& aClientInfo,
ServiceWorkerRegistrationInfo* aWorkerRegistration) {
MOZ_DIAGNOSTIC_ASSERT(aWorkerRegistration);
if (!aWorkerRegistration->GetActive()) {
CopyableErrorResult rv;
rv.ThrowInvalidStateError("Worker is not active");
return GenericErrorResultPromise::CreateAndReject(rv, __func__);
}
// Same origin check
auto principalOrErr = aClientInfo.GetPrincipal();
if (NS_WARN_IF(principalOrErr.isErr())) {
CopyableErrorResult rv;
rv.ThrowSecurityError("Could not extract client's principal");
return GenericErrorResultPromise::CreateAndReject(rv, __func__);
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
if (!aWorkerRegistration->Principal()->Equals(principal)) {
CopyableErrorResult rv;
rv.ThrowSecurityError("Worker is for a different origin");
return GenericErrorResultPromise::CreateAndReject(rv, __func__);
}
// The registration that should be controlling the client
RefPtr<ServiceWorkerRegistrationInfo> matchingRegistration =
GetServiceWorkerRegistrationInfo(aClientInfo);
// The registration currently controlling the client
RefPtr<ServiceWorkerRegistrationInfo> controllingRegistration;
GetClientRegistration(aClientInfo, getter_AddRefs(controllingRegistration));
if (aWorkerRegistration != matchingRegistration ||
aWorkerRegistration == controllingRegistration) {
return GenericErrorResultPromise::CreateAndResolve(true, __func__);
}
return StartControllingClient(aClientInfo, aWorkerRegistration);
}
RefPtr<GenericErrorResultPromise> ServiceWorkerManager::MaybeClaimClient(
const ClientInfo& aClientInfo,
const ServiceWorkerDescriptor& aServiceWorker) {
auto principalOrErr = aServiceWorker.GetPrincipal();
if (NS_WARN_IF(principalOrErr.isErr())) {
return GenericErrorResultPromise::CreateAndResolve(false, __func__);
}
nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
RefPtr<ServiceWorkerRegistrationInfo> registration =
GetRegistration(principal, aServiceWorker.Scope());
// While ServiceWorkerManager is distributed across child processes its
// possible for us to sometimes get a claim for a new worker that has
// not propagated to this process yet. For now, simply note that we
// are done. The fix for this is to move the SWM to the parent process
// so there are no consistency errors.
if (NS_WARN_IF(!registration) || NS_WARN_IF(!registration->GetActive())) {
return GenericErrorResultPromise::CreateAndResolve(false, __func__);
}
return MaybeClaimClient(aClientInfo, registration);
}
void ServiceWorkerManager::UpdateClientControllers(
ServiceWorkerRegistrationInfo* aRegistration) {
MOZ_ASSERT(NS_IsMainThread());
RefPtr<ServiceWorkerInfo> activeWorker = aRegistration->GetActive();
MOZ_DIAGNOSTIC_ASSERT(activeWorker);
AutoTArray<RefPtr<ClientHandle>, 16> handleList;
for (const auto& client : mControlledClients.Values()) {
if (client->mRegistrationInfo != aRegistration) {
continue;
}
handleList.AppendElement(client->mClientHandle);
}
// Fire event after iterating mControlledClients is done to prevent
// modification by reentering from the event handlers during iteration.
for (auto& handle : handleList) {
RefPtr<GenericErrorResultPromise> p =
handle->Control(activeWorker->Descriptor());
RefPtr<ServiceWorkerManager> self = this;
// If we fail to control the client, then automatically remove it
// from our list of controlled clients.
p->Then(
GetMainThreadSerialEventTarget(), __func__,
[](bool) {
// do nothing on success
},
[self, clientInfo = handle->Info()](const CopyableErrorResult& aRv) {
// failed to control, forget about this client
self->StopControllingClient(clientInfo);
});
}
}
void ServiceWorkerManager::EvictFromBFCache(
ServiceWorkerRegistrationInfo* aRegistration) {
MOZ_ASSERT(NS_IsMainThread());
for (const auto& client : mControlledClients.Values()) {
if (client->mRegistrationInfo == aRegistration) {
client->mClientHandle->EvictFromBFCache();
}
}
}
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);
}
already_AddRefed<ServiceWorkerRegistrationInfo>
ServiceWorkerManager::GetRegistration(const PrincipalInfo& aPrincipalInfo,
const nsACString& aScope) const {
nsAutoCString scopeKey;
nsresult rv = PrincipalInfoToScopeKey(aPrincipalInfo, scopeKey);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
return GetRegistration(scopeKey, aScope);
}
NS_IMETHODIMP
ServiceWorkerManager::ReloadRegistrationsForTest() {
if (NS_WARN_IF(!StaticPrefs::dom_serviceWorkers_testing_enabled())) {
return NS_ERROR_FAILURE;
}
// Let's keep it simple and fail if there are any controlled client,
// the test case can take care of making sure there is none when this
// method will be called.
if (NS_WARN_IF(!mControlledClients.IsEmpty())) {
return NS_ERROR_FAILURE;
}
for (const auto& info : mRegistrationInfos.Values()) {
for (ServiceWorkerRegistrationInfo* reg : info->mInfos.Values()) {
MOZ_ASSERT(reg);
reg->ForceShutdown();
}
}
mRegistrationInfos.Clear();
nsTArray<ServiceWorkerRegistrationData> data;
RefPtr<ServiceWorkerRegistrar> swr = ServiceWorkerRegistrar::Get();
if (NS_WARN_IF(!swr->ReloadDataForTest())) {
return NS_ERROR_FAILURE;
}
swr->GetRegistrations(data);
LoadRegistrations(data);
return NS_OK;
}
NS_IMETHODIMP
ServiceWorkerManager::RegisterForAddonPrincipal(nsIPrincipal* aPrincipal,
JSContext* aCx,
dom::Promise** aPromise) {
nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
if (NS_WARN_IF(!global)) {
return NS_ERROR_FAILURE;
}
ErrorResult erv;
RefPtr<Promise> outer = Promise::Create(global, erv);
if (NS_WARN_IF(erv.Failed())) {
return erv.StealNSResult();
}
auto enabled =
StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup();
if (!enabled) {
outer->MaybeRejectWithNotAllowedError(
"Disabled. extensions.backgroundServiceWorker.enabled is false");
outer.forget(aPromise);
return NS_OK;
}
MOZ_ASSERT(aPrincipal);
auto* addonPolicy = BasePrincipal::Cast(aPrincipal)->AddonPolicy();
if (!addonPolicy) {
outer->MaybeRejectWithNotAllowedError("Not an extension principal");
outer.forget(aPromise);
return NS_OK;
}
nsCString scope;
auto result = addonPolicy->GetURL(u""_ns);
if (result.isOk()) {
scope.Assign(NS_ConvertUTF16toUTF8(result.unwrap()));
} else {
outer->MaybeRejectWithUnknownError("Unable to resolve addon scope URL");
outer.forget(aPromise);
return NS_OK;
}
nsString scriptURL;
addonPolicy->GetBackgroundWorker(scriptURL);
if (scriptURL.IsEmpty()) {
outer->MaybeRejectWithNotFoundError("Missing background worker script url");
outer.forget(aPromise);
return NS_OK;
}
Maybe<ClientInfo> clientInfo =
dom::ClientManager::CreateInfo(ClientType::All, aPrincipal);
if (!clientInfo.isSome()) {
outer->MaybeRejectWithUnknownError("Error creating clientInfo");
outer.forget(aPromise);
return NS_OK;
}
auto regPromise =
Register(clientInfo.ref(), scope, NS_ConvertUTF16toUTF8(scriptURL),
dom::ServiceWorkerUpdateViaCache::Imports);
const RefPtr<ServiceWorkerManager> self(this);
const nsCOMPtr<nsIPrincipal> principal(aPrincipal);
regPromise->Then(
GetMainThreadSerialEventTarget(), __func__,
[self, outer, principal,
scope](const ServiceWorkerRegistrationDescriptor& regDesc) {
RefPtr<ServiceWorkerRegistrationInfo> registration =
self->GetRegistration(principal, scope);
if (registration) {
outer->MaybeResolve(registration);
} else {
outer->MaybeRejectWithUnknownError(
"Failed to retrieve ServiceWorkerRegistrationInfo");
}
},
[outer](const mozilla::CopyableErrorResult& err) {
CopyableErrorResult result(err);
outer->MaybeReject(std::move(result));
});
outer.forget(aPromise);
return NS_OK;
}
NS_IMETHODIMP
ServiceWorkerManager::GetRegistrationForAddonPrincipal(
nsIPrincipal* aPrincipal, nsIServiceWorkerRegistrationInfo** aInfo) {
MOZ_ASSERT(aPrincipal);
MOZ_ASSERT(aPrincipal);
auto* addonPolicy = BasePrincipal::Cast(aPrincipal)->AddonPolicy();
if (!addonPolicy) {
return NS_ERROR_FAILURE;
}
nsCString scope;
auto result = addonPolicy->GetURL(u""_ns);
if (result.isOk()) {
scope.Assign(NS_ConvertUTF16toUTF8(result.unwrap()));
} else {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIURI> scopeURI;
nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope);
if (NS_FAILED(rv)) {
return NS_ERROR_FAILURE;
}
RefPtr<ServiceWorkerRegistrationInfo> info =
GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI);
if (!info) {
aInfo = nullptr;
return NS_OK;
}
info.forget(aInfo);
return NS_OK;
}
NS_IMETHODIMP
ServiceWorkerManager::WakeForExtensionAPIEvent(
const nsAString& aExtensionBaseURL, const nsAString& aAPINamespace,
const nsAString& aAPIEventName, JSContext* aCx, dom::Promise** aPromise) {
nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
if (NS_WARN_IF(!global)) {
return NS_ERROR_FAILURE;
}
ErrorResult erv;
RefPtr<Promise> outer = Promise::Create(global, erv);
if (NS_WARN_IF(erv.Failed())) {
return erv.StealNSResult();
}
auto enabled =
StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup();
if (!enabled) {
outer->MaybeRejectWithNotAllowedError(
"Disabled. extensions.backgroundServiceWorker.enabled is false");
outer.forget(aPromise);
return NS_OK;
}
nsCOMPtr<nsIURI> scopeURI;
nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aExtensionBaseURL);
if (NS_FAILED(rv)) {
outer->MaybeReject(rv);
outer.forget(aPromise);
return NS_OK;
}
nsCOMPtr<nsIPrincipal> principal;
MOZ_TRY_VAR(principal, ScopeToPrincipal(scopeURI, {}));
auto* addonPolicy = BasePrincipal::Cast(principal)->AddonPolicy();
if (NS_WARN_IF(!addonPolicy)) {
outer->MaybeRejectWithNotAllowedError(
"Not an extension principal or extension disabled");
outer.forget(aPromise);
return NS_OK;
}
OriginAttributes attrs;
ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(
attrs, NS_ConvertUTF16toUTF8(aExtensionBaseURL));
if (NS_WARN_IF(!info)) {
outer->MaybeRejectWithInvalidStateError(
"No active worker for the extension background service worker");
outer.forget(aPromise);
return NS_OK;
}
ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate();
auto result =
workerPrivate->WakeForExtensionAPIEvent(aAPINamespace, aAPIEventName);
if (result.isErr()) {
outer->MaybeReject(result.propagateErr());
outer.forget(aPromise);
return NS_OK;
}
RefPtr<ServiceWorkerPrivate::PromiseExtensionWorkerHasListener> innerPromise =
result.unwrap();
innerPromise->Then(
GetMainThreadSerialEventTarget(), __func__,
[outer](bool aSubscribedEvent) { outer->MaybeResolve(aSubscribedEvent); },
[outer](nsresult aErrorResult) { outer->MaybeReject(aErrorResult); });
outer.forget(aPromise);
return NS_OK;
}
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);
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,
ServiceWorkerUpdateViaCache aUpdateViaCache,
IPCNavigationPreloadState aNavigationPreloadState) {
#ifdef DEBUG
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIURI> scopeURI;
nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
MOZ_ASSERT(NS_SUCCEEDED(rv));
RefPtr<ServiceWorkerRegistrationInfo> tmp =
GetRegistration(aPrincipal, aScope);
MOZ_ASSERT(!tmp);
#endif
RefPtr<ServiceWorkerRegistrationInfo> registration =
new ServiceWorkerRegistrationInfo(aScope, aPrincipal, aUpdateViaCache,
std::move(aNavigationPreloadState));
// 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->Principal(), aRegistration->Scope())) {
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.
MOZ_ASSERT(HasScope(aRegistration->Principal(), aRegistration->Scope()));
RemoveScopeAndRegistration(aRegistration);
}
NS_IMETHODIMP
ServiceWorkerManager::GetAllRegistrations(nsIArray** aResult) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIMutableArray> array(do_CreateInstance(NS_ARRAY_CONTRACTID));
if (!array) {
return NS_ERROR_OUT_OF_MEMORY;
}
for (const auto& info : mRegistrationInfos.Values()) {
for (ServiceWorkerRegistrationInfo* reg : info->mInfos.Values()) {
MOZ_ASSERT(reg);
array->AppendElement(reg);
}
}
array.forget(aResult);
return NS_OK;
}
NS_IMETHODIMP
ServiceWorkerManager::RemoveRegistrationsByOriginAttributes(
const nsAString& aPattern) {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!aPattern.IsEmpty());
OriginAttributesPattern pattern;
MOZ_ALWAYS_TRUE(pattern.Init(aPattern));
for (const auto& data : mRegistrationInfos.Values()) {
// We can use iteration because ForceUnregister (and Unregister) are
// async. Otherwise doing some R/W operations on an hashtable during
// iteration will crash.
for (ServiceWorkerRegistrationInfo* reg : data->mInfos.Values()) {
MOZ_ASSERT(reg);
MOZ_ASSERT(reg->Principal());
bool matches = pattern.Matches(reg->Principal()->OriginAttributesRef());
if (!matches) {
continue;
}
ForceUnregister(data.get(), reg);
}
}
return NS_OK;
}
void ServiceWorkerManager::ForceUnregister(
RegistrationDataPerPrincipal* aRegistrationData,
ServiceWorkerRegistrationInfo* aRegistration) {
MOZ_ASSERT(aRegistrationData);
MOZ_ASSERT(aRegistration);
RefPtr<ServiceWorkerJobQueue> queue;
aRegistrationData->mJobQueues.Get(aRegistration->Scope(),
getter_AddRefs(queue));
if (queue) {
queue->CancelAll();
}
if (auto entry =
aRegistrationData->mUpdateTimers.Lookup(aRegistration->Scope())) {
entry.Data()->Cancel();
entry.Remove();
}
// Since Unregister is async, it is ok to call it in an enumeration.
Unregister(aRegistration->Principal(), nullptr,
NS_ConvertUTF8toUTF16(aRegistration->Scope()));
}
NS_IMETHODIMP
ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener) {
MOZ_ASSERT(NS_IsMainThread());
if (!aListener || mListeners.Contains(aListener)) {
return NS_ERROR_INVALID_ARG;
}
mListeners.AppendElement(aListener);
return NS_OK;
}
NS_IMETHODIMP
ServiceWorkerManager::RemoveListener(
nsIServiceWorkerManagerListener* aListener) {
MOZ_ASSERT(NS_IsMainThread());
if (!aListener || !mListeners.Contains(aListener)) {
return NS_ERROR_INVALID_ARG;
}
mListeners.RemoveElement(aListener);
return NS_OK;
}
NS_IMETHODIMP
ServiceWorkerManager::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
if (strcmp(aTopic, kFinishShutdownTopic) == 0) {
MaybeFinishShutdown();
return NS_OK;
}
MOZ_CRASH("Received message we aren't supposed to be registered for!");
return NS_OK;
}
NS_IMETHODIMP
ServiceWorkerManager::PropagateUnregister(
nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback,
const nsAString& aScope) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aPrincipal);
// Return earlier with an explicit failure if this xpcom method is called
// when the ServiceWorkerManager is not initialized yet or it is already
// shutting down.
if (NS_WARN_IF(!mActor)) {
return NS_ERROR_FAILURE;
}
PrincipalInfo principalInfo;
if (NS_WARN_IF(
NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) {
return NS_ERROR_FAILURE;
}
mActor->SendPropagateUnregister(principalInfo, 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.Clone());
for (size_t index = 0; index < listeners.Length(); ++index) {
listeners[index]->OnRegister(aInfo);
}
}
void ServiceWorkerManager::NotifyListenersOnUnregister(
nsIServiceWorkerRegistrationInfo* aInfo) {
nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(
mListeners.Clone());
for (size_t index = 0; index < listeners.Length(); ++index) {
listeners[index]->OnUnregister(aInfo);
}
}
void ServiceWorkerManager::NotifyListenersOnQuotaUsageCheckFinish(
nsIServiceWorkerRegistrationInfo* aRegistration) {
nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(
mListeners.Clone());
for (size_t index = 0; index < listeners.Length(); ++index) {
listeners[index]->OnQuotaUsageCheckFinish(aRegistration);
}
}
class UpdateTimerCallback final : public nsITimerCallback, public nsINamed {
nsCOMPtr<nsIPrincipal> mPrincipal;
const nsCString mScope;
~UpdateTimerCallback() = default;
public:
UpdateTimerCallback(nsIPrincipal* aPrincipal, const nsACString& aScope)
: mPrincipal(aPrincipal), mScope(aScope) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mPrincipal);
MOZ_ASSERT(!mScope.IsEmpty());
}
NS_IMETHOD
Notify(nsITimer* aTimer) override {
MOZ_ASSERT(NS_IsMainThread());
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
if (!swm) {
// shutting down, do nothing
return NS_OK;
}
swm->UpdateTimerFired(mPrincipal, mScope);
return NS_OK;
}
NS_IMETHOD
GetName(nsACString& aName) override {
aName.AssignLiteral("UpdateTimerCallback");
return NS_OK;
}
NS_DECL_ISUPPORTS
};
NS_IMPL_ISUPPORTS(UpdateTimerCallback, nsITimerCallback, nsINamed)
void ServiceWorkerManager::ScheduleUpdateTimer(nsIPrincipal* aPrincipal,
const nsACString& aScope) {
MOZ_ASSERT(NS_IsMainThread());
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;
}
data->mUpdateTimers.WithEntryHandle(
aScope, [&aPrincipal, &aScope](auto&& entry) {
if (entry) {
// In case there is already a timer scheduled, 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;
}
nsCOMPtr<nsITimerCallback> callback =
new UpdateTimerCallback(aPrincipal, aScope);
const uint32_t UPDATE_DELAY_MS = 1000;
nsCOMPtr<nsITimer> timer;
const nsresult rv =
NS_NewTimerWithCallback(getter_AddRefs(timer), callback,
UPDATE_DELAY_MS, nsITimer::TYPE_ONE_SHOT);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
entry.Insert(std::move(timer));
});
}
void ServiceWorkerManager::UpdateTimerFired(nsIPrincipal* aPrincipal,
const nsACString& aScope) {
MOZ_ASSERT(NS_IsMainThread());
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;
}
if (auto entry = data->mUpdateTimers.Lookup(aScope)) {
entry.Data()->Cancel();
entry.Remove();
}
RefPtr<ServiceWorkerRegistrationInfo> registration;
data->mInfos.Get(aScope, getter_AddRefs(registration));
if (!registration) {
return;
}
if (!registration->CheckAndClearIfUpdateNeeded()) {
return;
}
OriginAttributes attrs = aPrincipal->OriginAttributesRef();
SoftUpdate(attrs, aScope);
}
void ServiceWorkerManager::MaybeSendUnregister(nsIPrincipal* aPrincipal,
const nsACString& aScope) {
MOZ_ASSERT(NS_IsMainThread());
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));
}
void ServiceWorkerManager::AddOrphanedRegistration(
ServiceWorkerRegistrationInfo* aRegistration) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aRegistration);
MOZ_ASSERT(aRegistration->IsUnregistered());
MOZ_ASSERT(!aRegistration->IsControllingClients());
MOZ_ASSERT(!aRegistration->IsIdle());
MOZ_ASSERT(!mOrphanedRegistrations.has(aRegistration));
MOZ_ALWAYS_TRUE(mOrphanedRegistrations.putNew(aRegistration));
}
void ServiceWorkerManager::RemoveOrphanedRegistration(
ServiceWorkerRegistrationInfo* aRegistration) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aRegistration);
MOZ_ASSERT(aRegistration->IsUnregistered());
MOZ_ASSERT(!aRegistration->IsControllingClients());
MOZ_ASSERT(aRegistration->IsIdle());
MOZ_ASSERT(mOrphanedRegistrations.has(aRegistration));
mOrphanedRegistrations.remove(aRegistration);
}
uint32_t ServiceWorkerManager::MaybeInitServiceWorkerShutdownProgress() const {
if (!mShutdownBlocker) {
return ServiceWorkerShutdownBlocker::kInvalidShutdownStateId;
}
return mShutdownBlocker->CreateShutdownState();
}
void ServiceWorkerManager::ReportServiceWorkerShutdownProgress(
uint32_t aShutdownStateId,
ServiceWorkerShutdownState::Progress aProgress) const {
MOZ_ASSERT(mShutdownBlocker);
mShutdownBlocker->ReportShutdownProgress(aShutdownStateId, aProgress);
}
} // namespace mozilla::dom