/* 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 "nsIScriptSecurityManager.h" #include "nsPIDOMWindow.h" #include "jsapi.h" #include "mozilla/Preferences.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/DOMError.h" #include "mozilla/dom/ErrorEvent.h" #include "mozilla/dom/InstallEventBinding.h" #include "mozilla/dom/PromiseNativeHandler.h" #include "nsContentUtils.h" #include "nsCxPusher.h" #include "nsNetUtil.h" #include "nsProxyRelease.h" #include "nsTArray.h" #include "RuntimeService.h" #include "ServiceWorker.h" #include "ServiceWorkerEvents.h" #include "WorkerInlines.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "WorkerScope.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); } } } void UpdatePromise::RejectAllPromises(const ErrorEventInit& aErrorDesc) { 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 go = do_QueryInterface(pendingPromise->GetParentObject()); MOZ_ASSERT(go); AutoJSAPI jsapi; jsapi.Init(go); JSContext* cx = jsapi.cx(); JS::Rooted stack(cx, JS_GetEmptyString(JS_GetRuntime(cx))); JS::Rooted fnval(cx); ToJSValue(cx, aErrorDesc.mFilename, &fnval); JS::Rooted fn(cx, fnval.toString()); JS::Rooted msgval(cx); ToJSValue(cx, aErrorDesc.mMessage, &msgval); JS::Rooted msg(cx, msgval.toString()); JS::Rooted error(cx); if (!JS::CreateError(cx, JSEXN_ERR, stack, fn, aErrorDesc.mLineno, aErrorDesc.mColno, nullptr, msg, &error)) { pendingPromise->MaybeReject(NS_ERROR_FAILURE); continue; } pendingPromise->MaybeReject(cx, error); } } } 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; ~ServiceWorkerUpdateInstance() {} 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(); } const nsCString& GetScriptSpec() const { return mScriptSpec; } 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; // 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; } void ServiceWorkerManager::RejectUpdatePromiseObservers(ServiceWorkerRegistration* aRegistration, const ErrorEventInit& aErrorDesc) { AssertIsOnMainThread(); MOZ_ASSERT(aRegistration->HasUpdatePromise()); aRegistration->mUpdatePromise->RejectAllPromises(aErrorDesc); 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::HandleError(JSContext* aCx, const nsACString& aScope, const nsAString& aWorkerURL, nsString aMessage, nsString aFilename, nsString aLine, uint32_t aLineNumber, uint32_t aColumnNumber, uint32_t aFlags) { AssertIsOnMainThread(); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aScope, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCString domain; rv = uri->GetHost(domain); if (NS_WARN_IF(NS_FAILED(rv))) { return; } ServiceWorkerDomainInfo* domainInfo; if (!mDomainMap.Get(domain, &domainInfo)) { return; } nsCString scope; scope.Assign(aScope); nsRefPtr registration = domainInfo->GetRegistration(scope); MOZ_ASSERT(registration); RootedDictionary init(aCx); init.mMessage = aMessage; init.mFilename = aFilename; init.mLineno = aLineNumber; init.mColno = aColumnNumber; // If the worker was the one undergoing registration, we reject the promises, // otherwise we fire events on the ServiceWorker instances. // If there is an update in progress and the worker that errored is the same one // that is being updated, it is a sufficient test for 'this worker is being // registered'. // FIXME(nsm): Except the case where an update is found for a worker, in // which case we'll need some other association than simply the URL. if (registration->mUpdateInstance && registration->mUpdateInstance->GetScriptSpec().Equals(NS_ConvertUTF16toUTF8(aWorkerURL))) { RejectUpdatePromiseObservers(registration, init); // We don't need to abort here since the worker has already run. registration->mUpdateInstance = nullptr; } else { // FIXME(nsm): Bug 983497 Fire 'error' on ServiceWorkerContainers. } } class FinishInstallRunnable MOZ_FINAL : public nsRunnable { nsMainThreadPtrHandle mRegistration; public: explicit FinishInstallRunnable( const nsMainThreadPtrHandle& aRegistration) : mRegistration(aRegistration) { MOZ_ASSERT(!NS_IsMainThread()); } NS_IMETHOD Run() MOZ_OVERRIDE { AssertIsOnMainThread(); nsRefPtr swm = ServiceWorkerManager::GetInstance(); swm->FinishInstall(mRegistration.get()); return NS_OK; } }; class CancelServiceWorkerInstallationRunnable MOZ_FINAL : public nsRunnable { nsMainThreadPtrHandle mRegistration; public: explicit CancelServiceWorkerInstallationRunnable( const nsMainThreadPtrHandle& aRegistration) : mRegistration(aRegistration) { } NS_IMETHOD Run() MOZ_OVERRIDE { AssertIsOnMainThread(); // FIXME(nsm): Change installing worker state to redundant. // FIXME(nsm): Fire statechange. mRegistration->mInstallingWorker.Invalidate(); return NS_OK; } }; /* * Used to handle InstallEvent::waitUntil() and proceed with installation. */ class FinishInstallHandler MOZ_FINAL : public PromiseNativeHandler { nsMainThreadPtrHandle mRegistration; virtual ~FinishInstallHandler() { } public: explicit FinishInstallHandler( const nsMainThreadPtrHandle& aRegistration) : mRegistration(aRegistration) { MOZ_ASSERT(!NS_IsMainThread()); } void ResolvedCallback(JSContext* aCx, JS::Handle aValue) MOZ_OVERRIDE { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); nsRefPtr r = new FinishInstallRunnable(mRegistration); NS_DispatchToMainThread(r); } void RejectedCallback(JSContext* aCx, JS::Handle aValue) MOZ_OVERRIDE { nsRefPtr r = new CancelServiceWorkerInstallationRunnable(mRegistration); NS_DispatchToMainThread(r); } }; /* * Fires 'install' event on the ServiceWorkerGlobalScope. Modifies busy count * since it fires the event. This is ok since there can't be nested * ServiceWorkers, so the parent thread -> worker thread requirement for * runnables is satisfied. */ class InstallEventRunnable MOZ_FINAL : public WorkerRunnable { nsMainThreadPtrHandle mRegistration; nsCString mScope; public: InstallEventRunnable( WorkerPrivate* aWorkerPrivate, const nsMainThreadPtrHandle& aRegistration) : WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount), mRegistration(aRegistration), mScope(aRegistration.get()->mScope) // copied for access on worker thread. { AssertIsOnMainThread(); MOZ_ASSERT(aWorkerPrivate); } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); return DispatchInstallEvent(aCx, aWorkerPrivate); } private: bool DispatchInstallEvent(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); InstallEventInit init; init.mBubbles = false; init.mCancelable = true; // FIXME(nsm): Bug 982787 pass previous active worker. nsRefPtr target = aWorkerPrivate->GlobalScope(); nsRefPtr event = InstallEvent::Constructor(target, NS_LITERAL_STRING("install"), init); event->SetTrusted(true); nsRefPtr waitUntilPromise; nsresult rv = target->DispatchDOMEvent(nullptr, event, nullptr, nullptr); nsCOMPtr sgo = aWorkerPrivate->GlobalScope(); if (NS_SUCCEEDED(rv)) { waitUntilPromise = event->GetPromise(); if (!waitUntilPromise) { ErrorResult rv; waitUntilPromise = Promise::Resolve(sgo, aCx, JS::UndefinedHandleValue, rv); } } else { ErrorResult rv; // Continue with a canceled install. waitUntilPromise = Promise::Reject(sgo, aCx, JS::UndefinedHandleValue, rv); } nsRefPtr handler = new FinishInstallHandler(mRegistration); waitUntilPromise->AppendNativeHandler(handler); return true; } }; void ServiceWorkerManager::Install(ServiceWorkerRegistration* aRegistration, ServiceWorkerInfo aServiceWorkerInfo) { AssertIsOnMainThread(); aRegistration->mInstallingWorker = aServiceWorkerInfo; nsMainThreadPtrHandle handle = new nsMainThreadPtrHolder(aRegistration); nsRefPtr serviceWorker; nsresult rv = CreateServiceWorker(aServiceWorkerInfo.GetScriptSpec(), aRegistration->mScope, getter_AddRefs(serviceWorker)); if (NS_WARN_IF(NS_FAILED(rv))) { aRegistration->mInstallingWorker.Invalidate(); return; } nsRefPtr r = new InstallEventRunnable(serviceWorker->GetWorkerPrivate(), handle); AutoSafeJSContext cx; r->Dispatch(cx); // When this function exits, although we've lost references to the ServiceWorker, // which means the underlying WorkerPrivate has no references, the worker // will stay alive due to the modified busy count until the install event has // been dispatched. // NOTE: The worker spec does not require Promises to keep a worker alive, so // the waitUntil() construct by itself will not keep a worker alive beyond // the event dispatch. On the other hand, networking, IDB and so on do keep // the worker alive, so the waitUntil() is only relevant if the Promise is // gated on those actions. I (nsm) am not sure if it is worth requiring // a special spec mention saying the install event should keep the worker // alive indefinitely purely on the basis of calling waitUntil(), since // a wait is likely to be required only when performing networking or storage // transactions in the first place. // FIXME(nsm): Bug 983497. Fire "updatefound" on ServiceWorkerContainers. } class ActivationRunnable : public nsRunnable { public: explicit ActivationRunnable(ServiceWorkerRegistration* aRegistration) { } }; void ServiceWorkerManager::FinishInstall(ServiceWorkerRegistration* aRegistration) { AssertIsOnMainThread(); if (aRegistration->mWaitingWorker.IsValid()) { // FIXME(nsm): Actually update the state of active ServiceWorker instances. } aRegistration->mWaitingWorker = aRegistration->mInstallingWorker; aRegistration->mInstallingWorker.Invalidate(); // FIXME(nsm): Actually update state of active ServiceWorker instances to // installed. // FIXME(nsm): Fire statechange on the instances. // FIXME(nsm): Handle replace(). // FIXME(nsm): Check that no document is using the registration! nsRefPtr r = new ActivationRunnable(aRegistration); nsresult rv = NS_DispatchToMainThread(r); if (NS_WARN_IF(NS_FAILED(rv))) { // FIXME(nsm): Handle error. } } 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; } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistration(nsPIDOMWindow* aWindow) { nsCOMPtr documentURI = aWindow->GetDocumentURI(); return GetServiceWorkerRegistration(documentURI); } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistration(nsIDocument* aDoc) { nsCOMPtr documentURI = aDoc->GetDocumentURI(); return GetServiceWorkerRegistration(documentURI); } already_AddRefed ServiceWorkerManager::GetServiceWorkerRegistration(nsIURI* aURI) { if (!aURI) { return nullptr; } nsCString domain; nsresult rv = aURI->GetHost(domain); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } ServiceWorkerDomainInfo* domainInfo = mDomainMap.Get(domain); // XXXnsm: This pattern can be refactored if we end up using it // often enough. if (!domainInfo) { return nullptr; } nsCString spec; rv = aURI->GetSpec(spec); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } nsCString scope = FindScopeForPath(domainInfo->mOrderedScopes, spec); if (scope.IsEmpty()) { return nullptr; } ServiceWorkerRegistration* registration; domainInfo->mServiceWorkerRegistrations.Get(scope, ®istration); // ordered scopes and registrations better be in sync. MOZ_ASSERT(registration); return registration; } namespace { /* * Returns string without trailing '*'. */ void ScopeWithoutStar(const nsACString& aScope, nsACString& out) { if (aScope.Last() == '*') { out.Assign(StringHead(aScope, aScope.Length() - 1)); return; } out.Assign(aScope); } }; // anonymous namespace /* static */ void ServiceWorkerManager::AddScope(nsTArray& aList, const nsACString& aScope) { for (uint32_t i = 0; i < aList.Length(); ++i) { const nsCString& current = aList[i]; // Perfect match! if (aScope.Equals(current)) { return; } nsCString withoutStar; ScopeWithoutStar(current, withoutStar); // Edge case of match without '*'. // /foo should be sorted before /foo*. if (aScope.Equals(withoutStar)) { aList.InsertElementAt(i, aScope); return; } // /foo/bar* should be before /foo/* // Similarly /foo/b* is between the two. // But is /foo* categorically different? if (StringBeginsWith(aScope, withoutStar)) { // If the new scope is a pattern and the old one is a path, the new one // goes after. This way Add(/foo) followed by Add(/foo*) ends up with // [/foo, /foo*]. if (aScope.Last() == '*' && withoutStar.Equals(current)) { aList.InsertElementAt(i+1, aScope); } else { aList.InsertElementAt(i, aScope); } return; } } aList.AppendElement(aScope); } // aPath can have a '*' at the end, but it is treated literally. /* static */ nsCString ServiceWorkerManager::FindScopeForPath(nsTArray& aList, const nsACString& aPath) { MOZ_ASSERT(aPath.FindChar('*') == -1); nsCString match; for (uint32_t i = 0; i < aList.Length(); ++i) { const nsCString& current = aList[i]; nsCString withoutStar; ScopeWithoutStar(current, withoutStar); if (StringBeginsWith(aPath, withoutStar)) { // If non-pattern match, then check equality. if (current.Last() == '*' || aPath.Equals(current)) { match = current; break; } } } return match; } /* static */ void ServiceWorkerManager::RemoveScope(nsTArray& aList, const nsACString& aScope) { aList.RemoveElement(aScope); } NS_IMETHODIMP ServiceWorkerManager::GetScopeForUrl(const nsAString& aUrl, nsAString& aScope) { nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } nsRefPtr r = GetServiceWorkerRegistration(uri); if (!r) { return NS_ERROR_FAILURE; } aScope = NS_ConvertUTF8toUTF16(r->mScope); return NS_OK; } NS_IMETHODIMP ServiceWorkerManager::CreateServiceWorker(const nsACString& aScriptSpec, const nsACString& aScope, ServiceWorker** aServiceWorker) { AssertIsOnMainThread(); WorkerPrivate::LoadInfo info; nsresult rv = NS_NewURI(getter_AddRefs(info.mBaseURI), aScriptSpec, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } info.mResolvedScriptURI = info.mBaseURI; rv = info.mBaseURI->GetHost(info.mDomain); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // FIXME(nsm): Create correct principal based on app-ness. // Would it make sense to store the nsIPrincipal of the first register() in // the ServiceWorkerRegistration and use that? nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); rv = ssm->GetNoAppCodebasePrincipal(info.mBaseURI, getter_AddRefs(info.mPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } AutoSafeJSContext cx; nsRefPtr serviceWorker; RuntimeService* rs = RuntimeService::GetService(); if (!rs) { return NS_ERROR_FAILURE; } rv = rs->CreateServiceWorkerFromLoadInfo(cx, info, NS_ConvertUTF8toUTF16(aScriptSpec), aScope, getter_AddRefs(serviceWorker)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } serviceWorker.forget(aServiceWorker); return NS_OK; } END_WORKERS_NAMESPACE