/* -*- 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 "mozIApplication.h" #include "nsIAppsService.h" #include "nsIConsoleService.h" #include "nsIDOMEventTarget.h" #include "nsIDocument.h" #include "nsIScriptSecurityManager.h" #include "nsIStreamLoader.h" #include "nsIHttpChannel.h" #include "nsIHttpChannelInternal.h" #include "nsIHttpHeaderVisitor.h" #include "nsIJARChannel.h" #include "nsINetworkInterceptController.h" #include "nsIMutableArray.h" #include "nsIScriptError.h" #include "nsISimpleEnumerator.h" #include "nsITimer.h" #include "nsIUploadChannel2.h" #include "nsPIDOMWindow.h" #include "nsScriptLoader.h" #include "nsServiceManagerUtils.h" #include "nsDebug.h" #include "nsISupportsPrimitives.h" #include "jsapi.h" #include "mozilla/BasePrincipal.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/ErrorNames.h" #include "mozilla/LoadContext.h" #include "mozilla/Telemetry.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/DOMError.h" #include "mozilla/dom/ErrorEvent.h" #include "mozilla/dom/Headers.h" #include "mozilla/dom/indexedDB/IndexedDatabaseManager.h" #include "mozilla/dom/indexedDB/IDBFactory.h" #include "mozilla/dom/InternalHeaders.h" #include "mozilla/dom/Navigator.h" #include "mozilla/dom/NotificationEvent.h" #include "mozilla/dom/PromiseNativeHandler.h" #include "mozilla/dom/Request.h" #include "mozilla/dom/RootedDictionary.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "mozilla/unused.h" #include "mozilla/EnumSet.h" #include "nsContentPolicyUtils.h" #include "nsContentSecurityManager.h" #include "nsContentUtils.h" #include "nsGlobalWindow.h" #include "nsNetUtil.h" #include "nsIURL.h" #include "nsProxyRelease.h" #include "nsQueryObject.h" #include "nsTArray.h" #include "RuntimeService.h" #include "ServiceWorker.h" #include "ServiceWorkerClient.h" #include "ServiceWorkerContainer.h" #include "ServiceWorkerManagerChild.h" #include "ServiceWorkerPrivate.h" #include "ServiceWorkerRegistrar.h" #include "ServiceWorkerRegistration.h" #include "ServiceWorkerScriptCache.h" #include "ServiceWorkerEvents.h" #include "SharedWorker.h" #include "WorkerInlines.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "WorkerScope.h" #ifndef MOZ_SIMPLEPUSH #include "mozilla/dom/TypedArray.h" #endif #ifdef PostMessage #undef PostMessage #endif using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::ipc; BEGIN_WORKERS_NAMESPACE #define PURGE_DOMAIN_DATA "browser:purge-domain-data" #define PURGE_SESSION_HISTORY "browser:purge-session-history" #define CLEAR_ORIGIN_DATA "clear-origin-data" static_assert(nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN == static_cast(RequestMode::Same_origin), "RequestMode enumeration value should match Necko CORS mode value."); static_assert(nsIHttpChannelInternal::CORS_MODE_NO_CORS == static_cast(RequestMode::No_cors), "RequestMode enumeration value should match Necko CORS mode value."); static_assert(nsIHttpChannelInternal::CORS_MODE_CORS == static_cast(RequestMode::Cors), "RequestMode enumeration value should match Necko CORS mode value."); static_assert(nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW == static_cast(RequestRedirect::Follow), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert(nsIHttpChannelInternal::REDIRECT_MODE_ERROR == static_cast(RequestRedirect::Error), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert(nsIHttpChannelInternal::REDIRECT_MODE_MANUAL == static_cast(RequestRedirect::Manual), "RequestRedirect enumeration value should make Necko Redirect mode value."); static_assert(3 == static_cast(RequestRedirect::EndGuard_), "RequestRedirect enumeration value should make Necko Redirect mode value."); static StaticRefPtr gInstance; struct ServiceWorkerManager::RegistrationDataPerPrincipal final { // Ordered list of scopes for glob matching. // Each entry is an absolute URL representing the scope. // Each value of the hash table is an array of an absolute URLs representing // the scopes. // // An array is used for now since the number of controlled scopes per // domain is expected to be relatively low. If that assumption was proved // wrong this should be replaced with a better structure to avoid the // memmoves associated with inserting stuff in the middle of the array. nsTArray mOrderedScopes; // Scope to registration. // The scope should be a fully qualified valid URL. nsRefPtrHashtable mInfos; // Maps scopes to job queues. nsClassHashtable mJobQueues; // Map scopes to scheduled update timers. nsInterfaceHashtable mUpdateTimers; }; struct ServiceWorkerManager::PendingOperation final { nsCOMPtr mRunnable; ServiceWorkerJobQueue* mQueue; RefPtr mJob; ServiceWorkerRegistrationData mRegistration; }; class ServiceWorkerJob : public nsISupports { friend class ServiceWorkerJobQueue; public: NS_DECL_ISUPPORTS enum Type { RegisterJob, UpdateJob, InstallJob, UnregisterJob }; virtual void Start() = 0; void Cancel() { mQueue = nullptr; mCanceled = true; } bool IsRegisterOrInstallJob() const { return mJobType == RegisterJob || mJobType == UpdateJob || mJobType == InstallJob; } protected: // The queue keeps the jobs alive, so they can hold a rawptr back to the // queue. ServiceWorkerJobQueue* mQueue; Type mJobType; bool mCanceled; explicit ServiceWorkerJob(ServiceWorkerJobQueue* aQueue, Type aJobType) : mQueue(aQueue) , mJobType(aJobType) , mCanceled(false) {} virtual ~ServiceWorkerJob() {} void Done(nsresult aStatus); }; class ServiceWorkerJobQueue final { friend class ServiceWorkerJob; struct QueueData final { QueueData() : mPopping(false) { } ~QueueData() { if (!mJobs.IsEmpty()) { NS_WARNING("Pending/running jobs still around on shutdown!"); } } nsTArray> mJobs; bool mPopping; }; const nsCString mOriginAttributesSuffix; QueueData mRegistrationJobQueue; QueueData mInstallationJobQueue; public: explicit ServiceWorkerJobQueue(const nsACString& aScopeKey) : mOriginAttributesSuffix(aScopeKey) {} ~ServiceWorkerJobQueue() { } void Append(ServiceWorkerJob* aJob) { MOZ_ASSERT(aJob); QueueData& queue = GetQueue(aJob->mJobType); MOZ_ASSERT(!queue.mJobs.Contains(aJob)); bool wasEmpty = queue.mJobs.IsEmpty(); queue.mJobs.AppendElement(aJob); if (wasEmpty) { aJob->Start(); } } void CancelJobs(); private: void CancelJobs(QueueData& aQueue); // Internal helper function used to assign jobs to the correct queue. QueueData& GetQueue(ServiceWorkerJob::Type aType) { switch (aType) { case ServiceWorkerJob::Type::RegisterJob: case ServiceWorkerJob::Type::UpdateJob: case ServiceWorkerJob::Type::UnregisterJob: return mRegistrationJobQueue; case ServiceWorkerJob::Type::InstallJob: return mInstallationJobQueue; default: MOZ_CRASH("Invalid job queue type."); return mRegistrationJobQueue; } } bool IsEmpty() { return mRegistrationJobQueue.mJobs.IsEmpty() && mInstallationJobQueue.mJobs.IsEmpty(); } void Pop(QueueData& aQueue) { MOZ_ASSERT(!aQueue.mPopping, "Pop() called recursively, did you write a job which calls Done() synchronously from Start()?"); AutoRestore savePopping(aQueue.mPopping); aQueue.mPopping = true; MOZ_ASSERT(!aQueue.mJobs.IsEmpty()); aQueue.mJobs.RemoveElementAt(0); if (!aQueue.mJobs.IsEmpty()) { aQueue.mJobs[0]->Start(); } else if (IsEmpty()) { RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); swm->MaybeRemoveRegistrationInfo(mOriginAttributesSuffix); } } void Done(ServiceWorkerJob* aJob) { MOZ_ASSERT(aJob); QueueData& queue = GetQueue(aJob->mJobType); MOZ_ASSERT(!queue.mJobs.IsEmpty()); MOZ_ASSERT(queue.mJobs[0] == aJob); if (NS_WARN_IF(queue.mJobs[0] != aJob)) { return; } Pop(queue); } }; namespace { nsresult PopulateRegistrationData(nsIPrincipal* aPrincipal, const ServiceWorkerRegistrationInfo* aRegistration, ServiceWorkerRegistrationData& aData) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aRegistration); if (NS_WARN_IF(!BasePrincipal::Cast(aPrincipal)->IsCodebasePrincipal())) { return NS_ERROR_FAILURE; } nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aData.principal()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } aData.scope() = aRegistration->mScope; RefPtr newest = aRegistration->Newest(); if (NS_WARN_IF(!newest)) { return NS_ERROR_FAILURE; } aData.scriptSpec() = newest->ScriptSpec(); if (aRegistration->mActiveWorker) { aData.currentWorkerURL() = aRegistration->mActiveWorker->ScriptSpec(); aData.activeCacheName() = aRegistration->mActiveWorker->CacheName(); } if (aRegistration->mWaitingWorker) { aData.waitingCacheName() = aRegistration->mWaitingWorker->CacheName(); } return NS_OK; } class TeardownRunnable final : public nsRunnable { public: explicit TeardownRunnable(ServiceWorkerManagerChild* aActor) : mActor(aActor) { MOZ_ASSERT(mActor); } NS_IMETHODIMP Run() override { MOZ_ASSERT(mActor); mActor->SendShutdown(); return NS_OK; } private: ~TeardownRunnable() {} RefPtr mActor; }; } // namespace NS_IMPL_ISUPPORTS0(ServiceWorkerJob) void ServiceWorkerJob::Done(nsresult aStatus) { if (NS_WARN_IF(NS_FAILED(aStatus))) { #ifdef DEBUG nsAutoCString errorName; GetErrorName(aStatus, errorName); #endif NS_WARNING(nsPrintfCString("ServiceWorkerJob failed with error: %s", errorName.get()).get()); } if (mQueue) { mQueue->Done(this); } } void ServiceWorkerRegistrationInfo::Clear() { if (mInstallingWorker) { mInstallingWorker->UpdateState(ServiceWorkerState::Redundant); mInstallingWorker->WorkerPrivate()->NoteDeadServiceWorkerInfo(); mInstallingWorker = nullptr; // FIXME(nsm): Abort any inflight requests from installing worker. } if (mWaitingWorker) { mWaitingWorker->UpdateState(ServiceWorkerState::Redundant); nsresult rv = serviceWorkerScriptCache::PurgeCache(mPrincipal, mWaitingWorker->CacheName()); if (NS_FAILED(rv)) { NS_WARNING("Failed to purge the waiting cache."); } mWaitingWorker->WorkerPrivate()->NoteDeadServiceWorkerInfo(); mWaitingWorker = nullptr; } if (mActiveWorker) { mActiveWorker->UpdateState(ServiceWorkerState::Redundant); nsresult rv = serviceWorkerScriptCache::PurgeCache(mPrincipal, mActiveWorker->CacheName()); if (NS_FAILED(rv)) { NS_WARNING("Failed to purge the active cache."); } mActiveWorker->WorkerPrivate()->NoteDeadServiceWorkerInfo(); mActiveWorker = nullptr; } RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); swm->InvalidateServiceWorkerRegistrationWorker(this, WhichServiceWorker::INSTALLING_WORKER | WhichServiceWorker::WAITING_WORKER | WhichServiceWorker::ACTIVE_WORKER); } ServiceWorkerRegistrationInfo::ServiceWorkerRegistrationInfo(const nsACString& aScope, nsIPrincipal* aPrincipal) : mControlledDocumentsCounter(0) , mUpdateState(NoUpdate) , mLastUpdateCheckTime(0) , mScope(aScope) , mPrincipal(aPrincipal) , mUpdating(false) , mPendingUninstall(false) {} ServiceWorkerRegistrationInfo::~ServiceWorkerRegistrationInfo() { if (IsControllingDocuments()) { NS_WARNING("ServiceWorkerRegistrationInfo is still controlling documents. This can be a bug or a leak in ServiceWorker API or in any other API that takes the document alive."); } } NS_IMPL_ISUPPORTS(ServiceWorkerRegistrationInfo, nsIServiceWorkerRegistrationInfo) NS_IMETHODIMP ServiceWorkerRegistrationInfo::GetPrincipal(nsIPrincipal** aPrincipal) { AssertIsOnMainThread(); NS_ADDREF(*aPrincipal = mPrincipal); return NS_OK; } NS_IMETHODIMP ServiceWorkerRegistrationInfo::GetScope(nsAString& aScope) { AssertIsOnMainThread(); CopyUTF8toUTF16(mScope, aScope); return NS_OK; } NS_IMETHODIMP ServiceWorkerRegistrationInfo::GetScriptSpec(nsAString& aScriptSpec) { AssertIsOnMainThread(); RefPtr newest = Newest(); if (newest) { CopyUTF8toUTF16(newest->ScriptSpec(), aScriptSpec); } return NS_OK; } NS_IMETHODIMP ServiceWorkerRegistrationInfo::GetInstallingWorker(nsIServiceWorkerInfo **aResult) { AssertIsOnMainThread(); nsCOMPtr info = do_QueryInterface(mInstallingWorker); info.forget(aResult); return NS_OK; } NS_IMETHODIMP ServiceWorkerRegistrationInfo::GetWaitingWorker(nsIServiceWorkerInfo **aResult) { AssertIsOnMainThread(); nsCOMPtr info = do_QueryInterface(mWaitingWorker); info.forget(aResult); return NS_OK; } NS_IMETHODIMP ServiceWorkerRegistrationInfo::GetActiveWorker(nsIServiceWorkerInfo **aResult) { AssertIsOnMainThread(); nsCOMPtr info = do_QueryInterface(mActiveWorker); info.forget(aResult); return NS_OK; } NS_IMETHODIMP ServiceWorkerRegistrationInfo::AddListener( nsIServiceWorkerRegistrationInfoListener *aListener) { AssertIsOnMainThread(); if (!aListener || mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.AppendElement(aListener); return NS_OK; } NS_IMETHODIMP ServiceWorkerRegistrationInfo::RemoveListener( nsIServiceWorkerRegistrationInfoListener *aListener) { AssertIsOnMainThread(); if (!aListener || !mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.RemoveElement(aListener); return NS_OK; } already_AddRefed ServiceWorkerRegistrationInfo::GetServiceWorkerInfoById(uint64_t aId) { RefPtr serviceWorker; if (mInstallingWorker && mInstallingWorker->ID() == aId) { serviceWorker = mInstallingWorker; } else if (mWaitingWorker && mWaitingWorker->ID() == aId) { serviceWorker = mWaitingWorker; } else if (mActiveWorker && mActiveWorker->ID() == aId) { serviceWorker = mActiveWorker; } return serviceWorker.forget(); } ////////////////////////// // ServiceWorkerManager // ////////////////////////// NS_IMPL_ADDREF(ServiceWorkerManager) NS_IMPL_RELEASE(ServiceWorkerManager) NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager) NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager) NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIServiceWorkerManager) NS_INTERFACE_MAP_END ServiceWorkerManager::ServiceWorkerManager() : mActor(nullptr) , mShuttingDown(false) { // Register this component to PBackground. MOZ_ALWAYS_TRUE(BackgroundChild::GetOrCreateForCurrentThread(this)); } ServiceWorkerManager::~ServiceWorkerManager() { // The map will assert if it is not empty when destroyed. mRegistrationInfos.Clear(); MOZ_ASSERT(!mActor); } void ServiceWorkerManager::Init() { nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { DebugOnly rv; rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); } if (XRE_IsParentProcess()) { RefPtr swr = ServiceWorkerRegistrar::Get(); MOZ_ASSERT(swr); nsTArray data; swr->GetRegistrations(data); LoadRegistrations(data); if (obs) { DebugOnly rv; rv = obs->AddObserver(this, PURGE_SESSION_HISTORY, false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = obs->AddObserver(this, PURGE_DOMAIN_DATA, false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = obs->AddObserver(this, CLEAR_ORIGIN_DATA, false /* ownsWeak */); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } } class ContinueLifecycleTask : public nsISupports { NS_DECL_ISUPPORTS protected: virtual ~ContinueLifecycleTask() {} public: virtual void ContinueAfterWorkerEvent(bool aSuccess) = 0; }; NS_IMPL_ISUPPORTS0(ContinueLifecycleTask); class ServiceWorkerInstallJob; class ContinueInstallTask final : public ContinueLifecycleTask { RefPtr mJob; public: explicit ContinueInstallTask(ServiceWorkerInstallJob* aJob) : mJob(aJob) {} void ContinueAfterWorkerEvent(bool aSuccess) override; }; class ContinueActivateTask final : public ContinueLifecycleTask { RefPtr mRegistration; public: explicit ContinueActivateTask(ServiceWorkerRegistrationInfo* aReg) : mRegistration(aReg) {} void ContinueAfterWorkerEvent(bool aSuccess) override; }; class ContinueLifecycleRunnable final : public LifeCycleEventCallback { nsMainThreadPtrHandle mTask; bool mSuccess; public: explicit ContinueLifecycleRunnable(const nsMainThreadPtrHandle& aTask) : mTask(aTask) , mSuccess(false) { AssertIsOnMainThread(); } void SetResult(bool aResult) override { mSuccess = aResult; } NS_IMETHOD Run() override { AssertIsOnMainThread(); mTask->ContinueAfterWorkerEvent(mSuccess); return NS_OK; } }; class ServiceWorkerResolveWindowPromiseOnUpdateCallback final : public ServiceWorkerUpdateFinishCallback { RefPtr mWindow; // The promise "returned" by the call to Update up to // navigator.serviceWorker.register(). RefPtr mPromise; ~ServiceWorkerResolveWindowPromiseOnUpdateCallback() {} public: ServiceWorkerResolveWindowPromiseOnUpdateCallback(nsPIDOMWindow* aWindow, Promise* aPromise) : mWindow(aWindow) , mPromise(aPromise) {} void UpdateSucceeded(ServiceWorkerRegistrationInfo* aInfo) override { RefPtr swr = mWindow->GetServiceWorkerRegistration(NS_ConvertUTF8toUTF16(aInfo->mScope)); mPromise->MaybeResolve(swr); } void UpdateFailed(ErrorResult& aStatus) override { mPromise->MaybeReject(aStatus); } }; class ContinueUpdateRunnable final : public LifeCycleEventCallback { nsMainThreadPtrHandle mJob; bool mScriptEvaluationResult; public: explicit ContinueUpdateRunnable(const nsMainThreadPtrHandle aJob) : mJob(aJob) , mScriptEvaluationResult(false) { AssertIsOnMainThread(); } void SetResult(bool aResult) { mScriptEvaluationResult = aResult; } NS_IMETHOD Run(); }; namespace { /** * The spec mandates slightly different behaviors for computing the scope * prefix string in case a Service-Worker-Allowed header is specified versus * when it's not available. * * With the header: * "Set maxScopeString to "/" concatenated with the strings in maxScope's * path (including empty strings), separated from each other by "/"." * Without the header: * "Set maxScopeString to "/" concatenated with the strings, except the last * string that denotes the script's file name, in registration's registering * script url's path (including empty strings), separated from each other by * "/"." * * In simpler terms, if the header is not present, we should only use the * "directory" part of the pathname, and otherwise the entire pathname should be * used. ScopeStringPrefixMode allows the caller to specify the desired * behavior. */ enum ScopeStringPrefixMode { eUseDirectory, eUsePath }; nsresult GetRequiredScopeStringPrefix(nsIURI* aScriptURI, nsACString& aPrefix, ScopeStringPrefixMode aPrefixMode) { nsresult rv = aScriptURI->GetPrePath(aPrefix); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (aPrefixMode == eUseDirectory) { nsCOMPtr scriptURL(do_QueryInterface(aScriptURI)); if (NS_WARN_IF(!scriptURL)) { return NS_ERROR_FAILURE; } nsAutoCString dir; rv = scriptURL->GetDirectory(dir); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } aPrefix.Append(dir); } else if (aPrefixMode == eUsePath) { nsAutoCString path; rv = aScriptURI->GetPath(path); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } aPrefix.Append(path); } else { MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode"); } return NS_OK; } class PropagateSoftUpdateRunnable final : public nsRunnable { public: PropagateSoftUpdateRunnable(const PrincipalOriginAttributes& aOriginAttributes, const nsAString& aScope) : mOriginAttributes(aOriginAttributes) , mScope(aScope) {} NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); swm->PropagateSoftUpdate(mOriginAttributes, mScope); return NS_OK; } private: ~PropagateSoftUpdateRunnable() {} const PrincipalOriginAttributes mOriginAttributes; const nsString mScope; }; class PropagateUnregisterRunnable final : public nsRunnable { public: PropagateUnregisterRunnable(nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback, const nsAString& aScope) : mPrincipal(aPrincipal) , mCallback(aCallback) , mScope(aScope) { MOZ_ASSERT(aPrincipal); } NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); nsresult rv = swm->PropagateUnregister(mPrincipal, mCallback, mScope); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } private: ~PropagateUnregisterRunnable() {} nsCOMPtr mPrincipal; nsCOMPtr mCallback; const nsString mScope; }; class RemoveRunnable final : public nsRunnable { public: explicit RemoveRunnable(const nsACString& aHost) {} NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); swm->Remove(mHost); return NS_OK; } private: ~RemoveRunnable() {} const nsCString mHost; }; class PropagateRemoveRunnable final : public nsRunnable { public: explicit PropagateRemoveRunnable(const nsACString& aHost) {} NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); swm->PropagateRemove(mHost); return NS_OK; } private: ~PropagateRemoveRunnable() {} const nsCString mHost; }; class PropagateRemoveAllRunnable final : public nsRunnable { public: PropagateRemoveAllRunnable() {} NS_IMETHOD Run() override { AssertIsOnMainThread(); RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); swm->PropagateRemoveAll(); return NS_OK; } private: ~PropagateRemoveAllRunnable() {} }; } // namespace class ServiceWorkerJobBase : public ServiceWorkerJob { public: ServiceWorkerJobBase(ServiceWorkerJobQueue* aQueue, ServiceWorkerJob::Type aJobType, ServiceWorkerUpdateFinishCallback* aCallback) : ServiceWorkerJobBase(aQueue, aJobType, aCallback, nullptr, nullptr) { } ServiceWorkerJobBase(ServiceWorkerJobQueue* aQueue, ServiceWorkerJob::Type aJobType, ServiceWorkerUpdateFinishCallback* aCallback, ServiceWorkerRegistrationInfo* aRegistration, ServiceWorkerInfo* aServiceWorkerInfo) : ServiceWorkerJob(aQueue, aJobType) , mCallback(aCallback) , mRegistration(aRegistration) , mUpdateAndInstallInfo(aServiceWorkerInfo) { AssertIsOnMainThread(); } protected: RefPtr mCallback; RefPtr mRegistration; RefPtr mUpdateAndInstallInfo; ~ServiceWorkerJobBase() { } void Succeed() { AssertIsOnMainThread(); // We don't have a callback for soft updates. if (mCallback) { mCallback->UpdateSucceeded(mRegistration); mCallback = nullptr; } } }; // Base type for jobs that work with a specific service worker script. class ServiceWorkerScriptJobBase : public ServiceWorkerJobBase { protected: const nsCString mScriptSpec; ServiceWorkerScriptJobBase(ServiceWorkerJobQueue* aQueue, ServiceWorkerJob::Type aJobType, ServiceWorkerUpdateFinishCallback* aCallback, ServiceWorkerRegistrationInfo* aRegistration, ServiceWorkerInfo* aServiceWorkerInfo, const nsACString& aScriptSpec) : ServiceWorkerJobBase(aQueue, aJobType, aCallback, aRegistration, aServiceWorkerInfo) , mScriptSpec(aScriptSpec) { } // This MUST only be called when the job is still performing actions related // to registration or update. After the spec resolves the update promise, use // Done() with the failure code instead. // Callers MUST hold a strong ref before calling this! void FailWithErrorResult(ErrorResult& aRv) { AssertIsOnMainThread(); MOZ_ASSERT(mRegistration); // With cancellation support, we may only be running with one reference // from another object like a stream loader or something. RefPtr kungFuDeathGrip = this; // Save off the plain error code to pass to Done() where its logged to // stderr as a warning. nsresult origStatus = static_cast(aRv.ErrorCodeAsInt()); // Ensure that we only surface SecurityErr or TypeErr to script. if (aRv.Failed() && !aRv.ErrorCodeIs(NS_ERROR_DOM_SECURITY_ERR) && !aRv.ErrorCodeIs(NS_ERROR_DOM_TYPE_ERR)) { // Remove the old error code so we can replace it with a TypeError. aRv.SuppressException(); NS_ConvertUTF8toUTF16 scriptSpec(mScriptSpec); NS_ConvertUTF8toUTF16 scope(mRegistration->mScope); // Throw the type error with a generic error message. aRv.ThrowTypeError(scriptSpec, scope); } if (mCallback) { mCallback->UpdateFailed(aRv); mCallback = nullptr; } // In case the callback does not consume the exception aRv.SuppressException(); mUpdateAndInstallInfo = nullptr; if (mRegistration->mInstallingWorker) { nsresult rv = serviceWorkerScriptCache::PurgeCache(mRegistration->mPrincipal, mRegistration->mInstallingWorker->CacheName()); if (NS_FAILED(rv)) { NS_WARNING("Failed to purge the installing worker cache."); } } RefPtr swm = ServiceWorkerManager::GetInstance(); swm->MaybeRemoveRegistration(mRegistration); // Ensures that the job can't do anything useful from this point on. mRegistration = nullptr; Done(origStatus); } void Fail(nsresult aRv) { ErrorResult rv(aRv); FailWithErrorResult(rv); } }; class ServiceWorkerInstallJob final : public ServiceWorkerScriptJobBase { friend class ContinueInstallTask; public: ServiceWorkerInstallJob(ServiceWorkerJobQueue* aQueue, ServiceWorkerUpdateFinishCallback* aCallback, ServiceWorkerRegistrationInfo* aRegistration, ServiceWorkerInfo* aServiceWorkerInfo, const nsACString& aScriptSpec) : ServiceWorkerScriptJobBase(aQueue, Type::InstallJob, aCallback, aRegistration, aServiceWorkerInfo, aScriptSpec) { MOZ_ASSERT(aRegistration); } void Start() { AssertIsOnMainThread(); nsCOMPtr r = NS_NewRunnableMethod(this, &ServiceWorkerInstallJob::Install); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(r))); } void Install() { RefPtr kungFuDeathGrip = this; if (mCanceled) { return Fail(NS_ERROR_DOM_ABORT_ERR); } MOZ_ASSERT(mRegistration); // Begin [[Install]] atomic step 3. if (mRegistration->mInstallingWorker) { mRegistration->mInstallingWorker->UpdateState(ServiceWorkerState::Redundant); mRegistration->mInstallingWorker->WorkerPrivate()->TerminateWorker(); } RefPtr swm = ServiceWorkerManager::GetInstance(); swm->InvalidateServiceWorkerRegistrationWorker(mRegistration, WhichServiceWorker::INSTALLING_WORKER); mRegistration->mInstallingWorker = mUpdateAndInstallInfo.forget(); mRegistration->mInstallingWorker->UpdateState(ServiceWorkerState::Installing); mRegistration->NotifyListenersOnChange(); Succeed(); // The job should NOT call fail from this point on. // Step 8 "Queue a task..." for updatefound. nsCOMPtr upr = NS_NewRunnableMethodWithArg>( swm, &ServiceWorkerManager::FireUpdateFoundOnServiceWorkerRegistrations, mRegistration); NS_DispatchToMainThread(upr); // Call ContinueAfterInstallEvent(false) on main thread if the SW // script fails to load. nsCOMPtr failRunnable = NS_NewRunnableMethodWithArgs (this, &ServiceWorkerInstallJob::ContinueAfterInstallEvent, false); nsMainThreadPtrHandle installTask( new nsMainThreadPtrHolder(new ContinueInstallTask(this))); RefPtr callback = new ContinueLifecycleRunnable(installTask); // This triggers Step 4.7 "Queue a task to run the following substeps..." // which sends the install event to the worker. ServiceWorkerPrivate* workerPrivate = mRegistration->mInstallingWorker->WorkerPrivate(); nsresult rv = workerPrivate->SendLifeCycleEvent(NS_LITERAL_STRING("install"), callback, failRunnable); if (NS_WARN_IF(NS_FAILED(rv))) { ContinueAfterInstallEvent(false /* aSuccess */); } } void ContinueAfterInstallEvent(bool aInstallEventSuccess) { if (mCanceled) { return Done(NS_ERROR_DOM_ABORT_ERR); } if (!mRegistration->mInstallingWorker) { NS_WARNING("mInstallingWorker was null."); return Done(NS_ERROR_DOM_ABORT_ERR); } RefPtr swm = ServiceWorkerManager::GetInstance(); // "If installFailed is true" if (NS_WARN_IF(!aInstallEventSuccess)) { mRegistration->mInstallingWorker->UpdateState(ServiceWorkerState::Redundant); mRegistration->mInstallingWorker = nullptr; swm->InvalidateServiceWorkerRegistrationWorker(mRegistration, WhichServiceWorker::INSTALLING_WORKER); swm->MaybeRemoveRegistration(mRegistration); return Done(NS_ERROR_DOM_ABORT_ERR); } // "If registration's waiting worker is not null" if (mRegistration->mWaitingWorker) { mRegistration->mWaitingWorker->WorkerPrivate()->TerminateWorker(); mRegistration->mWaitingWorker->UpdateState(ServiceWorkerState::Redundant); nsresult rv = serviceWorkerScriptCache::PurgeCache(mRegistration->mPrincipal, mRegistration->mWaitingWorker->CacheName()); if (NS_FAILED(rv)) { NS_WARNING("Failed to purge the old waiting cache."); } } mRegistration->mWaitingWorker = mRegistration->mInstallingWorker.forget(); mRegistration->mWaitingWorker->UpdateState(ServiceWorkerState::Installed); mRegistration->NotifyListenersOnChange(); swm->InvalidateServiceWorkerRegistrationWorker(mRegistration, WhichServiceWorker::INSTALLING_WORKER | WhichServiceWorker::WAITING_WORKER); // "If registration's waiting worker's skip waiting flag is set" if (mRegistration->mWaitingWorker->SkipWaitingFlag()) { mRegistration->PurgeActiveWorker(); } Done(NS_OK); // Activate() is invoked out of band of atomic. mRegistration->TryToActivate(); } }; class ServiceWorkerRegisterJob final : public ServiceWorkerScriptJobBase, public serviceWorkerScriptCache::CompareCallback { friend class ContinueUpdateRunnable; nsCString mScope; nsCOMPtr mPrincipal; nsCOMPtr mLoadGroup; ~ServiceWorkerRegisterJob() { } public: NS_DECL_ISUPPORTS_INHERITED // [[Register]] ServiceWorkerRegisterJob(ServiceWorkerJobQueue* aQueue, const nsCString& aScope, const nsCString& aScriptSpec, ServiceWorkerUpdateFinishCallback* aCallback, nsIPrincipal* aPrincipal, nsILoadGroup* aLoadGroup) : ServiceWorkerScriptJobBase(aQueue, Type::RegisterJob, aCallback, nullptr, nullptr, aScriptSpec) , mScope(aScope) , mPrincipal(aPrincipal) , mLoadGroup(aLoadGroup) { AssertIsOnMainThread(); MOZ_ASSERT(mLoadGroup); MOZ_ASSERT(aCallback); } // [[Update]] ServiceWorkerRegisterJob(ServiceWorkerJobQueue* aQueue, ServiceWorkerRegistrationInfo* aRegistration, ServiceWorkerUpdateFinishCallback* aCallback, const nsACString& aScriptSpec) : ServiceWorkerScriptJobBase(aQueue, Type::UpdateJob, aCallback, aRegistration, nullptr, aScriptSpec) { AssertIsOnMainThread(); } void Start() override { AssertIsOnMainThread(); MOZ_ASSERT(!mCanceled); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm->HasBackgroundActor()) { nsCOMPtr runnable = NS_NewRunnableMethod(this, &ServiceWorkerRegisterJob::Start); swm->AppendPendingOperation(runnable); return; } if (mJobType == RegisterJob) { mRegistration = swm->GetRegistration(mPrincipal, mScope); if (mRegistration) { mRegistration->mPendingUninstall = false; RefPtr newest = mRegistration->Newest(); if (newest && mScriptSpec.Equals(newest->ScriptSpec())) { swm->StoreRegistration(mPrincipal, mRegistration); Succeed(); // Done() must always be called async from Start() nsCOMPtr runnable = NS_NewRunnableMethodWithArg( this, &ServiceWorkerRegisterJob::Done, NS_OK); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToCurrentThread(runnable))); return; } } else { mRegistration = swm->CreateNewRegistration(mScope, mPrincipal); } swm->StoreRegistration(mPrincipal, mRegistration); } else { MOZ_ASSERT(mJobType == UpdateJob); // If a different script spec has been registered between when this update // was scheduled and it running now, then simply abort. RefPtr newest = mRegistration->Newest(); if (newest && !mScriptSpec.Equals(newest->ScriptSpec())) { // Done() must always be called async from Start() nsCOMPtr runnable = NS_NewRunnableMethodWithArg( this, &ServiceWorkerRegisterJob::Fail, NS_ERROR_DOM_ABORT_ERR); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToCurrentThread(runnable))); return; } } Update(); } void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual, const nsAString& aNewCacheName, const nsACString& aMaxScope) override { RefPtr kungFuDeathGrip = this; if (NS_WARN_IF(mCanceled)) { Fail(NS_ERROR_DOM_ABORT_ERR); return; } if (NS_WARN_IF(NS_FAILED(aStatus))) { Fail(aStatus); return; } if (aInCacheAndEqual) { Succeed(); Done(NS_OK); return; } AssertIsOnMainThread(); Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED, 1); RefPtr swm = ServiceWorkerManager::GetInstance(); nsCOMPtr scriptURI; nsresult rv = NS_NewURI(getter_AddRefs(scriptURI), mScriptSpec); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(NS_ERROR_DOM_SECURITY_ERR); return; } nsCOMPtr maxScopeURI; if (!aMaxScope.IsEmpty()) { rv = NS_NewURI(getter_AddRefs(maxScopeURI), aMaxScope, nullptr, scriptURI); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(NS_ERROR_DOM_SECURITY_ERR); return; } } nsAutoCString defaultAllowedPrefix; rv = GetRequiredScopeStringPrefix(scriptURI, defaultAllowedPrefix, eUseDirectory); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(NS_ERROR_DOM_SECURITY_ERR); return; } nsAutoCString maxPrefix(defaultAllowedPrefix); if (maxScopeURI) { rv = GetRequiredScopeStringPrefix(maxScopeURI, maxPrefix, eUsePath); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(NS_ERROR_DOM_SECURITY_ERR); return; } } if (!StringBeginsWith(mRegistration->mScope, maxPrefix)) { NS_WARNING("By default a service worker's scope is restricted to at or below it's script's location."); Fail(NS_ERROR_DOM_SECURITY_ERR); return; } nsAutoCString scopeKey; rv = swm->PrincipalToScopeKey(mRegistration->mPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return Fail(NS_ERROR_FAILURE); } ServiceWorkerManager::RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { return Fail(NS_ERROR_FAILURE); } MOZ_ASSERT(!mUpdateAndInstallInfo); mUpdateAndInstallInfo = new ServiceWorkerInfo(mRegistration, mScriptSpec, aNewCacheName); RefPtr upcasted = this; nsMainThreadPtrHandle handle( new nsMainThreadPtrHolder(upcasted)); RefPtr callback = new ContinueUpdateRunnable(handle); ServiceWorkerPrivate* workerPrivate = mUpdateAndInstallInfo->WorkerPrivate(); rv = workerPrivate->CheckScriptEvaluation(callback); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(NS_ERROR_DOM_ABORT_ERR); } } private: // This will perform steps 27 and 28 from [[Update]] // Remove the job from the registration queue and invoke [[Install]] void ContinueInstall(bool aScriptEvaluationResult) { AssertIsOnMainThread(); MOZ_ASSERT(mRegistration); mRegistration->mUpdating = false; RefPtr kungFuDeathGrip = this; if (mCanceled) { return Fail(NS_ERROR_DOM_ABORT_ERR); } if (NS_WARN_IF(!aScriptEvaluationResult)) { ErrorResult error; NS_ConvertUTF8toUTF16 scriptSpec(mScriptSpec); NS_ConvertUTF8toUTF16 scope(mRegistration->mScope); error.ThrowTypeError(scriptSpec, scope); return FailWithErrorResult(error); } RefPtr job = new ServiceWorkerInstallJob(mQueue, mCallback, mRegistration, mUpdateAndInstallInfo, mScriptSpec); mQueue->Append(job); Done(NS_OK); } void Update() { AssertIsOnMainThread(); // Since Update() is called synchronously from Start(), we can assert this. MOZ_ASSERT(!mCanceled); MOZ_ASSERT(mRegistration); nsCOMPtr r = NS_NewRunnableMethod(this, &ServiceWorkerRegisterJob::ContinueUpdate); NS_DispatchToMainThread(r); mRegistration->mUpdating = true; } // Aspects of (actually the whole algorithm) of [[Update]] after // "Run the following steps in parallel." void ContinueUpdate() { AssertIsOnMainThread(); RefPtr kungFuDeathGrip = this; if (mCanceled) { return Fail(NS_ERROR_DOM_ABORT_ERR); } if (mRegistration->mInstallingWorker) { mRegistration->mInstallingWorker->UpdateState(ServiceWorkerState::Redundant); mRegistration->mInstallingWorker->WorkerPrivate()->TerminateWorker(); mRegistration->mInstallingWorker = nullptr; } RefPtr workerInfo = mRegistration->Newest(); nsAutoString cacheName; // 9.2.20 If newestWorker is not null, and newestWorker's script url is // equal to registration's registering script url and response is a // byte-for-byte match with the script resource of newestWorker... if (workerInfo && workerInfo->ScriptSpec().Equals(mScriptSpec)) { cacheName = workerInfo->CacheName(); } nsresult rv = serviceWorkerScriptCache::Compare(mRegistration, mRegistration->mPrincipal, cacheName, NS_ConvertUTF8toUTF16(mScriptSpec), this, mLoadGroup); if (NS_WARN_IF(NS_FAILED(rv))) { return Fail(rv); } } void Done(nsresult aStatus) { AssertIsOnMainThread(); if (mRegistration) { mRegistration->mUpdating = false; } ServiceWorkerJob::Done(aStatus); } }; NS_IMPL_ISUPPORTS_INHERITED0(ServiceWorkerRegisterJob, ServiceWorkerJob); void ServiceWorkerJobQueue::CancelJobs() { // The order doesn't matter. Cancel() just sets a flag on these jobs. CancelJobs(mRegistrationJobQueue); CancelJobs(mInstallationJobQueue); } void ServiceWorkerJobQueue::CancelJobs(QueueData& aQueue) { if (aQueue.mJobs.IsEmpty()) { return; } // We have to treat the first job specially. It is the running job and needs // to be notified correctly. Even if the job continues some work in the // background, this still needs to be done to let the job know its no longer // in the queue. aQueue.mJobs[0]->Cancel(); // Get rid of everything. Non-main thread objects may still be holding a ref // to the running register job. Since we called Cancel() on it, the job's // main thread functions will just exit. aQueue.mJobs.Clear(); } NS_IMETHODIMP ContinueUpdateRunnable::Run() { AssertIsOnMainThread(); RefPtr job = static_cast(mJob.get()); RefPtr upjob = static_cast(job.get()); upjob->ContinueInstall(mScriptEvaluationResult); return NS_OK; } void ContinueInstallTask::ContinueAfterWorkerEvent(bool aSuccess) { // This does not start the job immediately if there are other jobs in the // queue, which captures the "atomic" behaviour we want. mJob->ContinueAfterInstallEvent(aSuccess); } // This function implements parts of the step 3 of the following algorithm: // https://w3c.github.io/webappsec/specs/powerfulfeatures/#settings-secure static bool IsFromAuthenticatedOrigin(nsIDocument* aDoc) { MOZ_ASSERT(aDoc); nsCOMPtr doc(aDoc); nsCOMPtr csm = do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID); if (NS_WARN_IF(!csm)) { return false; } while (doc && !nsContentUtils::IsChromeDoc(doc)) { bool trustworthyURI = false; // The origin of the document may be different from the document URI // itself. Check the principal, not the document URI itself. nsCOMPtr documentPrincipal = doc->NodePrincipal(); // The check for IsChromeDoc() above should mean we never see a system // principal inside the loop. MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(documentPrincipal)); // Pass the principal as a URI to the security manager nsCOMPtr uri; documentPrincipal->GetURI(getter_AddRefs(uri)); if (NS_WARN_IF(!uri)) { return false; } csm->IsURIPotentiallyTrustworthy(uri, &trustworthyURI); if (!trustworthyURI) { return false; } doc = doc->GetParentDocument(); } return true; } // If we return an error code here, the ServiceWorkerContainer will // automatically reject the Promise. NS_IMETHODIMP ServiceWorkerManager::Register(nsIDOMWindow* aWindow, nsIURI* aScopeURI, nsIURI* aScriptURI, nsISupports** aPromise) { AssertIsOnMainThread(); nsCOMPtr window = do_QueryInterface(aWindow); if (NS_WARN_IF(!window)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } nsCOMPtr doc = window->GetExtantDoc(); if (!doc) { return NS_ERROR_FAILURE; } // Don't allow service workers to register when the *document* is chrome. if (NS_WARN_IF(nsContentUtils::IsSystemPrincipal(doc->NodePrincipal()))) { return NS_ERROR_DOM_SECURITY_ERR; } nsCOMPtr outerWindow = window->GetOuterWindow(); bool serviceWorkersTestingEnabled = outerWindow->GetServiceWorkersTestingEnabled(); bool authenticatedOrigin; if (Preferences::GetBool("dom.serviceWorkers.testing.enabled") || serviceWorkersTestingEnabled) { authenticatedOrigin = true; } else { authenticatedOrigin = IsFromAuthenticatedOrigin(doc); } if (!authenticatedOrigin) { NS_WARNING("ServiceWorker registration from insecure websites is not allowed."); return NS_ERROR_DOM_SECURITY_ERR; } // Data URLs are not allowed. nsCOMPtr documentPrincipal = doc->NodePrincipal(); nsresult rv = documentPrincipal->CheckMayLoad(aScriptURI, true /* report */, false /* allowIfInheritsPrincipal */); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } // Check content policy. int16_t decision = nsIContentPolicy::ACCEPT; rv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER, aScriptURI, documentPrincipal, doc, EmptyCString(), nullptr, &decision); NS_ENSURE_SUCCESS(rv, rv); if (NS_WARN_IF(decision != nsIContentPolicy::ACCEPT)) { return NS_ERROR_CONTENT_BLOCKED; } rv = documentPrincipal->CheckMayLoad(aScopeURI, true /* report */, false /* allowIfInheritsPrinciple */); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } // The IsURIPotentiallyTrustworthy() check allows file:// and possibly other // URI schemes. We need to explicitly only allows http and https schemes. // Note, we just use the aScriptURI here for the check since its already // been verified as same origin with the document principal. This also // is a good block against accidentally allowing blob: script URIs which // might inherit the origin. bool isHttp = false; bool isHttps = false; aScriptURI->SchemeIs("http", &isHttp); aScriptURI->SchemeIs("https", &isHttps); if (NS_WARN_IF(!isHttp && !isHttps)) { #ifdef RELEASE_BUILD return NS_ERROR_DOM_SECURITY_ERR; #else bool isApp = false; aScriptURI->SchemeIs("app", &isApp); if (NS_WARN_IF(!isApp)) { return NS_ERROR_DOM_SECURITY_ERR; } #endif } nsCString cleanedScope; rv = aScopeURI->GetSpecIgnoringRef(cleanedScope); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } nsAutoCString spec; rv = aScriptURI->GetSpecIgnoringRef(spec); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr sgo = do_QueryInterface(window); ErrorResult result; RefPtr promise = Promise::Create(sgo, result); if (result.Failed()) { return result.StealNSResult(); } nsAutoCString originSuffix; rv = PrincipalToScopeKey(documentPrincipal, originSuffix); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } AddRegisteringDocument(cleanedScope, doc); ServiceWorkerJobQueue* queue = GetOrCreateJobQueue(originSuffix, cleanedScope); MOZ_ASSERT(queue); RefPtr cb = new ServiceWorkerResolveWindowPromiseOnUpdateCallback(window, promise); nsCOMPtr docLoadGroup = doc->GetDocumentLoadGroup(); RefPtr ir = new WorkerLoadInfo::InterfaceRequestor(documentPrincipal, docLoadGroup); ir->MaybeAddTabChild(docLoadGroup); // Create a load group that is separate from, yet related to, the document's load group. // This allows checks for interfaces like nsILoadContext to yield the values used by the // the document, yet will not cancel the update job if the document's load group is cancelled. nsCOMPtr loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); rv = loadGroup->SetNotificationCallbacks(ir); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(rv)); RefPtr job = new ServiceWorkerRegisterJob(queue, cleanedScope, spec, cb, documentPrincipal, loadGroup); queue->Append(job); AssertIsOnMainThread(); Telemetry::Accumulate(Telemetry::SERVICE_WORKER_REGISTRATIONS, 1); promise.forget(aPromise); return NS_OK; } void ServiceWorkerManager::AppendPendingOperation(ServiceWorkerJobQueue* aQueue, ServiceWorkerJob* aJob) { MOZ_ASSERT(!mActor); MOZ_ASSERT(aQueue); MOZ_ASSERT(aJob); if (!mShuttingDown) { PendingOperation* opt = mPendingOperations.AppendElement(); opt->mQueue = aQueue; opt->mJob = aJob; } } void ServiceWorkerManager::AppendPendingOperation(nsIRunnable* aRunnable) { MOZ_ASSERT(!mActor); MOZ_ASSERT(aRunnable); if (!mShuttingDown) { PendingOperation* opt = mPendingOperations.AppendElement(); opt->mRunnable = aRunnable; } } void ServiceWorkerRegistrationInfo::TryToActivate() { if (!IsControllingDocuments() || mWaitingWorker->SkipWaitingFlag()) { Activate(); } } void ContinueActivateTask::ContinueAfterWorkerEvent(bool aSuccess) { mRegistration->FinishActivate(aSuccess); } void ServiceWorkerRegistrationInfo::PurgeActiveWorker() { RefPtr exitingWorker = mActiveWorker.forget(); if (!exitingWorker) return; // FIXME(jaoo): Bug 1170543 - Wait for exitingWorker to finish and terminate it. exitingWorker->UpdateState(ServiceWorkerState::Redundant); nsresult rv = serviceWorkerScriptCache::PurgeCache(mPrincipal, exitingWorker->CacheName()); if (NS_FAILED(rv)) { NS_WARNING("Failed to purge the activating cache."); } RefPtr swm = ServiceWorkerManager::GetInstance(); swm->InvalidateServiceWorkerRegistrationWorker(this, WhichServiceWorker::ACTIVE_WORKER); } void ServiceWorkerRegistrationInfo::Activate() { RefPtr activatingWorker = mWaitingWorker; if (!activatingWorker) { return; } PurgeActiveWorker(); RefPtr swm = ServiceWorkerManager::GetInstance(); swm->InvalidateServiceWorkerRegistrationWorker(this, WhichServiceWorker::WAITING_WORKER); mActiveWorker = activatingWorker.forget(); mWaitingWorker = nullptr; mActiveWorker->UpdateState(ServiceWorkerState::Activating); NotifyListenersOnChange(); // FIXME(nsm): Unlink appcache if there is one. swm->CheckPendingReadyPromises(); // "Queue a task to fire a simple event named controllerchange..." nsCOMPtr controllerChangeRunnable = NS_NewRunnableMethodWithArg>( swm, &ServiceWorkerManager::FireControllerChange, this); NS_DispatchToMainThread(controllerChangeRunnable); nsCOMPtr failRunnable = NS_NewRunnableMethodWithArg(this, &ServiceWorkerRegistrationInfo::FinishActivate, false /* success */); nsMainThreadPtrHandle continueActivateTask( new nsMainThreadPtrHolder(new ContinueActivateTask(this))); RefPtr callback = new ContinueLifecycleRunnable(continueActivateTask); ServiceWorkerPrivate* workerPrivate = mActiveWorker->WorkerPrivate(); nsresult rv = workerPrivate->SendLifeCycleEvent(NS_LITERAL_STRING("activate"), callback, failRunnable); if (NS_WARN_IF(NS_FAILED(rv))) { MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(failRunnable))); return; } } /* * Implements the async aspects of the getRegistrations algorithm. */ class GetRegistrationsRunnable final : public nsRunnable { nsCOMPtr mWindow; RefPtr mPromise; public: GetRegistrationsRunnable(nsPIDOMWindow* aWindow, Promise* aPromise) : mWindow(aWindow), mPromise(aPromise) {} NS_IMETHODIMP Run() { RefPtr swm = ServiceWorkerManager::GetInstance(); nsIDocument* doc = mWindow->GetExtantDoc(); if (!doc) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr docURI = doc->GetDocumentURI(); if (!docURI) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr principal = doc->NodePrincipal(); if (!principal) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsTArray> array; if (NS_WARN_IF(!BasePrincipal::Cast(principal)->IsCodebasePrincipal())) { return NS_OK; } nsAutoCString scopeKey; nsresult rv = swm->PrincipalToScopeKey(principal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } ServiceWorkerManager::RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { mPromise->MaybeResolve(array); return NS_OK; } for (uint32_t i = 0; i < data->mOrderedScopes.Length(); ++i) { NS_ConvertUTF8toUTF16 scope(data->mOrderedScopes[i]); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { mPromise->MaybeReject(rv); break; } rv = principal->CheckMayLoad(scopeURI, true /* report */, false /* allowIfInheritsPrincipal */); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } RefPtr swr = mWindow->GetServiceWorkerRegistration(scope); array.AppendElement(swr); } mPromise->MaybeResolve(array); return NS_OK; } }; // If we return an error code here, the ServiceWorkerContainer will // automatically reject the Promise. NS_IMETHODIMP ServiceWorkerManager::GetRegistrations(nsIDOMWindow* aWindow, nsISupports** aPromise) { AssertIsOnMainThread(); nsCOMPtr window = do_QueryInterface(aWindow); if (NS_WARN_IF(!window)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } nsCOMPtr doc = window->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } // Don't allow service workers to register when the *document* is chrome for // now. MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); nsCOMPtr sgo = do_QueryInterface(window); ErrorResult result; RefPtr promise = Promise::Create(sgo, result); if (result.Failed()) { return result.StealNSResult(); } nsCOMPtr runnable = new GetRegistrationsRunnable(window, promise); promise.forget(aPromise); return NS_DispatchToCurrentThread(runnable); } /* * Implements the async aspects of the getRegistration algorithm. */ class GetRegistrationRunnable final : public nsRunnable { nsCOMPtr mWindow; RefPtr mPromise; nsString mDocumentURL; public: GetRegistrationRunnable(nsPIDOMWindow* aWindow, Promise* aPromise, const nsAString& aDocumentURL) : mWindow(aWindow), mPromise(aPromise), mDocumentURL(aDocumentURL) {} NS_IMETHODIMP Run() { RefPtr swm = ServiceWorkerManager::GetInstance(); nsIDocument* doc = mWindow->GetExtantDoc(); if (!doc) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr docURI = doc->GetDocumentURI(); if (!docURI) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), mDocumentURL, nullptr, docURI); if (NS_WARN_IF(NS_FAILED(rv))) { mPromise->MaybeReject(rv); return NS_OK; } nsCOMPtr principal = doc->NodePrincipal(); if (!principal) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } rv = principal->CheckMayLoad(uri, true /* report */, false /* allowIfInheritsPrinciple */); if (NS_FAILED(rv)) { mPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); return NS_OK; } RefPtr registration = swm->GetServiceWorkerRegistrationInfo(principal, uri); if (!registration) { mPromise->MaybeResolve(JS::UndefinedHandleValue); return NS_OK; } NS_ConvertUTF8toUTF16 scope(registration->mScope); RefPtr swr = mWindow->GetServiceWorkerRegistration(scope); mPromise->MaybeResolve(swr); return NS_OK; } }; // If we return an error code here, the ServiceWorkerContainer will // automatically reject the Promise. NS_IMETHODIMP ServiceWorkerManager::GetRegistration(nsIDOMWindow* aWindow, const nsAString& aDocumentURL, nsISupports** aPromise) { AssertIsOnMainThread(); nsCOMPtr window = do_QueryInterface(aWindow); if (NS_WARN_IF(!window)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } nsCOMPtr doc = window->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } // Don't allow service workers to register when the *document* is chrome for // now. MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); nsCOMPtr sgo = do_QueryInterface(window); ErrorResult result; RefPtr promise = Promise::Create(sgo, result); if (result.Failed()) { return result.StealNSResult(); } nsCOMPtr runnable = new GetRegistrationRunnable(window, promise, aDocumentURL); promise.forget(aPromise); return NS_DispatchToCurrentThread(runnable); } class GetReadyPromiseRunnable final : public nsRunnable { nsCOMPtr mWindow; RefPtr mPromise; public: GetReadyPromiseRunnable(nsPIDOMWindow* aWindow, Promise* aPromise) : mWindow(aWindow), mPromise(aPromise) {} NS_IMETHODIMP Run() { RefPtr swm = ServiceWorkerManager::GetInstance(); nsIDocument* doc = mWindow->GetExtantDoc(); if (!doc) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } nsCOMPtr docURI = doc->GetDocumentURI(); if (!docURI) { mPromise->MaybeReject(NS_ERROR_UNEXPECTED); return NS_OK; } if (!swm->CheckReadyPromise(mWindow, docURI, mPromise)) { swm->StorePendingReadyPromise(mWindow, docURI, mPromise); } return NS_OK; } }; NS_IMETHODIMP ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes, const nsACString& aScope, uint32_t aDataLength, uint8_t* aDataBytes, uint8_t optional_argc) { #ifdef MOZ_SIMPLEPUSH return NS_ERROR_NOT_AVAILABLE; #else PrincipalOriginAttributes attrs; if (!attrs.PopulateFromSuffix(aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } ServiceWorkerInfo* serviceWorker = GetActiveWorkerInfoForScope(attrs, aScope); if (NS_WARN_IF(!serviceWorker)) { return NS_ERROR_FAILURE; } RefPtr registration = GetRegistration(serviceWorker->GetPrincipal(), aScope); if (optional_argc == 2) { nsTArray data; if (!data.InsertElementsAt(0, aDataBytes, aDataLength, fallible)) { return NS_ERROR_OUT_OF_MEMORY; } return serviceWorker->WorkerPrivate()->SendPushEvent(Some(data), registration); } else { MOZ_ASSERT(optional_argc == 0); return serviceWorker->WorkerPrivate()->SendPushEvent(Nothing(), registration); } #endif // MOZ_SIMPLEPUSH } NS_IMETHODIMP ServiceWorkerManager::SendPushSubscriptionChangeEvent(const nsACString& aOriginAttributes, const nsACString& aScope) { #ifdef MOZ_SIMPLEPUSH return NS_ERROR_NOT_AVAILABLE; #else PrincipalOriginAttributes attrs; if (!attrs.PopulateFromSuffix(aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); if (!info) { return NS_ERROR_FAILURE; } return info->WorkerPrivate()->SendPushSubscriptionChangeEvent(); #endif } 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) { PrincipalOriginAttributes attrs; if (!attrs.PopulateFromSuffix(aOriginSuffix)) { return NS_ERROR_INVALID_ARG; } ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); if (!info) { return NS_ERROR_FAILURE; } ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate(); return workerPrivate->SendNotificationClickEvent(aID, aTitle, aDir, aLang, aBody, aTag, aIcon, aData, aBehavior, NS_ConvertUTF8toUTF16(aScope)); } NS_IMETHODIMP ServiceWorkerManager::GetReadyPromise(nsIDOMWindow* aWindow, nsISupports** aPromise) { AssertIsOnMainThread(); nsCOMPtr window = do_QueryInterface(aWindow); if (NS_WARN_IF(!window)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } nsCOMPtr doc = window->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return NS_ERROR_FAILURE; } // Don't allow service workers to register when the *document* is chrome for // now. MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); MOZ_ASSERT(!mPendingReadyPromises.Contains(window)); nsCOMPtr sgo = do_QueryInterface(window); ErrorResult result; RefPtr promise = Promise::Create(sgo, result); if (result.Failed()) { return result.StealNSResult(); } nsCOMPtr runnable = new GetReadyPromiseRunnable(window, promise); promise.forget(aPromise); return NS_DispatchToCurrentThread(runnable); } NS_IMETHODIMP ServiceWorkerManager::RemoveReadyPromise(nsIDOMWindow* aWindow) { AssertIsOnMainThread(); MOZ_ASSERT(aWindow); nsCOMPtr window = do_QueryInterface(aWindow); if (!window) { return NS_ERROR_FAILURE; } mPendingReadyPromises.Remove(aWindow); return NS_OK; } void ServiceWorkerManager::StorePendingReadyPromise(nsPIDOMWindow* aWindow, nsIURI* aURI, Promise* aPromise) { PendingReadyPromise* data; // We should not have 2 pending promises for the same window. MOZ_ASSERT(!mPendingReadyPromises.Get(aWindow, &data)); data = new PendingReadyPromise(aURI, aPromise); mPendingReadyPromises.Put(aWindow, data); } void ServiceWorkerManager::CheckPendingReadyPromises() { for (auto iter = mPendingReadyPromises.Iter(); !iter.Done(); iter.Next()) { nsCOMPtr window = do_QueryInterface(iter.Key()); MOZ_ASSERT(window); nsAutoPtr& pendingReadyPromise = iter.Data(); if (CheckReadyPromise(window, pendingReadyPromise->mURI, pendingReadyPromise->mPromise)) { iter.Remove(); } } } bool ServiceWorkerManager::CheckReadyPromise(nsPIDOMWindow* aWindow, nsIURI* aURI, Promise* aPromise) { MOZ_ASSERT(aWindow); MOZ_ASSERT(aURI); nsCOMPtr doc = aWindow->GetExtantDoc(); MOZ_ASSERT(doc); nsCOMPtr principal = doc->NodePrincipal(); MOZ_ASSERT(principal); RefPtr registration = GetServiceWorkerRegistrationInfo(principal, aURI); if (registration && registration->mActiveWorker) { NS_ConvertUTF8toUTF16 scope(registration->mScope); RefPtr swr = aWindow->GetServiceWorkerRegistration(scope); aPromise->MaybeResolve(swr); return true; } return false; } ServiceWorkerInfo* ServiceWorkerManager::GetActiveWorkerInfoForScope(const PrincipalOriginAttributes& aOriginAttributes, const nsACString& aScope) { AssertIsOnMainThread(); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); if (NS_FAILED(rv)) { return nullptr; } RefPtr registration = GetServiceWorkerRegistrationInfo(aOriginAttributes, scopeURI); if (!registration) { return nullptr; } return registration->mActiveWorker; } ServiceWorkerInfo* ServiceWorkerManager::GetActiveWorkerInfoForDocument(nsIDocument* aDocument) { AssertIsOnMainThread(); RefPtr registration; GetDocumentRegistration(aDocument, getter_AddRefs(registration)); if (!registration) { return nullptr; } return registration->mActiveWorker; } class ServiceWorkerUnregisterJob final : public ServiceWorkerJob { RefPtr mRegistration; const nsCString mScope; nsCOMPtr mCallback; nsCOMPtr mPrincipal; ~ServiceWorkerUnregisterJob() {} public: ServiceWorkerUnregisterJob(ServiceWorkerJobQueue* aQueue, const nsACString& aScope, nsIServiceWorkerUnregisterCallback* aCallback, nsIPrincipal* aPrincipal) : ServiceWorkerJob(aQueue, Type::UnregisterJob) , mScope(aScope) , mCallback(aCallback) , mPrincipal(aPrincipal) { AssertIsOnMainThread(); } void Start() override { AssertIsOnMainThread(); nsCOMPtr r = NS_NewRunnableMethod(this, &ServiceWorkerUnregisterJob::UnregisterAndDone); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(r))); } private: // You probably want UnregisterAndDone(). nsresult Unregister() { AssertIsOnMainThread(); if (mCanceled) { return mCallback ? mCallback->UnregisterSucceeded(false) : NS_OK; } PrincipalInfo principalInfo; if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(mPrincipal, &principalInfo)))) { return mCallback ? mCallback->UnregisterSucceeded(false) : NS_OK; } RefPtr swm = ServiceWorkerManager::GetInstance(); // Could it be that we are shutting down. if (swm->mActor) { swm->mActor->SendUnregister(principalInfo, NS_ConvertUTF8toUTF16(mScope)); } nsAutoCString scopeKey; nsresult rv = swm->PrincipalToScopeKey(mPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return mCallback ? mCallback->UnregisterSucceeded(false) : NS_OK; } // "Let registration be the result of running [[Get Registration]] // algorithm passing scope as the argument." ServiceWorkerManager::RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { // "If registration is null, then, resolve promise with false." return mCallback ? mCallback->UnregisterSucceeded(false) : NS_OK; } RefPtr registration; if (!data->mInfos.Get(mScope, getter_AddRefs(registration))) { // "If registration is null, then, resolve promise with false." return mCallback ? mCallback->UnregisterSucceeded(false) : NS_OK; } MOZ_ASSERT(registration); // "Set registration's uninstalling flag." registration->mPendingUninstall = true; // "Resolve promise with true" rv = mCallback ? mCallback->UnregisterSucceeded(true) : NS_OK; if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // "If no service worker client is using registration..." if (!registration->IsControllingDocuments()) { // "If registration's uninstalling flag is set.." if (!registration->mPendingUninstall) { return NS_OK; } // "Invoke [[Clear Registration]]..." registration->Clear(); swm->RemoveRegistration(registration); } return NS_OK; } // The unregister job is done irrespective of success or failure of any sort. void UnregisterAndDone() { nsresult rv = Unregister(); Unused << NS_WARN_IF(NS_FAILED(rv)); Done(rv); } }; NS_IMETHODIMP ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback, const nsAString& aScope) { AssertIsOnMainThread(); if (!aPrincipal) { return NS_ERROR_FAILURE; } nsresult rv; // This is not accessible by content, and callers should always ensure scope is // a correct URI, so this is wrapped in DEBUG #ifdef DEBUG nsCOMPtr scopeURI; rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } #endif nsAutoCString originSuffix; rv = PrincipalToScopeKey(aPrincipal, originSuffix); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } NS_ConvertUTF16toUTF8 scope(aScope); ServiceWorkerJobQueue* queue = GetOrCreateJobQueue(originSuffix, scope); MOZ_ASSERT(queue); RefPtr job = new ServiceWorkerUnregisterJob(queue, scope, aCallback, aPrincipal); if (mActor) { queue->Append(job); return NS_OK; } AppendPendingOperation(queue, job); return NS_OK; } ServiceWorkerJobQueue* ServiceWorkerManager::GetOrCreateJobQueue(const nsACString& aKey, const nsACString& aScope) { ServiceWorkerManager::RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(aKey, &data)) { data = new RegistrationDataPerPrincipal(); mRegistrationInfos.Put(aKey, data); } ServiceWorkerJobQueue* queue; if (!data->mJobQueues.Get(aScope, &queue)) { queue = new ServiceWorkerJobQueue(aKey); data->mJobQueues.Put(aScope, queue); } return queue; } /* static */ already_AddRefed ServiceWorkerManager::GetInstance() { // Note: We don't simply check gInstance for null-ness here, since otherwise // this can resurrect the ServiceWorkerManager pretty late during shutdown. static bool firstTime = true; if (firstTime) { firstTime = false; AssertIsOnMainThread(); gInstance = new ServiceWorkerManager(); gInstance->Init(); ClearOnShutdown(&gInstance); } RefPtr copy = gInstance.get(); return copy.forget(); } void ServiceWorkerManager::FinishFetch(ServiceWorkerRegistrationInfo* aRegistration) { } void ServiceWorkerManager::ReportToAllClients(const nsCString& aScope, const nsString& aMessage, const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, uint32_t aColumnNumber, uint32_t aFlags) { nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aFilename); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsAutoTArray windows; // Report errors to every controlled document. for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { ServiceWorkerRegistrationInfo* reg = iter.UserData(); MOZ_ASSERT(reg); if (!reg->mScope.Equals(aScope)) { continue; } nsCOMPtr doc = do_QueryInterface(iter.Key()); if (!doc || !doc->IsCurrentActiveDocument() || !doc->GetWindow()) { continue; } windows.AppendElement(doc->InnerWindowID()); nsContentUtils::ReportToConsoleNonLocalized(aMessage, aFlags, NS_LITERAL_CSTRING("Service Workers"), doc, uri, aLine, aLineNumber, aColumnNumber); } // Report to any documents that have called .register() for this scope. They // may not be controlled, but will still want to see error reports. WeakDocumentList* regList = mRegisteringDocuments.Get(aScope); if (regList) { for (int32_t i = regList->Length() - 1; i >= 0; --i) { nsCOMPtr doc = do_QueryReferent(regList->ElementAt(i)); if (!doc) { regList->RemoveElementAt(i); continue; } if (!doc->IsCurrentActiveDocument()) { continue; } uint64_t innerWindowId = doc->InnerWindowID(); if (windows.Contains(innerWindowId)) { continue; } windows.AppendElement(innerWindowId); nsContentUtils::ReportToConsoleNonLocalized(aMessage, aFlags, NS_LITERAL_CSTRING("Service Workers"), doc, uri, aLine, aLineNumber, aColumnNumber); } if (regList->IsEmpty()) { regList = nullptr; nsAutoPtr doomed; mRegisteringDocuments.RemoveAndForget(aScope, doomed); } } InterceptionList* intList = mNavigationInterceptions.Get(aScope); if (intList) { nsIConsoleService* consoleService = nullptr; for (uint32_t i = 0; i < intList->Length(); ++i) { nsCOMPtr channel = intList->ElementAt(i); nsCOMPtr inner; rv = channel->GetChannel(getter_AddRefs(inner)); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } uint64_t innerWindowId = nsContentUtils::GetInnerWindowID(inner); if (innerWindowId == 0 || windows.Contains(innerWindowId)) { continue; } windows.AppendElement(innerWindowId); // Unfortunately the nsContentUtils helpers don't provide a convenient // way to log to a window ID without a document. Use console service // directly. nsCOMPtr errorObject = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return; } rv = errorObject->InitWithWindowID(aMessage, aFilename, aLine, aLineNumber, aColumnNumber, aFlags, NS_LITERAL_CSTRING("Service Workers"), innerWindowId); if (NS_WARN_IF(NS_FAILED(rv))) { return; } if (!consoleService) { rv = CallGetService(NS_CONSOLESERVICE_CONTRACTID, &consoleService); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } consoleService->LogMessage(errorObject); } } // If there are no documents to report to, at least report something to the // browser console. if (windows.IsEmpty()) { nsContentUtils::ReportToConsoleNonLocalized(aMessage, aFlags, NS_LITERAL_CSTRING("Service Workers"), nullptr, // document uri, aLine, aLineNumber, aColumnNumber); return; } } void ServiceWorkerManager::HandleError(JSContext* aCx, nsIPrincipal* aPrincipal, const nsCString& aScope, const nsString& aWorkerURL, const nsString& aMessage, const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber, uint32_t aColumnNumber, uint32_t aFlags, JSExnType aExnType) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } ServiceWorkerManager::RegistrationDataPerPrincipal* data; if (NS_WARN_IF(!mRegistrationInfos.Get(scopeKey, &data))) { return; } // Always report any uncaught exceptions or errors to the console of // each client. ReportToAllClients(aScope, aMessage, aFilename, aLine, aLineNumber, aColumnNumber, aFlags); } void ServiceWorkerRegistrationInfo::FinishActivate(bool aSuccess) { if (mPendingUninstall || !mActiveWorker || mActiveWorker->State() != ServiceWorkerState::Activating) { return; } // Activation never fails, so aSuccess is ignored. mActiveWorker->UpdateState(ServiceWorkerState::Activated); RefPtr swm = ServiceWorkerManager::GetInstance(); swm->StoreRegistration(mPrincipal, this); } void ServiceWorkerRegistrationInfo::RefreshLastUpdateCheckTime() { AssertIsOnMainThread(); mLastUpdateCheckTime = PR_IntervalNow() / PR_MSEC_PER_SEC; } bool ServiceWorkerRegistrationInfo::IsLastUpdateCheckTimeOverOneDay() const { AssertIsOnMainThread(); // For testing. if (Preferences::GetBool("dom.serviceWorkers.testUpdateOverOneDay")) { return true; } const uint64_t kSecondsPerDay = 86400; const uint64_t now = PR_IntervalNow() / PR_MSEC_PER_SEC; if ((mLastUpdateCheckTime != 0) && (now - mLastUpdateCheckTime > kSecondsPerDay)) { return true; } return false; } void ServiceWorkerRegistrationInfo::NotifyListenersOnChange() { nsTArray> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnChange(); } } void ServiceWorkerRegistrationInfo::MaybeScheduleTimeCheckAndUpdate() { AssertIsOnMainThread(); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { // shutting down, do nothing return; } if (mUpdateState == NoUpdate) { mUpdateState = NeedTimeCheckAndUpdate; } swm->ScheduleUpdateTimer(mPrincipal, mScope); } void ServiceWorkerRegistrationInfo::MaybeScheduleUpdate() { AssertIsOnMainThread(); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { // shutting down, do nothing return; } mUpdateState = NeedUpdate; swm->ScheduleUpdateTimer(mPrincipal, mScope); } bool ServiceWorkerRegistrationInfo::CheckAndClearIfUpdateNeeded() { AssertIsOnMainThread(); bool result = mUpdateState == NeedUpdate || (mUpdateState == NeedTimeCheckAndUpdate && IsLastUpdateCheckTimeOverOneDay()); mUpdateState = NoUpdate; return result; } void ServiceWorkerManager::LoadRegistration( const ServiceWorkerRegistrationData& aRegistration) { AssertIsOnMainThread(); nsCOMPtr principal = PrincipalInfoToPrincipal(aRegistration.principal()); if (!principal) { return; } RefPtr registration = GetRegistration(principal, aRegistration.scope()); if (!registration) { registration = CreateNewRegistration(aRegistration.scope(), principal); } else { RefPtr newest = registration->Newest(); if (newest && newest->ScriptSpec() == aRegistration.scriptSpec() && !!registration->mActiveWorker == aRegistration.currentWorkerURL().IsEmpty()) { // No needs for updates. return; } } const nsCString& currentWorkerURL = aRegistration.currentWorkerURL(); if (!currentWorkerURL.IsEmpty()) { registration->mActiveWorker = new ServiceWorkerInfo(registration, currentWorkerURL, aRegistration.activeCacheName()); registration->mActiveWorker->SetActivateStateUncheckedWithoutEvent(ServiceWorkerState::Activated); } } void ServiceWorkerManager::LoadRegistrations( const nsTArray& aRegistrations) { AssertIsOnMainThread(); for (uint32_t i = 0, len = aRegistrations.Length(); i < len; ++i) { LoadRegistration(aRegistrations[i]); } } void ServiceWorkerManager::ActorFailed() { MOZ_CRASH("Failed to create a PBackgroundChild actor!"); } void ServiceWorkerManager::ActorCreated(mozilla::ipc::PBackgroundChild* aActor) { MOZ_ASSERT(aActor); MOZ_ASSERT(!mActor); PServiceWorkerManagerChild* actor = aActor->SendPServiceWorkerManagerConstructor(); mActor = static_cast(actor); // Flush the pending requests. for (uint32_t i = 0, len = mPendingOperations.Length(); i < len; ++i) { MOZ_ASSERT(mPendingOperations[i].mRunnable || (mPendingOperations[i].mJob && mPendingOperations[i].mQueue)); if (mPendingOperations[i].mRunnable) { nsresult rv = NS_DispatchToCurrentThread(mPendingOperations[i].mRunnable); if (NS_FAILED(rv)) { NS_WARNING("Failed to dispatch a runnable."); return; } } else { mPendingOperations[i].mQueue->Append(mPendingOperations[i].mJob); } } mPendingOperations.Clear(); } void ServiceWorkerManager::StoreRegistration( nsIPrincipal* aPrincipal, ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aRegistration); if (mShuttingDown) { return; } MOZ_ASSERT(mActor); 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 ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsPIDOMWindow* aWindow) { MOZ_ASSERT(aWindow); nsCOMPtr document = aWindow->GetExtantDoc(); return GetServiceWorkerRegistrationInfo(document); } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIDocument* aDoc) { MOZ_ASSERT(aDoc); nsCOMPtr documentURI = aDoc->GetDocumentURI(); nsCOMPtr principal = aDoc->NodePrincipal(); return GetServiceWorkerRegistrationInfo(principal, documentURI); } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal, nsIURI* aURI) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aURI); //XXXnsm Temporary fix until Bug 1171432 is fixed. if (NS_WARN_IF(BasePrincipal::Cast(aPrincipal)->AppId() == nsIScriptSecurityManager::UNKNOWN_APP_ID)) { return nullptr; } nsAutoCString originAttributesSuffix; nsresult rv = PrincipalToScopeKey(aPrincipal, originAttributesSuffix); if (NS_FAILED(rv)) { return nullptr; } return GetServiceWorkerRegistrationInfo(originAttributesSuffix, aURI); } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistrationInfo(const PrincipalOriginAttributes& aOriginAttributes, nsIURI* aURI) { MOZ_ASSERT(aURI); nsAutoCString originAttributesSuffix; aOriginAttributes.CreateSuffix(originAttributesSuffix); return GetServiceWorkerRegistrationInfo(originAttributesSuffix, aURI); } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistrationInfo(const nsACString& aScopeKey, nsIURI* aURI) { MOZ_ASSERT(aURI); nsAutoCString spec; nsresult rv = aURI->GetSpec(spec); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } nsAutoCString scope; RegistrationDataPerPrincipal* data; if (!FindScopeForPath(aScopeKey, spec, &data, scope)) { return nullptr; } MOZ_ASSERT(data); RefPtr registration; data->mInfos.Get(scope, getter_AddRefs(registration)); // ordered scopes and registrations better be in sync. MOZ_ASSERT(registration); #ifdef DEBUG nsAutoCString originSuffix; rv = registration->mPrincipal->GetOriginSuffix(originSuffix); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(originSuffix.Equals(aScopeKey)); #endif if (registration->mPendingUninstall) { return nullptr; } return registration.forget(); } /* static */ nsresult ServiceWorkerManager::PrincipalToScopeKey(nsIPrincipal* aPrincipal, nsACString& aKey) { MOZ_ASSERT(aPrincipal); if (!BasePrincipal::Cast(aPrincipal)->IsCodebasePrincipal()) { return NS_ERROR_FAILURE; } nsresult rv = aPrincipal->GetOriginSuffix(aKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } /* static */ void ServiceWorkerManager::AddScopeAndRegistration(const nsACString& aScope, ServiceWorkerRegistrationInfo* aInfo) { MOZ_ASSERT(aInfo); MOZ_ASSERT(aInfo->mPrincipal); RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); nsAutoCString scopeKey; nsresult rv = swm->PrincipalToScopeKey(aInfo->mPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { data = new RegistrationDataPerPrincipal(); swm->mRegistrationInfos.Put(scopeKey, data); } for (uint32_t i = 0; i < data->mOrderedScopes.Length(); ++i) { const nsCString& current = data->mOrderedScopes[i]; // Perfect match! if (aScope.Equals(current)) { data->mInfos.Put(aScope, aInfo); swm->NotifyListenersOnRegister(aInfo); return; } // Sort by length, with longest match first. // /foo/bar should be before /foo/ // Similarly /foo/b is between the two. if (StringBeginsWith(aScope, current)) { data->mOrderedScopes.InsertElementAt(i, aScope); data->mInfos.Put(aScope, aInfo); swm->NotifyListenersOnRegister(aInfo); return; } } data->mOrderedScopes.AppendElement(aScope); data->mInfos.Put(aScope, aInfo); swm->NotifyListenersOnRegister(aInfo); } /* static */ bool ServiceWorkerManager::FindScopeForPath(const nsACString& aScopeKey, const nsACString& aPath, RegistrationDataPerPrincipal** aData, nsACString& aMatch) { MOZ_ASSERT(aData); RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); if (!swm->mRegistrationInfos.Get(aScopeKey, aData)) { return false; } for (uint32_t i = 0; i < (*aData)->mOrderedScopes.Length(); ++i) { const nsCString& current = (*aData)->mOrderedScopes[i]; if (StringBeginsWith(aPath, current)) { aMatch = current; return true; } } return false; } /* static */ bool ServiceWorkerManager::HasScope(nsIPrincipal* aPrincipal, const nsACString& aScope) { RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { return false; } return data->mOrderedScopes.Contains(aScope); } /* static */ void ServiceWorkerManager::RemoveScopeAndRegistration(ServiceWorkerRegistrationInfo* aRegistration) { RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); nsAutoCString scopeKey; nsresult rv = swm->PrincipalToScopeKey(aRegistration->mPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RegistrationDataPerPrincipal* data; if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { return; } nsCOMPtr timer = data->mUpdateTimers.Get(aRegistration->mScope); if (timer) { timer->Cancel(); data->mUpdateTimers.Remove(aRegistration->mScope); } RefPtr info; data->mInfos.Get(aRegistration->mScope, getter_AddRefs(info)); data->mInfos.Remove(aRegistration->mScope); data->mOrderedScopes.RemoveElement(aRegistration->mScope); swm->NotifyListenersOnUnregister(info); swm->MaybeRemoveRegistrationInfo(scopeKey); swm->NotifyServiceWorkerRegistrationRemoved(aRegistration); } void ServiceWorkerManager::MaybeRemoveRegistrationInfo(const nsACString& aScopeKey) { RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(aScopeKey, &data)) { return; } if (data->mOrderedScopes.IsEmpty() && data->mJobQueues.Count() == 0) { mRegistrationInfos.Remove(aScopeKey); } } void ServiceWorkerManager::MaybeStartControlling(nsIDocument* aDoc, const nsAString& aDocumentId) { AssertIsOnMainThread(); // We keep a set of documents that service workers may choose to start // controlling using claim(). MOZ_ASSERT(!mAllDocuments.Contains(aDoc)); mAllDocuments.PutEntry(aDoc); RefPtr registration = GetServiceWorkerRegistrationInfo(aDoc); if (registration) { MOZ_ASSERT(!mControlledDocuments.Contains(aDoc)); StartControllingADocument(registration, aDoc, aDocumentId); } } void ServiceWorkerManager::MaybeStopControlling(nsIDocument* aDoc) { MOZ_ASSERT(aDoc); RefPtr registration; mControlledDocuments.Remove(aDoc, getter_AddRefs(registration)); // A document which was uncontrolled does not maintain that state itself, so // it will always call MaybeStopControlling() even if there isn't an // associated registration. So this check is required. if (registration) { StopControllingADocument(registration); } mAllDocuments.RemoveEntry(aDoc); } void ServiceWorkerManager::MaybeCheckNavigationUpdate(nsIDocument* aDoc) { AssertIsOnMainThread(); MOZ_ASSERT(aDoc); // We perform these success path navigation update steps when the // document tells us its more or less done loading. This avoids // slowing down page load and also lets pages consistently get // updatefound events when they fire. // // 9.8.20 If respondWithEntered is false, then: // 9.8.22 Else: (respondWith was entered and succeeded) // If request is a non-subresource request, then: Invoke Soft Update // algorithm. RefPtr registration; mControlledDocuments.Get(aDoc, getter_AddRefs(registration)); if (registration) { registration->MaybeScheduleUpdate(); } } void ServiceWorkerManager::StartControllingADocument(ServiceWorkerRegistrationInfo* aRegistration, nsIDocument* aDoc, const nsAString& aDocumentId) { MOZ_ASSERT(aRegistration); MOZ_ASSERT(aDoc); aRegistration->StartControllingADocument(); mControlledDocuments.Put(aDoc, aRegistration); if (!aDocumentId.IsEmpty()) { aDoc->SetId(aDocumentId); } Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS, 1); } void ServiceWorkerManager::StopControllingADocument(ServiceWorkerRegistrationInfo* aRegistration) { aRegistration->StopControllingADocument(); if (!aRegistration->IsControllingDocuments()) { if (aRegistration->mPendingUninstall) { aRegistration->Clear(); RemoveRegistration(aRegistration); } else { // If the registration has an active worker that is running // this might be a good time to stop it. if (aRegistration->mActiveWorker) { ServiceWorkerPrivate* serviceWorkerPrivate = aRegistration->mActiveWorker->WorkerPrivate(); serviceWorkerPrivate->NoteStoppedControllingDocuments(); } aRegistration->TryToActivate(); } } } NS_IMETHODIMP ServiceWorkerManager::GetScopeForUrl(nsIPrincipal* aPrincipal, const nsAString& aUrl, nsAString& aScope) { MOZ_ASSERT(aPrincipal); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } RefPtr r = GetServiceWorkerRegistrationInfo(aPrincipal, uri); if (!r) { return NS_ERROR_FAILURE; } aScope = NS_ConvertUTF8toUTF16(r->mScope); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::AddRegistrationEventListener(const nsAString& aScope, ServiceWorkerRegistrationListener* aListener) { AssertIsOnMainThread(); MOZ_ASSERT(aListener); #ifdef DEBUG // Ensure a registration is only listening for it's own scope. nsAutoString regScope; aListener->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); MOZ_ASSERT(aScope.Equals(regScope)); #endif MOZ_ASSERT(!mServiceWorkerRegistrationListeners.Contains(aListener)); mServiceWorkerRegistrationListeners.AppendElement(aListener); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::RemoveRegistrationEventListener(const nsAString& aScope, ServiceWorkerRegistrationListener* aListener) { AssertIsOnMainThread(); MOZ_ASSERT(aListener); #ifdef DEBUG // Ensure a registration is unregistering for it's own scope. nsAutoString regScope; aListener->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); MOZ_ASSERT(aScope.Equals(regScope)); #endif MOZ_ASSERT(mServiceWorkerRegistrationListeners.Contains(aListener)); mServiceWorkerRegistrationListeners.RemoveElement(aListener); return NS_OK; } void ServiceWorkerManager::FireUpdateFoundOnServiceWorkerRegistrations( ServiceWorkerRegistrationInfo* aRegistration) { AssertIsOnMainThread(); nsTObserverArray::ForwardIterator it(mServiceWorkerRegistrationListeners); while (it.HasMore()) { RefPtr target = it.GetNext(); nsAutoString regScope; target->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); NS_ConvertUTF16toUTF8 utf8Scope(regScope); if (utf8Scope.Equals(aRegistration->mScope)) { target->UpdateFound(); } } } /* * This is used for installing, waiting and active. */ NS_IMETHODIMP ServiceWorkerManager::GetServiceWorkerForScope(nsIDOMWindow* aWindow, const nsAString& aScope, WhichServiceWorker aWhichWorker, nsISupports** aServiceWorker) { AssertIsOnMainThread(); nsCOMPtr window = do_QueryInterface(aWindow); if (NS_WARN_IF(!window)) { return NS_ERROR_DOM_INVALID_STATE_ERR; } nsCOMPtr doc = window->GetExtantDoc(); MOZ_ASSERT(doc); /////////////////////////////////////////// // Security check nsAutoCString scope = NS_ConvertUTF16toUTF8(aScope); nsCOMPtr scopeURI; // We pass nullptr as the base URI since scopes obtained from // ServiceWorkerRegistrations MUST be fully qualified URIs. nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } nsCOMPtr documentPrincipal = doc->NodePrincipal(); rv = documentPrincipal->CheckMayLoad(scopeURI, true /* report */, false /* allowIfInheritsPrinciple */); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } //////////////////////////////////////////// RefPtr registration = GetRegistration(documentPrincipal, scope); if (NS_WARN_IF(!registration)) { return NS_ERROR_FAILURE; } RefPtr info; if (aWhichWorker == WhichServiceWorker::INSTALLING_WORKER) { info = registration->mInstallingWorker; } else if (aWhichWorker == WhichServiceWorker::WAITING_WORKER) { info = registration->mWaitingWorker; } else if (aWhichWorker == WhichServiceWorker::ACTIVE_WORKER) { info = registration->mActiveWorker; } else { MOZ_CRASH("Invalid worker type"); } if (NS_WARN_IF(!info)) { return NS_ERROR_DOM_NOT_FOUND_ERR; } RefPtr serviceWorker = new ServiceWorker(window, info); serviceWorker->SetState(info->State()); serviceWorker.forget(aServiceWorker); return NS_OK; } namespace { class ContinueDispatchFetchEventRunnable : public nsRunnable { RefPtr mServiceWorkerPrivate; nsCOMPtr mChannel; nsCOMPtr mLoadGroup; nsString mDocumentId; bool mIsReload; public: ContinueDispatchFetchEventRunnable(ServiceWorkerPrivate* aServiceWorkerPrivate, nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup, const nsAString& aDocumentId, bool aIsReload) : mServiceWorkerPrivate(aServiceWorkerPrivate) , mChannel(aChannel) , mLoadGroup(aLoadGroup) , mDocumentId(aDocumentId) , mIsReload(aIsReload) { MOZ_ASSERT(aServiceWorkerPrivate); MOZ_ASSERT(aChannel); } void HandleError() { AssertIsOnMainThread(); NS_WARNING("Unexpected error while dispatching fetch event!"); DebugOnly rv = mChannel->ResetInterception(); NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to resume intercepted network request"); } NS_IMETHOD Run() override { AssertIsOnMainThread(); nsCOMPtr channel; nsresult rv = mChannel->GetChannel(getter_AddRefs(channel)); if (NS_WARN_IF(NS_FAILED(rv))) { HandleError(); return NS_OK; } // The channel might have encountered an unexpected error while ensuring // the upload stream is cloneable. Check here and reset the interception // if that happens. nsresult status; rv = channel->GetStatus(&status); if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(status))) { HandleError(); return NS_OK; } rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, mDocumentId, mIsReload); if (NS_WARN_IF(NS_FAILED(rv))) { HandleError(); } return NS_OK; } }; } // anonymous namespace already_AddRefed ServiceWorkerManager::PrepareFetchEvent(const PrincipalOriginAttributes& aOriginAttributes, nsIDocument* aDoc, const nsAString& aDocumentIdForTopLevelNavigation, nsIInterceptedChannel* aChannel, bool aIsReload, bool aIsSubresourceLoad, ErrorResult& aRv) { MOZ_ASSERT(aChannel); AssertIsOnMainThread(); RefPtr serviceWorker; nsCOMPtr loadGroup; nsAutoString documentId; if (aIsSubresourceLoad) { MOZ_ASSERT(aDoc); serviceWorker = GetActiveWorkerInfoForDocument(aDoc); loadGroup = aDoc->GetDocumentLoadGroup(); nsresult rv = aDoc->GetOrCreateId(documentId); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } } else { nsCOMPtr internalChannel; aRv = aChannel->GetChannel(getter_AddRefs(internalChannel)); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } internalChannel->GetLoadGroup(getter_AddRefs(loadGroup)); documentId = aDocumentIdForTopLevelNavigation; nsCOMPtr uri; aRv = internalChannel->GetURI(getter_AddRefs(uri)); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr registration = GetServiceWorkerRegistrationInfo(aOriginAttributes, uri); if (!registration) { NS_WARNING("No registration found when dispatching the fetch event"); aRv.Throw(NS_ERROR_FAILURE); return nullptr; } // This should only happen if IsAvailable() returned true. MOZ_ASSERT(registration->mActiveWorker); serviceWorker = registration->mActiveWorker; AddNavigationInterception(serviceWorker->Scope(), aChannel); } if (NS_WARN_IF(aRv.Failed()) || !serviceWorker) { return nullptr; } nsCOMPtr continueRunnable = new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(), aChannel, loadGroup, documentId, aIsReload); return continueRunnable.forget(); } void ServiceWorkerManager::DispatchPreparedFetchEvent(nsIInterceptedChannel* aChannel, nsIRunnable* aPreparedRunnable, ErrorResult& aRv) { MOZ_ASSERT(aChannel); MOZ_ASSERT(aPreparedRunnable); AssertIsOnMainThread(); nsCOMPtr innerChannel; aRv = aChannel->GetChannel(getter_AddRefs(innerChannel)); if (NS_WARN_IF(aRv.Failed())) { return; } nsCOMPtr uploadChannel = do_QueryInterface(innerChannel); // If there is no upload stream, then continue immediately if (!uploadChannel) { MOZ_ALWAYS_TRUE(NS_SUCCEEDED(aPreparedRunnable->Run())); return; } // Otherwise, ensure the upload stream can be cloned directly. This may // require some async copying, so provide a callback. aRv = uploadChannel->EnsureUploadStreamIsCloneable(aPreparedRunnable); } bool ServiceWorkerManager::IsAvailable(const PrincipalOriginAttributes& aOriginAttributes, nsIURI* aURI) { MOZ_ASSERT(aURI); RefPtr registration = GetServiceWorkerRegistrationInfo(aOriginAttributes, aURI); return registration && registration->mActiveWorker; } bool ServiceWorkerManager::IsControlled(nsIDocument* aDoc, ErrorResult& aRv) { MOZ_ASSERT(aDoc); RefPtr registration; nsresult rv = GetDocumentRegistration(aDoc, getter_AddRefs(registration)); if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE)) { // It's OK to ignore the case where we don't have a registration. aRv.Throw(rv); return false; } MOZ_ASSERT_IF(!!registration, !nsContentUtils::IsInPrivateBrowsing(aDoc)); return !!registration; } nsresult ServiceWorkerManager::GetDocumentRegistration(nsIDocument* aDoc, ServiceWorkerRegistrationInfo** aRegistrationInfo) { RefPtr registration; if (!mControlledDocuments.Get(aDoc, getter_AddRefs(registration))) { return NS_ERROR_NOT_AVAILABLE; } // If the document is controlled, the current worker MUST be non-null. if (!registration->mActiveWorker) { return NS_ERROR_NOT_AVAILABLE; } registration.forget(aRegistrationInfo); return NS_OK; } /* * The .controller is for the registration associated with the document when * the document was loaded. */ NS_IMETHODIMP ServiceWorkerManager::GetDocumentController(nsIDOMWindow* aWindow, nsISupports** aServiceWorker) { nsCOMPtr window = do_QueryInterface(aWindow); MOZ_ASSERT(window); if (!window || !window->GetExtantDoc()) { return NS_ERROR_DOM_INVALID_STATE_ERR; } nsCOMPtr doc = window->GetExtantDoc(); RefPtr registration; nsresult rv = GetDocumentRegistration(doc, getter_AddRefs(registration)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(registration->mActiveWorker); RefPtr serviceWorker = new ServiceWorker(window, registration->mActiveWorker); serviceWorker.forget(aServiceWorker); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::GetInstalling(nsIDOMWindow* aWindow, const nsAString& aScope, nsISupports** aServiceWorker) { return GetServiceWorkerForScope(aWindow, aScope, WhichServiceWorker::INSTALLING_WORKER, aServiceWorker); } NS_IMETHODIMP ServiceWorkerManager::GetWaiting(nsIDOMWindow* aWindow, const nsAString& aScope, nsISupports** aServiceWorker) { return GetServiceWorkerForScope(aWindow, aScope, WhichServiceWorker::WAITING_WORKER, aServiceWorker); } NS_IMETHODIMP ServiceWorkerManager::GetActive(nsIDOMWindow* aWindow, const nsAString& aScope, nsISupports** aServiceWorker) { return GetServiceWorkerForScope(aWindow, aScope, WhichServiceWorker::ACTIVE_WORKER, aServiceWorker); } void ServiceWorkerManager::InvalidateServiceWorkerRegistrationWorker(ServiceWorkerRegistrationInfo* aRegistration, WhichServiceWorker aWhichOnes) { AssertIsOnMainThread(); nsTObserverArray::ForwardIterator it(mServiceWorkerRegistrationListeners); while (it.HasMore()) { RefPtr target = it.GetNext(); nsAutoString regScope; target->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); NS_ConvertUTF16toUTF8 utf8Scope(regScope); if (utf8Scope.Equals(aRegistration->mScope)) { target->InvalidateWorkers(aWhichOnes); } } } void ServiceWorkerManager::NotifyServiceWorkerRegistrationRemoved(ServiceWorkerRegistrationInfo* aRegistration) { AssertIsOnMainThread(); nsTObserverArray::ForwardIterator it(mServiceWorkerRegistrationListeners); while (it.HasMore()) { RefPtr target = it.GetNext(); nsAutoString regScope; target->GetScope(regScope); MOZ_ASSERT(!regScope.IsEmpty()); NS_ConvertUTF16toUTF8 utf8Scope(regScope); if (utf8Scope.Equals(aRegistration->mScope)) { target->RegistrationRemoved(); } } } void ServiceWorkerManager::SoftUpdate(const OriginAttributes& aOriginAttributes, const nsACString& aScope) { AssertIsOnMainThread(); nsAutoCString scopeKey; aOriginAttributes.CreateSuffix(scopeKey); RefPtr registration = GetRegistration(scopeKey, aScope); if (NS_WARN_IF(!registration)) { return; } // "If registration's uninstalling flag is set, abort these steps." if (registration->mPendingUninstall) { return; } // "If registration's installing worker is not null, abort these steps." if (registration->mInstallingWorker) { 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 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 if (!registration->mUpdating) { ServiceWorkerJobQueue* queue = GetOrCreateJobQueue(scopeKey, aScope); MOZ_ASSERT(queue); RefPtr job = new ServiceWorkerRegisterJob(queue, registration, nullptr, newest->ScriptSpec()); queue->Append(job); } } void ServiceWorkerManager::Update(nsIPrincipal* aPrincipal, const nsACString& aScope, ServiceWorkerUpdateFinishCallback* aCallback) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aCallback); nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RefPtr registration = GetRegistration(scopeKey, aScope); if (NS_WARN_IF(!registration)) { return; } // "If registration's uninstalling flag is set, abort these steps." if (registration->mPendingUninstall) { return; } // "Let newestWorker be the result of running Get Newest Worker algorithm // passing registration as its argument. // If newestWorker is null, return a promise rejected with "InvalidStateError" RefPtr newest = registration->Newest(); if (!newest) { ErrorResult error(NS_ERROR_DOM_INVALID_STATE_ERR); aCallback->UpdateFailed(error); // In case the callback does not consume the exception error.SuppressException(); return; } ServiceWorkerJobQueue* queue = GetOrCreateJobQueue(scopeKey, aScope); MOZ_ASSERT(queue); // "Invoke Update algorithm, or its equivalent, with client, registration as // its argument." RefPtr job = new ServiceWorkerRegisterJob(queue, registration, aCallback, newest->ScriptSpec()); queue->Append(job); } namespace { static void FireControllerChangeOnDocument(nsIDocument* aDocument) { AssertIsOnMainThread(); MOZ_ASSERT(aDocument); nsCOMPtr w = aDocument->GetWindow(); if (!w) { NS_WARNING("Failed to dispatch controllerchange event"); return; } w = w->GetCurrentInnerWindow(); if (!w) { NS_WARNING("Failed to dispatch controllerchange event"); return; } auto* window = static_cast(w.get()); ErrorResult result; dom::Navigator* navigator = window->GetNavigator(result); if (NS_WARN_IF(result.Failed())) { result.SuppressException(); return; } RefPtr container = navigator->ServiceWorker(); container->ControllerChanged(result); if (result.Failed()) { NS_WARNING("Failed to dispatch controllerchange event"); } } } // anonymous namespace UniquePtr ServiceWorkerManager::GetClient(nsIPrincipal* aPrincipal, const nsAString& aClientId, ErrorResult& aRv) { UniquePtr clientInfo; nsCOMPtr ifptr = do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID); if (NS_WARN_IF(!ifptr)) { return clientInfo; } nsCOMPtr obs = mozilla::services::GetObserverService(); if (NS_WARN_IF(!obs)) { return clientInfo; } nsresult rv = obs->NotifyObservers(ifptr, "service-worker-get-client", PromiseFlatString(aClientId).get()); if (NS_WARN_IF(NS_FAILED(rv))) { return clientInfo; } nsCOMPtr ptr; ifptr->GetData(getter_AddRefs(ptr)); nsCOMPtr doc = do_QueryInterface(ptr); if (NS_WARN_IF(!doc)) { return clientInfo; } bool equals = false; aPrincipal->Equals(doc->NodePrincipal(), &equals); if (!equals) { return clientInfo; } if (!IsFromAuthenticatedOrigin(doc)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return clientInfo; } clientInfo.reset(new ServiceWorkerClientInfo(doc)); return clientInfo; } void ServiceWorkerManager::GetAllClients(nsIPrincipal* aPrincipal, const nsCString& aScope, bool aIncludeUncontrolled, nsTArray& aDocuments) { MOZ_ASSERT(aPrincipal); RefPtr registration = GetRegistration(aPrincipal, aScope); if (!registration) { // The registration was removed, leave the array empty. return; } nsCOMPtr obs = mozilla::services::GetObserverService(); if (NS_WARN_IF(!obs)) { return; } nsCOMPtr enumerator; nsresult rv = obs->EnumerateObservers("service-worker-get-client", getter_AddRefs(enumerator)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } auto ProcessDocument = [&aDocuments](nsIPrincipal* aPrincipal, nsIDocument* aDoc) { if (!aDoc || !aDoc->GetWindow()) { return; } bool equals = false; aPrincipal->Equals(aDoc->NodePrincipal(), &equals); if (!equals) { return; } if (!Preferences::GetBool("dom.serviceWorkers.testing.enabled") && !IsFromAuthenticatedOrigin(aDoc)) { return; } ServiceWorkerClientInfo clientInfo(aDoc); aDocuments.AppendElement(aDoc); }; // Since it's not simple to check whether a document is in // mControlledDocuments, we take different code paths depending on whether we // need to look at all documents. The common parts of the two loops are // factored out into the ProcessDocument lambda. if (aIncludeUncontrolled) { bool loop = true; while (NS_SUCCEEDED(enumerator->HasMoreElements(&loop)) && loop) { nsCOMPtr ptr; rv = enumerator->GetNext(getter_AddRefs(ptr)); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } nsCOMPtr doc = do_QueryInterface(ptr); ProcessDocument(aPrincipal, doc); } } else { for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { ServiceWorkerRegistrationInfo* thisRegistration = iter.UserData(); MOZ_ASSERT(thisRegistration); if (!registration->mScope.Equals(thisRegistration->mScope)) { continue; } nsCOMPtr doc = do_QueryInterface(iter.Key()); ProcessDocument(aPrincipal, doc); } } } void ServiceWorkerManager::MaybeClaimClient(nsIDocument* aDocument, ServiceWorkerRegistrationInfo* aWorkerRegistration) { MOZ_ASSERT(aWorkerRegistration); MOZ_ASSERT(aWorkerRegistration->mActiveWorker); // Same origin check if (!aWorkerRegistration->mPrincipal->Equals(aDocument->NodePrincipal())) { return; } // The registration that should be controlling the client RefPtr matchingRegistration = GetServiceWorkerRegistrationInfo(aDocument); // The registration currently controlling the client RefPtr controllingRegistration; GetDocumentRegistration(aDocument, getter_AddRefs(controllingRegistration)); if (aWorkerRegistration != matchingRegistration || aWorkerRegistration == controllingRegistration) { return; } if (controllingRegistration) { StopControllingADocument(controllingRegistration); } StartControllingADocument(aWorkerRegistration, aDocument, NS_LITERAL_STRING("")); FireControllerChangeOnDocument(aDocument); } nsresult ServiceWorkerManager::ClaimClients(nsIPrincipal* aPrincipal, const nsCString& aScope, uint64_t aId) { RefPtr registration = GetRegistration(aPrincipal, aScope); if (!registration || !registration->mActiveWorker || !(registration->mActiveWorker->ID() == aId)) { // The worker is not active. return NS_ERROR_DOM_INVALID_STATE_ERR; } RefPtr swm = ServiceWorkerManager::GetInstance(); for (auto iter = mAllDocuments.Iter(); !iter.Done(); iter.Next()) { nsCOMPtr document = do_QueryInterface(iter.Get()->GetKey()); swm->MaybeClaimClient(document, registration); } return NS_OK; } nsresult ServiceWorkerManager::SetSkipWaitingFlag(nsIPrincipal* aPrincipal, const nsCString& aScope, uint64_t aServiceWorkerID) { RefPtr registration = GetRegistration(aPrincipal, aScope); if (NS_WARN_IF(!registration)) { return NS_ERROR_FAILURE; } if (registration->mInstallingWorker && (registration->mInstallingWorker->ID() == aServiceWorkerID)) { registration->mInstallingWorker->SetSkipWaitingFlag(); } else if (registration->mWaitingWorker && (registration->mWaitingWorker->ID() == aServiceWorkerID)) { registration->mWaitingWorker->SetSkipWaitingFlag(); if (registration->mWaitingWorker->State() == ServiceWorkerState::Installed) { registration->TryToActivate(); } } else { NS_WARNING("Failed to set skipWaiting flag, no matching worker."); return NS_ERROR_FAILURE; } return NS_OK; } void ServiceWorkerManager::FireControllerChange(ServiceWorkerRegistrationInfo* aRegistration) { AssertIsOnMainThread(); for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { if (iter.UserData() != aRegistration) { continue; } nsCOMPtr doc = do_QueryInterface(iter.Key()); if (NS_WARN_IF(!doc)) { continue; } FireControllerChangeOnDocument(doc); } } already_AddRefed 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 ServiceWorkerManager::GetRegistration(const nsACString& aScopeKey, const nsACString& aScope) const { RefPtr reg; RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(aScopeKey, &data)) { return reg.forget(); } data->mInfos.Get(aScope, getter_AddRefs(reg)); return reg.forget(); } ServiceWorkerRegistrationInfo* ServiceWorkerManager::CreateNewRegistration(const nsCString& aScope, nsIPrincipal* aPrincipal) { #ifdef DEBUG AssertIsOnMainThread(); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); MOZ_ASSERT(NS_SUCCEEDED(rv)); RefPtr tmp = GetRegistration(aPrincipal, aScope); MOZ_ASSERT(!tmp); #endif ServiceWorkerRegistrationInfo* registration = new ServiceWorkerRegistrationInfo(aScope, aPrincipal); // From now on ownership of registration is with // mServiceWorkerRegistrationInfos. AddScopeAndRegistration(aScope, registration); return registration; } void ServiceWorkerManager::MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(aRegistration); RefPtr newest = aRegistration->Newest(); if (!newest && HasScope(aRegistration->mPrincipal, aRegistration->mScope)) { aRegistration->Clear(); RemoveRegistration(aRegistration); } } void ServiceWorkerManager::RemoveRegistrationInternal(ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(aRegistration); MOZ_ASSERT(!aRegistration->IsControllingDocuments()); if (mShuttingDown) { return; } // All callers should be either from a job in which case the actor is // available, or from MaybeStopControlling(), in which case, this will only be // called if a valid registration is found. If a valid registration exists, // it means the actor is available since the original map of registrations is // populated by it, and any new registrations wait until the actor is // available before proceeding (See ServiceWorkerRegisterJob::Start). MOZ_ASSERT(mActor); PrincipalInfo principalInfo; if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(aRegistration->mPrincipal, &principalInfo)))) { //XXXnsm I can't think of any other reason a stored principal would fail to //convert. NS_WARNING("Unable to unregister serviceworker due to possible OOM"); return; } mActor->SendUnregister(principalInfo, NS_ConvertUTF8toUTF16(aRegistration->mScope)); } void ServiceWorkerManager::RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration) { RemoveRegistrationInternal(aRegistration); MOZ_ASSERT(HasScope(aRegistration->mPrincipal, aRegistration->mScope)); RemoveScopeAndRegistration(aRegistration); } namespace { /** * See browser/components/sessionstore/Utils.jsm function hasRootDomain(). * * Returns true if the |url| passed in is part of the given root |domain|. * For example, if |url| is "www.mozilla.org", and we pass in |domain| as * "mozilla.org", this will return true. It would return false the other way * around. */ bool HasRootDomain(nsIURI* aURI, const nsACString& aDomain) { AssertIsOnMainThread(); MOZ_ASSERT(aURI); nsAutoCString host; nsresult rv = aURI->GetHost(host); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } nsACString::const_iterator start, end; host.BeginReading(start); host.EndReading(end); if (!FindInReadable(aDomain, start, end)) { return false; } if (host.Equals(aDomain)) { return true; } // Beginning of the string matches, can't look at the previous char. if (start.get() == host.BeginReading()) { // Equals failed so this is fine. return false; } char prevChar = *(--start); return prevChar == '.'; } } // namespace NS_IMETHODIMP ServiceWorkerManager::GetAllRegistrations(nsIArray** aResult) { AssertIsOnMainThread(); nsCOMPtr array(do_CreateInstance(NS_ARRAY_CONTRACTID)); if (!array) { return NS_ERROR_OUT_OF_MEMORY; } for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { for (auto it2 = it1.UserData()->mInfos.Iter(); !it2.Done(); it2.Next()) { ServiceWorkerRegistrationInfo* reg = it2.UserData(); MOZ_ASSERT(reg); if (reg->mPendingUninstall) { continue; } array->AppendElement(reg, false); } } array.forget(aResult); return NS_OK; } // MUST ONLY BE CALLED FROM Remove(), RemoveAll() and RemoveAllRegistrations()! void ServiceWorkerManager::ForceUnregister(RegistrationDataPerPrincipal* aRegistrationData, ServiceWorkerRegistrationInfo* aRegistration) { MOZ_ASSERT(aRegistrationData); MOZ_ASSERT(aRegistration); ServiceWorkerJobQueue* queue; aRegistrationData->mJobQueues.Get(aRegistration->mScope, &queue); if (queue) { queue->CancelJobs(); } nsCOMPtr timer = aRegistrationData->mUpdateTimers.Get(aRegistration->mScope); if (timer) { timer->Cancel(); aRegistrationData->mUpdateTimers.Remove(aRegistration->mScope); } // Since Unregister is async, it is ok to call it in an enumeration. Unregister(aRegistration->mPrincipal, nullptr, NS_ConvertUTF8toUTF16(aRegistration->mScope)); } NS_IMETHODIMP ServiceWorkerManager::RemoveAndPropagate(const nsACString& aHost) { Remove(aHost); PropagateRemove(aHost); return NS_OK; } void ServiceWorkerManager::Remove(const nsACString& aHost) { AssertIsOnMainThread(); // We need to postpone this operation in case we don't have an actor because // this is needed by the ForceUnregister. if (!mActor) { RefPtr runnable = new RemoveRunnable(aHost); AppendPendingOperation(runnable); return; } RefPtr swm = ServiceWorkerManager::GetInstance(); for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { ServiceWorkerRegistrationInfo* reg = it2.UserData(); nsCOMPtr scopeURI; nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), it2.Key(), nullptr, nullptr); // This way subdomains are also cleared. if (NS_SUCCEEDED(rv) && HasRootDomain(scopeURI, aHost)) { swm->ForceUnregister(data, reg); } } } } void ServiceWorkerManager::PropagateRemove(const nsACString& aHost) { AssertIsOnMainThread(); if (!mActor) { RefPtr runnable = new PropagateRemoveRunnable(aHost); AppendPendingOperation(runnable); return; } mActor->SendPropagateRemove(nsCString(aHost)); } void ServiceWorkerManager::RemoveAll() { AssertIsOnMainThread(); RefPtr swm = ServiceWorkerManager::GetInstance(); for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { ServiceWorkerRegistrationInfo* reg = it2.UserData(); swm->ForceUnregister(data, reg); } } } void ServiceWorkerManager::PropagateRemoveAll() { AssertIsOnMainThread(); MOZ_ASSERT(XRE_IsParentProcess()); if (!mActor) { RefPtr runnable = new PropagateRemoveAllRunnable(); AppendPendingOperation(runnable); return; } mActor->SendPropagateRemoveAll(); } void ServiceWorkerManager::RemoveAllRegistrations(PrincipalOriginAttributes* aParams) { AssertIsOnMainThread(); MOZ_ASSERT(aParams); for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); // We can use iteration because ForceUnregister (and Unregister) are // async. Otherwise doing some R/W operations on an hashtable during // iteration will crash. for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { ServiceWorkerRegistrationInfo* reg = it2.UserData(); MOZ_ASSERT(reg); MOZ_ASSERT(reg->mPrincipal); bool equals = false; if (aParams->mInBrowser) { // When we do a system wide "clear cookies and stored data" on B2G we // get the "clear-origin-data" notification with the System app appID // and the browserOnly flag set to true. Web sites registering a // service worker on B2G have a principal with the following // information: web site origin + System app appId + inBrowser=1 So // we need to check if the service worker registration info contains // the System app appID and the enabled inBrowser flag and in that // case remove it from the registry. OriginAttributes attrs = mozilla::BasePrincipal::Cast(reg->mPrincipal)->OriginAttributesRef(); equals = attrs == *aParams; } else { // If we get the "clear-origin-data" notification because of an app // uninstallation, we need to check the full principal to get the // match in the service workers registry. If we find a match, we // unregister the worker. nsCOMPtr appsService = do_GetService(APPS_SERVICE_CONTRACTID); if (NS_WARN_IF(!appsService)) { continue; } nsCOMPtr app; appsService->GetAppByLocalId(aParams->mAppId, getter_AddRefs(app)); if (NS_WARN_IF(!app)) { continue; } nsCOMPtr principal; app->GetPrincipal(getter_AddRefs(principal)); if (NS_WARN_IF(!principal)) { continue; } reg->mPrincipal->Equals(principal, &equals); } if (equals) { RefPtr swm = ServiceWorkerManager::GetInstance(); swm->ForceUnregister(data, reg); } } } } NS_IMETHODIMP ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener) { AssertIsOnMainThread(); if (!aListener || mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.AppendElement(aListener); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::RemoveListener(nsIServiceWorkerManagerListener* aListener) { AssertIsOnMainThread(); if (!aListener || !mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.RemoveElement(aListener); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::ShouldReportToWindow(nsIDOMWindow* aWindow, const nsACString& aScope, bool* aResult) { AssertIsOnMainThread(); MOZ_ASSERT(aResult); *aResult = false; // Get the inner window ID to compare to our document windows below. nsCOMPtr targetWin = do_QueryInterface(aWindow); if (NS_WARN_IF(!targetWin)) { return NS_OK; } targetWin = targetWin->GetScriptableTop(); uint64_t winId = targetWin->WindowID(); // Check our weak registering document references first. This way we clear // out as many dead weak references as possible when this method is called. WeakDocumentList* list = mRegisteringDocuments.Get(aScope); if (list) { for (int32_t i = list->Length() - 1; i >= 0; --i) { nsCOMPtr doc = do_QueryReferent(list->ElementAt(i)); if (!doc) { list->RemoveElementAt(i); continue; } if (!doc->IsCurrentActiveDocument()) { continue; } nsCOMPtr win = doc->GetWindow(); if (!win) { continue; } win = win->GetScriptableTop(); // Match. We should report to this window. if (win && winId == win->WindowID()) { *aResult = true; return NS_OK; } } if (list->IsEmpty()) { list = nullptr; nsAutoPtr doomed; mRegisteringDocuments.RemoveAndForget(aScope, doomed); } } // Examine any windows performing a navigation that we are currently // intercepting. InterceptionList* intList = mNavigationInterceptions.Get(aScope); if (intList) { for (uint32_t i = 0; i < intList->Length(); ++i) { nsCOMPtr channel = intList->ElementAt(i); nsCOMPtr inner; nsresult rv = channel->GetChannel(getter_AddRefs(inner)); if (NS_WARN_IF(NS_FAILED(rv))) { continue; } uint64_t id = nsContentUtils::GetInnerWindowID(inner); if (id == 0) { continue; } nsCOMPtr win = nsGlobalWindow::GetInnerWindowWithId(id); if (!win) { continue; } win = win->GetScriptableTop(); // Match. We should report to this window. if (win && winId == win->WindowID()) { *aResult = true; return NS_OK; } } } // Next examine controlled documents to see if the windows match. for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { ServiceWorkerRegistrationInfo* reg = iter.UserData(); MOZ_ASSERT(reg); if (!reg->mScope.Equals(aScope)) { continue; } nsCOMPtr doc = do_QueryInterface(iter.Key()); if (!doc || !doc->IsCurrentActiveDocument()) { continue; } nsCOMPtr win = doc->GetWindow(); if (!win) { continue; } win = win->GetScriptableTop(); // Match. We should report to this window. if (win && winId == win->WindowID()) { *aResult = true; return NS_OK; } } // No match. We should not report to this window. return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (strcmp(aTopic, PURGE_SESSION_HISTORY) == 0) { MOZ_ASSERT(XRE_IsParentProcess()); RemoveAll(); PropagateRemoveAll(); return NS_OK; } if (strcmp(aTopic, PURGE_DOMAIN_DATA) == 0) { MOZ_ASSERT(XRE_IsParentProcess()); nsAutoString domain(aData); RemoveAndPropagate(NS_ConvertUTF16toUTF8(domain)); return NS_OK; } if (strcmp(aTopic, CLEAR_ORIGIN_DATA) == 0) { MOZ_ASSERT(XRE_IsParentProcess()); PrincipalOriginAttributes attrs; MOZ_ALWAYS_TRUE(attrs.Init(nsAutoString(aData))); RemoveAllRegistrations(&attrs); return NS_OK; } if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { mShuttingDown = true; for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { for (auto it2 = it1.UserData()->mUpdateTimers.Iter(); !it2.Done(); it2.Next()) { nsCOMPtr timer = it2.UserData(); timer->Cancel(); } it1.UserData()->mUpdateTimers.Clear(); } nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); if (XRE_IsParentProcess()) { obs->RemoveObserver(this, PURGE_SESSION_HISTORY); obs->RemoveObserver(this, PURGE_DOMAIN_DATA); obs->RemoveObserver(this, CLEAR_ORIGIN_DATA); } } if (mActor) { mActor->ManagerShuttingDown(); RefPtr runnable = new TeardownRunnable(mActor); nsresult rv = NS_DispatchToMainThread(runnable); Unused << NS_WARN_IF(NS_FAILED(rv)); mActor = nullptr; } return NS_OK; } MOZ_CRASH("Received message we aren't supposed to be registered for!"); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::PropagateSoftUpdate(JS::Handle aOriginAttributes, const nsAString& aScope, JSContext* aCx) { AssertIsOnMainThread(); PrincipalOriginAttributes attrs; if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { return NS_ERROR_INVALID_ARG; } PropagateSoftUpdate(attrs, aScope); return NS_OK; } void ServiceWorkerManager::PropagateSoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, const nsAString& aScope) { AssertIsOnMainThread(); if (!mActor) { RefPtr runnable = new PropagateSoftUpdateRunnable(aOriginAttributes, aScope); AppendPendingOperation(runnable); return; } mActor->SendPropagateSoftUpdate(aOriginAttributes, nsString(aScope)); } NS_IMETHODIMP ServiceWorkerManager::PropagateUnregister(nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback, const nsAString& aScope) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); if (!mActor) { RefPtr runnable = new PropagateUnregisterRunnable(aPrincipal, aCallback, aScope); AppendPendingOperation(runnable); return NS_OK; } PrincipalInfo principalInfo; if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) { return NS_ERROR_FAILURE; } mActor->SendPropagateUnregister(principalInfo, nsString(aScope)); nsresult rv = Unregister(aPrincipal, aCallback, aScope); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void ServiceWorkerManager::NotifyListenersOnRegister( nsIServiceWorkerRegistrationInfo* aInfo) { nsTArray> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnRegister(aInfo); } } void ServiceWorkerManager::NotifyListenersOnUnregister( nsIServiceWorkerRegistrationInfo* aInfo) { nsTArray> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnUnregister(aInfo); } } void ServiceWorkerManager::AddRegisteringDocument(const nsACString& aScope, nsIDocument* aDoc) { AssertIsOnMainThread(); MOZ_ASSERT(!aScope.IsEmpty()); MOZ_ASSERT(aDoc); WeakDocumentList* list = mRegisteringDocuments.LookupOrAdd(aScope); MOZ_ASSERT(list); for (int32_t i = list->Length() - 1; i >= 0; --i) { nsCOMPtr existing = do_QueryReferent(list->ElementAt(i)); if (!existing) { list->RemoveElementAt(i); continue; } if (existing == aDoc) { return; } } list->AppendElement(do_GetWeakReference(aDoc)); } class ServiceWorkerManager::InterceptionReleaseHandle final : public nsISupports { const nsCString mScope; // Weak reference to channel is safe, because the channel holds a // reference to this object. Also, the pointer is only used for // comparison purposes. nsIInterceptedChannel* mChannel; ~InterceptionReleaseHandle() { RefPtr swm = ServiceWorkerManager::GetInstance(); swm->RemoveNavigationInterception(mScope, mChannel); } public: InterceptionReleaseHandle(const nsACString& aScope, nsIInterceptedChannel* aChannel) : mScope(aScope) , mChannel(aChannel) { AssertIsOnMainThread(); MOZ_ASSERT(!aScope.IsEmpty()); MOZ_ASSERT(mChannel); } NS_DECL_ISUPPORTS }; NS_IMPL_ISUPPORTS0(ServiceWorkerManager::InterceptionReleaseHandle); void ServiceWorkerManager::AddNavigationInterception(const nsACString& aScope, nsIInterceptedChannel* aChannel) { AssertIsOnMainThread(); MOZ_ASSERT(!aScope.IsEmpty()); MOZ_ASSERT(aChannel); InterceptionList* list = mNavigationInterceptions.LookupOrAdd(aScope); MOZ_ASSERT(list); MOZ_ASSERT(!list->Contains(aChannel)); nsCOMPtr releaseHandle = new InterceptionReleaseHandle(aScope, aChannel); aChannel->SetReleaseHandle(releaseHandle); list->AppendElement(aChannel); } void ServiceWorkerManager::RemoveNavigationInterception(const nsACString& aScope, nsIInterceptedChannel* aChannel) { AssertIsOnMainThread(); MOZ_ASSERT(aChannel); InterceptionList* list = mNavigationInterceptions.Get(aScope); if (list) { MOZ_ALWAYS_TRUE(list->RemoveElement(aChannel)); MOZ_ASSERT(!list->Contains(aChannel)); if (list->IsEmpty()) { list = nullptr; nsAutoPtr doomed; mNavigationInterceptions.RemoveAndForget(aScope, doomed); } } } class UpdateTimerCallback final : public nsITimerCallback { nsCOMPtr mPrincipal; const nsCString mScope; ~UpdateTimerCallback() { } public: UpdateTimerCallback(nsIPrincipal* aPrincipal, const nsACString& aScope) : mPrincipal(aPrincipal) , mScope(aScope) { AssertIsOnMainThread(); MOZ_ASSERT(mPrincipal); MOZ_ASSERT(!mScope.IsEmpty()); } NS_IMETHOD Notify(nsITimer* aTimer) override { AssertIsOnMainThread(); RefPtr swm = ServiceWorkerManager::GetInstance(); if (!swm) { // shutting down, do nothing return NS_OK; } swm->UpdateTimerFired(mPrincipal, mScope); return NS_OK; } NS_DECL_ISUPPORTS }; NS_IMPL_ISUPPORTS(UpdateTimerCallback, nsITimerCallback) void ServiceWorkerManager::ScheduleUpdateTimer(nsIPrincipal* aPrincipal, const nsACString& aScope) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(!aScope.IsEmpty()); if (mShuttingDown) { return; } nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(scopeKey, &data)) { return; } nsCOMPtr timer = data->mUpdateTimers.Get(aScope); if (timer) { // There is already a timer scheduled. In this case just use the original // schedule time. We don't want to push it out to a later time since that // could allow updates to be starved forever if events are continuously // fired. return; } timer = do_CreateInstance("@mozilla.org/timer;1", &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr callback = new UpdateTimerCallback(aPrincipal, aScope); const uint32_t UPDATE_DELAY_MS = 1000; rv = timer->InitWithCallback(callback, UPDATE_DELAY_MS, nsITimer::TYPE_ONE_SHOT); if (NS_WARN_IF(NS_FAILED(rv))) { return; } data->mUpdateTimers.Put(aScope, timer); } void ServiceWorkerManager::UpdateTimerFired(nsIPrincipal* aPrincipal, const nsACString& aScope) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(!aScope.IsEmpty()); if (mShuttingDown) { return; } // First cleanup the timer. nsAutoCString scopeKey; nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); if (NS_WARN_IF(NS_FAILED(rv))) { return; } RegistrationDataPerPrincipal* data; if (!mRegistrationInfos.Get(scopeKey, &data)) { return; } nsCOMPtr timer = data->mUpdateTimers.Get(aScope); if (timer) { timer->Cancel(); data->mUpdateTimers.Remove(aScope); } RefPtr registration; data->mInfos.Get(aScope, getter_AddRefs(registration)); if (!registration) { return; } if (!registration->CheckAndClearIfUpdateNeeded()) { return; } PrincipalOriginAttributes attrs = mozilla::BasePrincipal::Cast(aPrincipal)->OriginAttributesRef(); // Then trigger an update to fire asynchronously now. PropagateSoftUpdate(attrs, NS_ConvertUTF8toUTF16(aScope)); } NS_IMPL_ISUPPORTS(ServiceWorkerInfo, nsIServiceWorkerInfo) NS_IMETHODIMP ServiceWorkerInfo::GetScriptSpec(nsAString& aScriptSpec) { AssertIsOnMainThread(); CopyUTF8toUTF16(mScriptSpec, aScriptSpec); return NS_OK; } NS_IMETHODIMP ServiceWorkerInfo::GetCacheName(nsAString& aCacheName) { AssertIsOnMainThread(); aCacheName = mCacheName; return NS_OK; } NS_IMETHODIMP ServiceWorkerInfo::GetDebugger(nsIWorkerDebugger** aResult) { if (NS_WARN_IF(!aResult)) { return NS_ERROR_FAILURE; } return mServiceWorkerPrivate->GetDebugger(aResult); } NS_IMETHODIMP ServiceWorkerInfo::AttachDebugger() { return mServiceWorkerPrivate->AttachDebugger(); } NS_IMETHODIMP ServiceWorkerInfo::DetachDebugger() { return mServiceWorkerPrivate->DetachDebugger(); } void ServiceWorkerInfo::AppendWorker(ServiceWorker* aWorker) { MOZ_ASSERT(aWorker); #ifdef DEBUG nsAutoString workerURL; aWorker->GetScriptURL(workerURL); MOZ_ASSERT(workerURL.Equals(NS_ConvertUTF8toUTF16(mScriptSpec))); #endif MOZ_ASSERT(!mInstances.Contains(aWorker)); mInstances.AppendElement(aWorker); aWorker->SetState(State()); } void ServiceWorkerInfo::RemoveWorker(ServiceWorker* aWorker) { MOZ_ASSERT(aWorker); #ifdef DEBUG nsAutoString workerURL; aWorker->GetScriptURL(workerURL); MOZ_ASSERT(workerURL.Equals(NS_ConvertUTF8toUTF16(mScriptSpec))); #endif MOZ_ASSERT(mInstances.Contains(aWorker)); mInstances.RemoveElement(aWorker); } namespace { class ChangeStateUpdater final : public nsRunnable { public: ChangeStateUpdater(const nsTArray& aInstances, ServiceWorkerState aState) : mState(aState) { for (size_t i = 0; i < aInstances.Length(); ++i) { mInstances.AppendElement(aInstances[i]); } } NS_IMETHODIMP Run() { // We need to update the state of all instances atomically before notifying // them to make sure that the observed state for all instances inside // statechange event handlers is correct. for (size_t i = 0; i < mInstances.Length(); ++i) { mInstances[i]->SetState(mState); } for (size_t i = 0; i < mInstances.Length(); ++i) { mInstances[i]->DispatchStateChange(mState); } return NS_OK; } private: nsAutoTArray, 1> mInstances; ServiceWorkerState mState; }; } void ServiceWorkerInfo::UpdateState(ServiceWorkerState aState) { AssertIsOnMainThread(); #ifdef DEBUG // Any state can directly transition to redundant, but everything else is // ordered. if (aState != ServiceWorkerState::Redundant) { MOZ_ASSERT_IF(mState == ServiceWorkerState::EndGuard_, aState == ServiceWorkerState::Installing); MOZ_ASSERT_IF(mState == ServiceWorkerState::Installing, aState == ServiceWorkerState::Installed); MOZ_ASSERT_IF(mState == ServiceWorkerState::Installed, aState == ServiceWorkerState::Activating); MOZ_ASSERT_IF(mState == ServiceWorkerState::Activating, aState == ServiceWorkerState::Activated); } // Activated can only go to redundant. MOZ_ASSERT_IF(mState == ServiceWorkerState::Activated, aState == ServiceWorkerState::Redundant); #endif // Flush any pending functional events to the worker when it transitions to the // activated state. // TODO: Do we care that these events will race with the propagation of the // state change? if (aState == ServiceWorkerState::Activated && mState != aState) { mServiceWorkerPrivate->Activated(); } mState = aState; nsCOMPtr r = new ChangeStateUpdater(mInstances, mState); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(r.forget()))); } ServiceWorkerInfo::ServiceWorkerInfo(ServiceWorkerRegistrationInfo* aReg, const nsACString& aScriptSpec, const nsAString& aCacheName) : mRegistration(aReg) , mScriptSpec(aScriptSpec) , mCacheName(aCacheName) , mState(ServiceWorkerState::EndGuard_) , mServiceWorkerID(GetNextID()) , mServiceWorkerPrivate(new ServiceWorkerPrivate(this)) , mSkipWaitingFlag(false) { MOZ_ASSERT(mRegistration); MOZ_ASSERT(!aCacheName.IsEmpty()); } ServiceWorkerInfo::~ServiceWorkerInfo() { MOZ_ASSERT(mServiceWorkerPrivate); mServiceWorkerPrivate->NoteDeadServiceWorkerInfo(); } static uint64_t gServiceWorkerInfoCurrentID = 0; uint64_t ServiceWorkerInfo::GetNextID() const { return ++gServiceWorkerInfoCurrentID; } END_WORKERS_NAMESPACE