/* -*- 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 "ServiceWorkerRegistration.h" #include "mozilla/dom/DOMMozPromiseRequestHolder.h" #include "mozilla/dom/Notification.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/PushManager.h" #include "mozilla/dom/ServiceWorker.h" #include "mozilla/dom/ServiceWorkerRegistrationBinding.h" #include "mozilla/dom/WorkerPrivate.h" #include "nsCycleCollectionParticipant.h" #include "nsISupportsPrimitives.h" #include "nsPIDOMWindow.h" #include "RemoteServiceWorkerRegistrationImpl.h" #include "ServiceWorkerRegistrationImpl.h" namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerRegistration, DOMEventTargetHelper, mInstallingWorker, mWaitingWorker, mActiveWorker, mPushManager); NS_IMPL_ADDREF_INHERITED(ServiceWorkerRegistration, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(ServiceWorkerRegistration, DOMEventTargetHelper) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerRegistration) NS_INTERFACE_MAP_ENTRY(ServiceWorkerRegistration) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) namespace { const uint64_t kInvalidUpdateFoundId = 0; } // anonymous namespace ServiceWorkerRegistration::ServiceWorkerRegistration(nsIGlobalObject* aGlobal, const ServiceWorkerRegistrationDescriptor& aDescriptor, ServiceWorkerRegistration::Inner* aInner) : DOMEventTargetHelper(aGlobal) , mDescriptor(aDescriptor) , mInner(aInner) , mScheduledUpdateFoundId(kInvalidUpdateFoundId) , mDispatchedUpdateFoundId(kInvalidUpdateFoundId) , mPendingUpdatePromises(0) { MOZ_DIAGNOSTIC_ASSERT(mInner); KeepAliveIfHasListenersFor(NS_LITERAL_STRING("updatefound")); UpdateState(mDescriptor); mInner->SetServiceWorkerRegistration(this); } ServiceWorkerRegistration::~ServiceWorkerRegistration() { mInner->ClearServiceWorkerRegistration(this); } JSObject* ServiceWorkerRegistration::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return ServiceWorkerRegistration_Binding::Wrap(aCx, this, aGivenProto); } /* static */ already_AddRefed ServiceWorkerRegistration::CreateForMainThread(nsPIDOMWindowInner* aWindow, const ServiceWorkerRegistrationDescriptor& aDescriptor) { MOZ_ASSERT(aWindow); MOZ_ASSERT(NS_IsMainThread()); RefPtr inner; if (ServiceWorkerParentInterceptEnabled()) { inner = new RemoteServiceWorkerRegistrationImpl(aDescriptor); } else { inner = new ServiceWorkerRegistrationMainThread(aDescriptor); } NS_ENSURE_TRUE(inner, nullptr); RefPtr registration = new ServiceWorkerRegistration(aWindow->AsGlobal(), aDescriptor, inner); return registration.forget(); } /* static */ already_AddRefed ServiceWorkerRegistration::CreateForWorker(WorkerPrivate* aWorkerPrivate, nsIGlobalObject* aGlobal, const ServiceWorkerRegistrationDescriptor& aDescriptor) { MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate); MOZ_DIAGNOSTIC_ASSERT(aGlobal); aWorkerPrivate->AssertIsOnWorkerThread(); RefPtr inner; if (ServiceWorkerParentInterceptEnabled()) { inner = new RemoteServiceWorkerRegistrationImpl(aDescriptor); } else { inner = new ServiceWorkerRegistrationWorkerThread(aDescriptor); } NS_ENSURE_TRUE(inner, nullptr); RefPtr registration = new ServiceWorkerRegistration(aGlobal, aDescriptor, inner); return registration.forget(); } void ServiceWorkerRegistration::DisconnectFromOwner() { DOMEventTargetHelper::DisconnectFromOwner(); } void ServiceWorkerRegistration::RegistrationRemoved() { // Its possible that the registration will fail to install and be // immediately removed. In that case we may never receive the // UpdateState() call if the actor was too slow to connect, etc. // Ensure that we force all our known actors to redundant so that // the appropriate statechange events are fired. If we got the // UpdateState() already then this will be a no-op. UpdateStateInternal(Maybe(), Maybe(), Maybe()); // Our underlying registration was removed from SWM, so we // will never get an updatefound event again. We can let // the object GC if content is not holding it alive. IgnoreKeepAliveIfHasListenersFor(NS_LITERAL_STRING("updatefound")); } already_AddRefed ServiceWorkerRegistration::GetInstalling() const { RefPtr ref = mInstallingWorker; return ref.forget(); } already_AddRefed ServiceWorkerRegistration::GetWaiting() const { RefPtr ref = mWaitingWorker; return ref.forget(); } already_AddRefed ServiceWorkerRegistration::GetActive() const { RefPtr ref = mActiveWorker; return ref.forget(); } void ServiceWorkerRegistration::UpdateState(const ServiceWorkerRegistrationDescriptor& aDescriptor) { MOZ_DIAGNOSTIC_ASSERT(MatchesDescriptor(aDescriptor)); mDescriptor = aDescriptor; UpdateStateInternal(aDescriptor.GetInstalling(), aDescriptor.GetWaiting(), aDescriptor.GetActive()); nsTArray> callbackList; mVersionCallbackList.SwapElements(callbackList); for (auto& cb : callbackList) { if (cb->mVersion > mDescriptor.Version()) { mVersionCallbackList.AppendElement(std::move(cb)); continue; } cb->mFunc(cb->mVersion == mDescriptor.Version()); } } bool ServiceWorkerRegistration::MatchesDescriptor(const ServiceWorkerRegistrationDescriptor& aDescriptor) const { return aDescriptor.Id() == mDescriptor.Id() && aDescriptor.PrincipalInfo() == mDescriptor.PrincipalInfo() && aDescriptor.Scope() == mDescriptor.Scope(); } void ServiceWorkerRegistration::GetScope(nsAString& aScope) const { CopyUTF8toUTF16(mDescriptor.Scope(), aScope); } ServiceWorkerUpdateViaCache ServiceWorkerRegistration::GetUpdateViaCache(ErrorResult& aRv) const { return mDescriptor.UpdateViaCache(); } already_AddRefed ServiceWorkerRegistration::Update(ErrorResult& aRv) { if (!mInner) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } nsIGlobalObject* global = GetParentObject(); if (!global) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } RefPtr outer = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr self = this; mPendingUpdatePromises += 1; mInner->Update( [outer, self](const ServiceWorkerRegistrationDescriptor& aDesc) { auto scopeExit = MakeScopeExit([&] { self->UpdatePromiseSettled(); }); nsIGlobalObject* global = self->GetParentObject(); MOZ_DIAGNOSTIC_ASSERT(global); RefPtr ref = global->GetOrCreateServiceWorkerRegistration(aDesc); if (!ref) { outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); return; } outer->MaybeResolve(ref); }, [outer, self] (ErrorResult& aRv) { auto scopeExit = MakeScopeExit([&] { self->UpdatePromiseSettled(); }); outer->MaybeReject(aRv); }); return outer.forget(); } already_AddRefed ServiceWorkerRegistration::Unregister(ErrorResult& aRv) { nsIGlobalObject* global = GetParentObject(); if (NS_WARN_IF(!global)) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } RefPtr outer = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } if (!mInner) { outer->MaybeResolve(false); return outer.forget(); } mInner->Unregister( [outer] (bool aSuccess) { outer->MaybeResolve(aSuccess); }, [outer] (ErrorResult& aRv) { // register() should be resilient and resolve false instead // of rejecting in most cases. outer->MaybeResolve(false); }); return outer.forget(); } already_AddRefed ServiceWorkerRegistration::GetPushManager(JSContext* aCx, ErrorResult& aRv) { if (!mPushManager) { nsCOMPtr globalObject = GetParentObject(); if (!globalObject) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } GlobalObject global(aCx, globalObject->GetGlobalJSObject()); mPushManager = PushManager::Constructor(global, NS_ConvertUTF8toUTF16(mDescriptor.Scope()), aRv); if (aRv.Failed()) { return nullptr; } } RefPtr ret = mPushManager; return ret.forget(); } already_AddRefed ServiceWorkerRegistration::ShowNotification(JSContext* aCx, const nsAString& aTitle, const NotificationOptions& aOptions, ErrorResult& aRv) { nsIGlobalObject* global = GetParentObject(); if (!global) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } NS_ConvertUTF8toUTF16 scope(mDescriptor.Scope()); // Until we ship ServiceWorker objects on worker threads the active // worker will always be nullptr. So limit this check to main // thread for now. if (mDescriptor.GetActive().isNothing() && NS_IsMainThread()) { aRv.ThrowTypeError(scope); return nullptr; } RefPtr p = Notification::ShowPersistentNotification(aCx, global, scope, aTitle, aOptions, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } return p.forget(); } already_AddRefed ServiceWorkerRegistration::GetNotifications(const GetNotificationOptions& aOptions, ErrorResult& aRv) { nsIGlobalObject* global = GetParentObject(); if (!global) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } NS_ConvertUTF8toUTF16 scope(mDescriptor.Scope()); if (NS_IsMainThread()) { nsCOMPtr window = do_QueryInterface(global); if (NS_WARN_IF(!window)) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } return Notification::Get(window, aOptions, scope, aRv); } WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); worker->AssertIsOnWorkerThread(); return Notification::WorkerGet(worker, aOptions, scope, aRv); } const ServiceWorkerRegistrationDescriptor& ServiceWorkerRegistration::Descriptor() const { return mDescriptor; } void ServiceWorkerRegistration::WhenVersionReached(uint64_t aVersion, ServiceWorkerBoolCallback&& aCallback) { if (aVersion <= mDescriptor.Version()) { aCallback(aVersion == mDescriptor.Version()); return; } mVersionCallbackList.AppendElement( MakeUnique(aVersion, std::move(aCallback))); } void ServiceWorkerRegistration::MaybeScheduleUpdateFound(const Maybe& aInstallingDescriptor) { uint64_t newId = aInstallingDescriptor.isSome() ? aInstallingDescriptor.ref().Id() : kInvalidUpdateFoundId; if (mScheduledUpdateFoundId != kInvalidUpdateFoundId) { if (mScheduledUpdateFoundId == newId) { return; } MaybeDispatchUpdateFound(); MOZ_DIAGNOSTIC_ASSERT(mScheduledUpdateFoundId == kInvalidUpdateFoundId); } bool updateFound = newId != kInvalidUpdateFoundId && mDispatchedUpdateFoundId != newId; if (!updateFound) { return; } mScheduledUpdateFoundId = newId; if (mPendingUpdatePromises > 0) { return; } nsIGlobalObject* global = GetParentObject(); NS_ENSURE_TRUE_VOID(global); nsCOMPtr r = NewCancelableRunnableMethod( "ServiceWorkerRegistration::MaybeDispatchUpdateFound", this, &ServiceWorkerRegistration::MaybeDispatchUpdateFound); Unused << global->EventTargetFor(TaskCategory::Other)->Dispatch( r.forget(), NS_DISPATCH_NORMAL); } void ServiceWorkerRegistration::MaybeDispatchUpdateFound() { uint64_t scheduledId = mScheduledUpdateFoundId; mScheduledUpdateFoundId = kInvalidUpdateFoundId; if (scheduledId == kInvalidUpdateFoundId || scheduledId == mDispatchedUpdateFoundId) { return; } mDispatchedUpdateFoundId = scheduledId; DispatchTrustedEvent(NS_LITERAL_STRING("updatefound")); } void ServiceWorkerRegistration::UpdatePromiseSettled() { MOZ_DIAGNOSTIC_ASSERT(mPendingUpdatePromises > 0); mPendingUpdatePromises -= 1; if (mPendingUpdatePromises > 0 || mScheduledUpdateFoundId == kInvalidUpdateFoundId) { return; } nsIGlobalObject* global = GetParentObject(); NS_ENSURE_TRUE_VOID(global); nsCOMPtr r = NewCancelableRunnableMethod( "ServiceWorkerRegistration::MaybeDispatchUpdateFound", this, &ServiceWorkerRegistration::MaybeDispatchUpdateFound); Unused << global->EventTargetFor(TaskCategory::Other)->Dispatch( r.forget(), NS_DISPATCH_NORMAL); } void ServiceWorkerRegistration::UpdateStateInternal(const Maybe& aInstalling, const Maybe& aWaiting, const Maybe& aActive) { // Do this immediately as it may flush an already pending updatefound // event. In that case we want to fire the pending event before // modifying any of the registration properties. MaybeScheduleUpdateFound(aInstalling); // Move the currently exposed workers into a separate list // of "old" workers. We will then potentially add them // back to the registration properties below based on the // given descriptor. Any that are not restored will need // to be moved to the redundant state. AutoTArray, 3> oldWorkerList({ mInstallingWorker.forget(), mWaitingWorker.forget(), mActiveWorker.forget(), }); // Its important that all state changes are actually applied before // dispatching any statechange events. Each ServiceWorker object // should be in the correct state and the ServiceWorkerRegistration // properties need to be set correctly as well. To accomplish this // we use a ScopeExit to dispatch any statechange events. auto scopeExit = MakeScopeExit([&] { // Check to see if any of the "old" workers was completely discarded. // Set these workers to the redundant state. for (auto& oldWorker : oldWorkerList) { if (!oldWorker || oldWorker == mInstallingWorker || oldWorker == mWaitingWorker || oldWorker == mActiveWorker) { continue; } oldWorker->SetState(ServiceWorkerState::Redundant); } // Check each worker to see if it needs a statechange event dispatched. if (mInstallingWorker) { mInstallingWorker->MaybeDispatchStateChangeEvent(); } if (mWaitingWorker) { mWaitingWorker->MaybeDispatchStateChangeEvent(); } if (mActiveWorker) { mActiveWorker->MaybeDispatchStateChangeEvent(); } // We also check the "old" workers to see if they need a statechange // event as well. Note, these may overlap with the known worker properties // above, but MaybeDispatchStateChangeEvent() will ignore duplicated calls. for (auto& oldWorker : oldWorkerList) { if (!oldWorker) { continue; } oldWorker->MaybeDispatchStateChangeEvent(); } }); // Clear all workers if the registration has been detached from the global. // Also, we cannot expose ServiceWorker objects on worker threads yet, so // do the same on when off-main-thread. This main thread check should be // removed as part of bug 1113522. nsCOMPtr global = GetParentObject(); if (!global || !NS_IsMainThread()) { return; } if (aActive.isSome()) { if ((mActiveWorker = global->GetOrCreateServiceWorker(aActive.ref()))) { mActiveWorker->SetState(aActive.ref().State()); } } else { mActiveWorker = nullptr; } if (aWaiting.isSome()) { if ((mWaitingWorker = global->GetOrCreateServiceWorker(aWaiting.ref()))) { mWaitingWorker->SetState(aWaiting.ref().State()); } } else { mWaitingWorker = nullptr; } if (aInstalling.isSome()) { if ((mInstallingWorker = global->GetOrCreateServiceWorker(aInstalling.ref()))) { mInstallingWorker->SetState(aInstalling.ref().State()); } } else { mInstallingWorker = nullptr; } } } // dom namespace } // mozilla namespace