/* 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 "nsIDocument.h" #include "nsPIDOMWindow.h" #include "jsapi.h" #include "mozilla/Preferences.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/DOMError.h" #include "nsContentUtils.h" #include "nsCxPusher.h" #include "nsNetUtil.h" #include "nsProxyRelease.h" #include "nsTArray.h" #include "RuntimeService.h" #include "ServiceWorker.h" #include "WorkerInlines.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" using namespace mozilla; using namespace mozilla::dom; BEGIN_WORKERS_NAMESPACE NS_IMPL_ISUPPORTS0(ServiceWorkerRegistration) UpdatePromise::UpdatePromise() : mState(Pending) { MOZ_COUNT_CTOR(UpdatePromise); } UpdatePromise::~UpdatePromise() { MOZ_COUNT_DTOR(UpdatePromise); } void UpdatePromise::AddPromise(Promise* aPromise) { MOZ_ASSERT(mState == Pending); mPromises.AppendElement(aPromise); } void UpdatePromise::ResolveAllPromises(const nsACString& aScriptSpec, const nsACString& aScope) { AssertIsOnMainThread(); MOZ_ASSERT(mState == Pending); mState = Resolved; RuntimeService* rs = RuntimeService::GetOrCreateService(); MOZ_ASSERT(rs); nsTArray> array; array.SwapElements(mPromises); for (uint32_t i = 0; i < array.Length(); ++i) { nsTWeakRef& pendingPromise = array.ElementAt(i); if (pendingPromise) { nsCOMPtr go = do_QueryInterface(pendingPromise->GetParentObject()); MOZ_ASSERT(go); AutoSafeJSContext cx; JS::Rooted global(cx, go->GetGlobalJSObject()); JSAutoCompartment ac(cx, global); GlobalObject domGlobal(cx, global); nsRefPtr serviceWorker; nsresult rv = rs->CreateServiceWorker(domGlobal, NS_ConvertUTF8toUTF16(aScriptSpec), aScope, getter_AddRefs(serviceWorker)); if (NS_WARN_IF(NS_FAILED(rv))) { pendingPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); continue; } pendingPromise->MaybeResolve(serviceWorker); } } } void UpdatePromise::RejectAllPromises(nsresult aRv) { AssertIsOnMainThread(); MOZ_ASSERT(mState == Pending); mState = Rejected; nsTArray> array; array.SwapElements(mPromises); for (uint32_t i = 0; i < array.Length(); ++i) { nsTWeakRef& pendingPromise = array.ElementAt(i); if (pendingPromise) { // Since ServiceWorkerContainer is only exposed to windows we can be // certain about this cast. nsCOMPtr window = do_QueryInterface(pendingPromise->GetParentObject()); MOZ_ASSERT(window); nsRefPtr domError = new DOMError(window, aRv); pendingPromise->MaybeRejectBrokenly(domError); } } } class FinishFetchOnMainThreadRunnable : public nsRunnable { nsMainThreadPtrHandle mUpdateInstance; public: FinishFetchOnMainThreadRunnable (const nsMainThreadPtrHandle& aUpdateInstance) : mUpdateInstance(aUpdateInstance) { } NS_IMETHOD Run() MOZ_OVERRIDE; }; class FinishSuccessfulFetchWorkerRunnable : public WorkerRunnable { nsMainThreadPtrHandle mUpdateInstance; public: FinishSuccessfulFetchWorkerRunnable(WorkerPrivate* aWorkerPrivate, const nsMainThreadPtrHandle& aUpdateInstance) : WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount), mUpdateInstance(aUpdateInstance) { AssertIsOnMainThread(); } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); if (!aWorkerPrivate->WorkerScriptExecutedSuccessfully()) { return true; } nsRefPtr r = new FinishFetchOnMainThreadRunnable(mUpdateInstance); NS_DispatchToMainThread(r); return true; } }; // Allows newer calls to Update() to 'abort' older calls. // Each call to Update() creates the instance which handles creating the // worker and queues up a runnable to resolve the update promise once the // worker has successfully been parsed. class ServiceWorkerUpdateInstance MOZ_FINAL : public nsISupports { // Owner of this instance. ServiceWorkerRegistration* mRegistration; nsCString mScriptSpec; nsCOMPtr mWindow; bool mAborted; public: NS_DECL_ISUPPORTS ServiceWorkerUpdateInstance(ServiceWorkerRegistration *aRegistration, nsPIDOMWindow* aWindow) : mRegistration(aRegistration), // Capture the current script spec in case register() gets called. mScriptSpec(aRegistration->mScriptSpec), mWindow(aWindow), mAborted(false) { AssertIsOnMainThread(); } void Abort() { MOZ_ASSERT(!mAborted); mAborted = true; } void Update() { AssertIsOnMainThread(); nsRefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); nsRefPtr serviceWorker; nsresult rv = swm->CreateServiceWorkerForWindow(mWindow, mScriptSpec, mRegistration->mScope, getter_AddRefs(serviceWorker)); if (NS_WARN_IF(NS_FAILED(rv))) { swm->RejectUpdatePromiseObservers(mRegistration, rv); return; } nsMainThreadPtrHandle handle = new nsMainThreadPtrHolder(this); // FIXME(nsm): Deal with error case (worker failed to download, redirect, // parse) in error handler patch. nsRefPtr r = new FinishSuccessfulFetchWorkerRunnable(serviceWorker->GetWorkerPrivate(), handle); AutoSafeJSContext cx; if (!r->Dispatch(cx)) { swm->RejectUpdatePromiseObservers(mRegistration, NS_ERROR_FAILURE); } } void FetchDone() { AssertIsOnMainThread(); if (mAborted) { return; } nsRefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); swm->FinishFetch(mRegistration, mWindow); } }; NS_IMPL_ISUPPORTS0(ServiceWorkerUpdateInstance) NS_IMETHODIMP FinishFetchOnMainThreadRunnable::Run() { AssertIsOnMainThread(); mUpdateInstance->FetchDone(); return NS_OK; } ServiceWorkerRegistration::ServiceWorkerRegistration(const nsACString& aScope) : mScope(aScope), mPendingUninstall(false) { } ServiceWorkerRegistration::~ServiceWorkerRegistration() { } ////////////////////////// // ServiceWorkerManager // ////////////////////////// NS_IMPL_ADDREF(ServiceWorkerManager) NS_IMPL_RELEASE(ServiceWorkerManager) NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager) NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager) if (aIID.Equals(NS_GET_IID(ServiceWorkerManager))) foundInterface = static_cast(this); else NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIServiceWorkerManager) NS_INTERFACE_MAP_END ServiceWorkerManager::ServiceWorkerManager() { } ServiceWorkerManager::~ServiceWorkerManager() { // The map will assert if it is not empty when destroyed. mDomainMap.EnumerateRead(CleanupServiceWorkerInformation, nullptr); mDomainMap.Clear(); } /* static */ PLDHashOperator ServiceWorkerManager::CleanupServiceWorkerInformation(const nsACString& aDomain, ServiceWorkerDomainInfo* aDomainInfo, void *aUnused) { aDomainInfo->mServiceWorkerRegistrations.Clear(); return PL_DHASH_NEXT; } /* * Implements the async aspects of the register algorithm. */ class RegisterRunnable : public nsRunnable { nsCOMPtr mWindow; const nsCString mScope; nsCOMPtr mScriptURI; nsRefPtr mPromise; public: RegisterRunnable(nsPIDOMWindow* aWindow, const nsCString aScope, nsIURI* aScriptURI, Promise* aPromise) : mWindow(aWindow), mScope(aScope), mScriptURI(aScriptURI), mPromise(aPromise) { } NS_IMETHODIMP Run() { nsCString domain; nsresult rv = mScriptURI->GetHost(domain); if (NS_WARN_IF(NS_FAILED(rv))) { mPromise->MaybeReject(rv); return NS_OK; } nsRefPtr swm = ServiceWorkerManager::GetInstance(); ServiceWorkerManager::ServiceWorkerDomainInfo* domainInfo = swm->mDomainMap.Get(domain); // XXXnsm: This pattern can be refactored if we end up using it // often enough. if (!swm->mDomainMap.Get(domain, &domainInfo)) { domainInfo = new ServiceWorkerManager::ServiceWorkerDomainInfo; swm->mDomainMap.Put(domain, domainInfo); } nsRefPtr registration = domainInfo->GetRegistration(mScope); nsCString spec; rv = mScriptURI->GetSpec(spec); if (NS_WARN_IF(NS_FAILED(rv))) { mPromise->MaybeReject(rv); return NS_OK; } if (registration) { registration->mPendingUninstall = false; if (spec.Equals(registration->mScriptSpec)) { // There is an existing update in progress. Resolve with whatever it // results in. if (registration->HasUpdatePromise()) { registration->AddUpdatePromiseObserver(mPromise); return NS_OK; } // There is no update in progress and since SW updating is upto the UA, // we will not update right now. Simply resolve with whatever worker we // have. ServiceWorkerInfo info = registration->Newest(); if (info.IsValid()) { nsRefPtr serviceWorker; nsresult rv = swm->CreateServiceWorkerForWindow(mWindow, info.GetScriptSpec(), registration->mScope, getter_AddRefs(serviceWorker)); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } mPromise->MaybeResolve(serviceWorker); return NS_OK; } } } else { registration = domainInfo->CreateNewRegistration(mScope); } registration->mScriptSpec = spec; rv = swm->Update(registration, mWindow); MOZ_ASSERT(registration->HasUpdatePromise()); // We append this register() call's promise after calling Update() because // we don't want this one to be aborted when the others (existing updates // for the same registration) are aborted. Update() sets a new // UpdatePromise on the registration. registration->mUpdatePromise->AddPromise(mPromise); return rv; } }; // If we return an error code here, the ServiceWorkerContainer will // automatically reject the Promise. NS_IMETHODIMP ServiceWorkerManager::Register(nsIDOMWindow* aWindow, const nsAString& aScope, const nsAString& aScriptURL, nsISupports** aPromise) { AssertIsOnMainThread(); MOZ_ASSERT(aWindow); // XXXnsm Don't allow chrome callers for now, we don't support chrome // ServiceWorkers. MOZ_ASSERT(!nsContentUtils::IsCallerChrome()); nsCOMPtr window = do_QueryInterface(aWindow); if (!window) { return NS_ERROR_FAILURE; } nsCOMPtr sgo = do_QueryInterface(window); nsRefPtr promise = new Promise(sgo); nsCOMPtr documentURI = window->GetDocumentURI(); if (!documentURI) { return NS_ERROR_FAILURE; } // Although the spec says that the same-origin checks should also be done // asynchronously, we do them in sync because the Promise created by the // WebIDL infrastructure due to a returned error will be resolved // asynchronously. We aren't making any internal state changes in these // checks, so ordering of multiple calls is not affected. nsresult rv; // FIXME(nsm): Bug 1003991. Disable check when devtools are open. if (!Preferences::GetBool("dom.serviceWorkers.testing.enabled")) { bool isHttps; rv = documentURI->SchemeIs("https", &isHttps); if (NS_FAILED(rv) || !isHttps) { NS_WARNING("ServiceWorker registration from insecure websites is not allowed."); return NS_ERROR_DOM_SECURITY_ERR; } } nsCOMPtr documentPrincipal; if (window->GetExtantDoc()) { documentPrincipal = window->GetExtantDoc()->NodePrincipal(); } else { documentPrincipal = do_CreateInstance("@mozilla.org/nullprincipal;1"); } nsCOMPtr scriptURI; rv = NS_NewURI(getter_AddRefs(scriptURI), aScriptURL, nullptr, documentURI); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Data URLs are not allowed. rv = documentPrincipal->CheckMayLoad(scriptURI, true /* report */, false /* allowIfInheritsPrincipal */); if (NS_FAILED(rv)) { return NS_ERROR_DOM_SECURITY_ERR; } nsCOMPtr scopeURI; rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, documentURI); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_DOM_SECURITY_ERR; } rv = documentPrincipal->CheckMayLoad(scopeURI, true /* report */, false /* allowIfInheritsPrinciple */); if (NS_FAILED(rv)) { return NS_ERROR_DOM_SECURITY_ERR; } nsCString cleanedScope; rv = scopeURI->GetSpecIgnoringRef(cleanedScope); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } nsRefPtr registerRunnable = new RegisterRunnable(window, cleanedScope, scriptURI, promise); promise.forget(aPromise); return NS_DispatchToCurrentThread(registerRunnable); } void ServiceWorkerManager::RejectUpdatePromiseObservers(ServiceWorkerRegistration* aRegistration, nsresult aRv) { AssertIsOnMainThread(); MOZ_ASSERT(aRegistration->HasUpdatePromise()); aRegistration->mUpdatePromise->RejectAllPromises(aRv); aRegistration->mUpdatePromise = nullptr; } /* * Update() does not return the Promise that the spec says it should. Callers * may access the registration's (new) Promise after calling this method. */ NS_IMETHODIMP ServiceWorkerManager::Update(ServiceWorkerRegistration* aRegistration, nsPIDOMWindow* aWindow) { if (aRegistration->HasUpdatePromise()) { NS_WARNING("Already had a UpdatePromise. Aborting that one!"); RejectUpdatePromiseObservers(aRegistration, NS_ERROR_DOM_ABORT_ERR); MOZ_ASSERT(aRegistration->mUpdateInstance); aRegistration->mUpdateInstance->Abort(); aRegistration->mUpdateInstance = nullptr; } if (aRegistration->mInstallingWorker.IsValid()) { // FIXME(nsm): Terminate the worker. We still haven't figured out worker // instance ownership when not associated with a window, so let's wait on // this. // FIXME(nsm): We should be setting the state on the actual worker // instance. // FIXME(nsm): Fire "statechange" on installing worker instance. aRegistration->mInstallingWorker.Invalidate(); } aRegistration->mUpdatePromise = new UpdatePromise(); // FIXME(nsm): Bug 931249. If we don't need to fetch & install, resolve // promise and skip this. // FIXME(nsm): Bug 931249. Force cache update if > 1 day. aRegistration->mUpdateInstance = new ServiceWorkerUpdateInstance(aRegistration, aWindow); aRegistration->mUpdateInstance->Update(); return NS_OK; } // If we return an error, ServiceWorkerContainer will reject the Promise. NS_IMETHODIMP ServiceWorkerManager::Unregister(nsIDOMWindow* aWindow, const nsAString& aScope, nsISupports** aPromise) { AssertIsOnMainThread(); MOZ_ASSERT(aWindow); // XXXnsm Don't allow chrome callers for now. MOZ_ASSERT(!nsContentUtils::IsCallerChrome()); // FIXME(nsm): Same bug, different patch. return NS_ERROR_DOM_NOT_SUPPORTED_ERR; } /* static */ already_AddRefed ServiceWorkerManager::GetInstance() { nsCOMPtr swm = do_GetService(SERVICEWORKERMANAGER_CONTRACTID); nsRefPtr concrete = do_QueryObject(swm); return concrete.forget(); } void ServiceWorkerManager::ResolveRegisterPromises(ServiceWorkerRegistration* aRegistration, const nsACString& aWorkerScriptSpec) { AssertIsOnMainThread(); MOZ_ASSERT(aRegistration->HasUpdatePromise()); if (aRegistration->mUpdatePromise->IsRejected()) { aRegistration->mUpdatePromise = nullptr; return; } aRegistration->mUpdatePromise->ResolveAllPromises(aWorkerScriptSpec, aRegistration->mScope); aRegistration->mUpdatePromise = nullptr; } // Must NS_Free() aString void ServiceWorkerManager::FinishFetch(ServiceWorkerRegistration* aRegistration, nsPIDOMWindow* aWindow) { AssertIsOnMainThread(); MOZ_ASSERT(aRegistration->HasUpdatePromise()); MOZ_ASSERT(aRegistration->mUpdateInstance); aRegistration->mUpdateInstance = nullptr; if (aRegistration->mUpdatePromise->IsRejected()) { aRegistration->mUpdatePromise = nullptr; return; } // We have skipped Steps 3-8.3 of the Update algorithm here! nsRefPtr worker; nsresult rv = CreateServiceWorkerForWindow(aWindow, aRegistration->mScriptSpec, aRegistration->mScope, getter_AddRefs(worker)); if (NS_WARN_IF(NS_FAILED(rv))) { RejectUpdatePromiseObservers(aRegistration, rv); return; } ResolveRegisterPromises(aRegistration, aRegistration->mScriptSpec); ServiceWorkerInfo info(aRegistration->mScriptSpec); Install(aRegistration, info); } void ServiceWorkerManager::Install(ServiceWorkerRegistration* aRegistration, ServiceWorkerInfo aServiceWorkerInfo) { // FIXME(nsm): Same bug, different patch. } NS_IMETHODIMP ServiceWorkerManager::CreateServiceWorkerForWindow(nsPIDOMWindow* aWindow, const nsACString& aScriptSpec, const nsACString& aScope, ServiceWorker** aServiceWorker) { AssertIsOnMainThread(); MOZ_ASSERT(aWindow); RuntimeService* rs = RuntimeService::GetOrCreateService(); nsRefPtr serviceWorker; nsCOMPtr sgo = do_QueryInterface(aWindow); AutoSafeJSContext cx; JS::Rooted jsGlobal(cx, sgo->GetGlobalJSObject()); JSAutoCompartment ac(cx, jsGlobal); GlobalObject global(cx, jsGlobal); nsresult rv = rs->CreateServiceWorker(global, NS_ConvertUTF8toUTF16(aScriptSpec), aScope, getter_AddRefs(serviceWorker)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } serviceWorker.forget(aServiceWorker); return rv; } END_WORKERS_NAMESPACE