/* -*- 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 "mozilla/dom/Promise.h" #include "mozilla/dom/PromiseWorkerProxy.h" #include "ServiceWorkerClient.h" #include "ServiceWorkerClients.h" #include "ServiceWorkerManager.h" #include "ServiceWorkerWindowClient.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "WorkerScope.h" #include "nsIDocShell.h" #include "nsIDOMChromeWindow.h" #include "nsIDOMWindow.h" #include "nsIWebNavigation.h" #include "nsIWebProgress.h" #include "nsIWebProgressListener.h" #include "nsIWindowMediator.h" #include "nsIWindowWatcher.h" #include "nsPIWindowWatcher.h" #include "nsWindowWatcher.h" #include "nsWeakReference.h" using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::dom::workers; NS_IMPL_CYCLE_COLLECTING_ADDREF(ServiceWorkerClients) NS_IMPL_CYCLE_COLLECTING_RELEASE(ServiceWorkerClients) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ServiceWorkerClients, mWorkerScope) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerClients) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END ServiceWorkerClients::ServiceWorkerClients(ServiceWorkerGlobalScope* aWorkerScope) : mWorkerScope(aWorkerScope) { MOZ_ASSERT(mWorkerScope); } JSObject* ServiceWorkerClients::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return ClientsBinding::Wrap(aCx, this, aGivenProto); } namespace { class GetRunnable final : public nsRunnable { class ResolvePromiseWorkerRunnable final : public WorkerRunnable { RefPtr mPromiseProxy; UniquePtr mValue; nsresult mRv; public: ResolvePromiseWorkerRunnable(WorkerPrivate* aWorkerPrivate, PromiseWorkerProxy* aPromiseProxy, UniquePtr&& aValue, nsresult aRv) : WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount), mPromiseProxy(aPromiseProxy), mValue(Move(aValue)), mRv(Move(aRv)) { AssertIsOnMainThread(); } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); Promise* promise = mPromiseProxy->WorkerPromise(); MOZ_ASSERT(promise); if (NS_FAILED(mRv)) { promise->MaybeReject(mRv); } else if (mValue) { RefPtr windowClient = new ServiceWorkerWindowClient(promise->GetParentObject(), *mValue); promise->MaybeResolve(windowClient.get()); } else { promise->MaybeResolve(JS::UndefinedHandleValue); } mPromiseProxy->CleanUp(aCx); return true; } }; RefPtr mPromiseProxy; nsString mClientId; public: GetRunnable(PromiseWorkerProxy* aPromiseProxy, const nsAString& aClientId) : mPromiseProxy(aPromiseProxy), mClientId(aClientId) { } NS_IMETHOD Run() override { AssertIsOnMainThread(); MutexAutoLock lock(mPromiseProxy->Lock()); if (mPromiseProxy->CleanedUp()) { return NS_OK; } WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); MOZ_ASSERT(workerPrivate); RefPtr swm = ServiceWorkerManager::GetInstance(); ErrorResult rv; UniquePtr result = swm->GetClient(workerPrivate->GetPrincipal(), mClientId, rv); RefPtr r = new ResolvePromiseWorkerRunnable(mPromiseProxy->GetWorkerPrivate(), mPromiseProxy, Move(result), rv.StealNSResult()); rv.SuppressException(); AutoJSAPI jsapi; jsapi.Init(); r->Dispatch(jsapi.cx()); return NS_OK; } }; class MatchAllRunnable final : public nsRunnable { class ResolvePromiseWorkerRunnable final : public WorkerRunnable { RefPtr mPromiseProxy; nsTArray mValue; public: ResolvePromiseWorkerRunnable(WorkerPrivate* aWorkerPrivate, PromiseWorkerProxy* aPromiseProxy, nsTArray& aValue) : WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount), mPromiseProxy(aPromiseProxy) { AssertIsOnMainThread(); mValue.SwapElements(aValue); } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); Promise* promise = mPromiseProxy->WorkerPromise(); MOZ_ASSERT(promise); nsTArray> ret; for (size_t i = 0; i < mValue.Length(); i++) { ret.AppendElement(RefPtr( new ServiceWorkerWindowClient(promise->GetParentObject(), mValue.ElementAt(i)))); } promise->MaybeResolve(ret); mPromiseProxy->CleanUp(aCx); return true; } }; RefPtr mPromiseProxy; nsCString mScope; bool mIncludeUncontrolled; public: MatchAllRunnable(PromiseWorkerProxy* aPromiseProxy, const nsCString& aScope, bool aIncludeUncontrolled) : mPromiseProxy(aPromiseProxy), mScope(aScope), mIncludeUncontrolled(aIncludeUncontrolled) { MOZ_ASSERT(mPromiseProxy); } NS_IMETHOD Run() override { AssertIsOnMainThread(); MutexAutoLock lock(mPromiseProxy->Lock()); if (mPromiseProxy->CleanedUp()) { return NS_OK; } RefPtr swm = ServiceWorkerManager::GetInstance(); nsTArray result; swm->GetAllClients(mPromiseProxy->GetWorkerPrivate()->GetPrincipal(), mScope, mIncludeUncontrolled, result); RefPtr r = new ResolvePromiseWorkerRunnable(mPromiseProxy->GetWorkerPrivate(), mPromiseProxy, result); AutoJSAPI jsapi; jsapi.Init(); r->Dispatch(jsapi.cx()); return NS_OK; } }; class ResolveClaimRunnable final : public WorkerRunnable { RefPtr mPromiseProxy; nsresult mResult; public: ResolveClaimRunnable(WorkerPrivate* aWorkerPrivate, PromiseWorkerProxy* aPromiseProxy, nsresult aResult) : WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount) , mPromiseProxy(aPromiseProxy) , mResult(aResult) { AssertIsOnMainThread(); } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); RefPtr promise = mPromiseProxy->WorkerPromise(); MOZ_ASSERT(promise); if (NS_SUCCEEDED(mResult)) { promise->MaybeResolve(JS::UndefinedHandleValue); } else { promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); } mPromiseProxy->CleanUp(aCx); return true; } }; class ClaimRunnable final : public nsRunnable { RefPtr mPromiseProxy; nsCString mScope; uint64_t mServiceWorkerID; public: ClaimRunnable(PromiseWorkerProxy* aPromiseProxy, const nsCString& aScope) : mPromiseProxy(aPromiseProxy) , mScope(aScope) // Safe to call GetWorkerPrivate() since we are being called on the worker // thread via script (so no clean up has occured yet). , mServiceWorkerID(aPromiseProxy->GetWorkerPrivate()->ServiceWorkerID()) { MOZ_ASSERT(aPromiseProxy); } NS_IMETHOD Run() override { MutexAutoLock lock(mPromiseProxy->Lock()); if (mPromiseProxy->CleanedUp()) { return NS_OK; } WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); MOZ_ASSERT(workerPrivate); RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); nsresult rv = swm->ClaimClients(workerPrivate->GetPrincipal(), mScope, mServiceWorkerID); RefPtr r = new ResolveClaimRunnable(workerPrivate, mPromiseProxy, rv); AutoJSAPI jsapi; jsapi.Init(); r->Dispatch(jsapi.cx()); return NS_OK; } }; class ResolveOpenWindowRunnable final : public WorkerRunnable { public: ResolveOpenWindowRunnable(PromiseWorkerProxy* aPromiseProxy, UniquePtr&& aClientInfo, const nsresult aStatus) : WorkerRunnable(aPromiseProxy->GetWorkerPrivate(), WorkerThreadModifyBusyCount) , mPromiseProxy(aPromiseProxy) , mClientInfo(Move(aClientInfo)) , mStatus(aStatus) { AssertIsOnMainThread(); MOZ_ASSERT(aPromiseProxy); } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); Promise* promise = mPromiseProxy->WorkerPromise(); if (NS_WARN_IF(NS_FAILED(mStatus))) { promise->MaybeReject(mStatus); } else if (mClientInfo) { RefPtr client = new ServiceWorkerWindowClient(promise->GetParentObject(), *mClientInfo); promise->MaybeResolve(client); } else { promise->MaybeResolve(JS::NullHandleValue); } mPromiseProxy->CleanUp(aCx); return true; } private: RefPtr mPromiseProxy; UniquePtr mClientInfo; const nsresult mStatus; }; class WebProgressListener final : public nsIWebProgressListener, public nsSupportsWeakReference { public: NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(WebProgressListener, nsIWebProgressListener) WebProgressListener(PromiseWorkerProxy* aPromiseProxy, ServiceWorkerPrivate* aServiceWorkerPrivate, nsPIDOMWindow* aWindow, nsIURI* aBaseURI) : mPromiseProxy(aPromiseProxy) , mServiceWorkerPrivate(aServiceWorkerPrivate) , mWindow(aWindow) , mBaseURI(aBaseURI) { MOZ_ASSERT(aPromiseProxy); MOZ_ASSERT(aServiceWorkerPrivate); MOZ_ASSERT(aWindow); MOZ_ASSERT(aWindow->IsOuterWindow()); MOZ_ASSERT(aBaseURI); AssertIsOnMainThread(); mServiceWorkerPrivate->StoreISupports(static_cast(this)); } NS_IMETHOD OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aStateFlags, nsresult aStatus) override { if (!(aStateFlags & STATE_IS_DOCUMENT) || !(aStateFlags & (STATE_STOP | STATE_TRANSFERRING))) { return NS_OK; } // Our caller keeps a strong reference, so it is safe to remove the listener // from ServiceWorkerPrivate. mServiceWorkerPrivate->RemoveISupports(static_cast(this)); aWebProgress->RemoveProgressListener(this); MutexAutoLock lock(mPromiseProxy->Lock()); if (mPromiseProxy->CleanedUp()) { return NS_OK; } nsCOMPtr doc = mWindow->GetExtantDoc(); UniquePtr clientInfo; if (doc) { // Check same origin. nsCOMPtr securityManager = nsContentUtils::GetSecurityManager(); nsresult rv = securityManager->CheckSameOriginURI(doc->GetOriginalURI(), mBaseURI, false); if (NS_SUCCEEDED(rv)) { clientInfo.reset(new ServiceWorkerClientInfo(doc)); } } RefPtr r = new ResolveOpenWindowRunnable(mPromiseProxy, Move(clientInfo), NS_OK); AutoJSAPI jsapi; jsapi.Init(); JSContext* cx = jsapi.cx(); r->Dispatch(cx); return NS_OK; } NS_IMETHOD OnProgressChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, int32_t aCurSelfProgress, int32_t aMaxSelfProgress, int32_t aCurTotalProgress, int32_t aMaxTotalProgress) override { MOZ_ASSERT(false, "Unexpected notification."); return NS_OK; } NS_IMETHOD OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI* aLocation, uint32_t aFlags) override { MOZ_ASSERT(false, "Unexpected notification."); return NS_OK; } NS_IMETHOD OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsresult aStatus, const char16_t* aMessage) override { MOZ_ASSERT(false, "Unexpected notification."); return NS_OK; } NS_IMETHOD OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aState) override { MOZ_ASSERT(false, "Unexpected notification."); return NS_OK; } private: ~WebProgressListener() { } RefPtr mPromiseProxy; RefPtr mServiceWorkerPrivate; nsCOMPtr mWindow; nsCOMPtr mBaseURI; }; NS_IMPL_CYCLE_COLLECTING_ADDREF(WebProgressListener) NS_IMPL_CYCLE_COLLECTING_RELEASE(WebProgressListener) NS_IMPL_CYCLE_COLLECTION(WebProgressListener, mPromiseProxy, mServiceWorkerPrivate, mWindow) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebProgressListener) NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_END class OpenWindowRunnable final : public nsRunnable { RefPtr mPromiseProxy; nsString mUrl; nsString mScope; public: OpenWindowRunnable(PromiseWorkerProxy* aPromiseProxy, const nsAString& aUrl, const nsAString& aScope) : mPromiseProxy(aPromiseProxy) , mUrl(aUrl) , mScope(aScope) { MOZ_ASSERT(aPromiseProxy); MOZ_ASSERT(aPromiseProxy->GetWorkerPrivate()); aPromiseProxy->GetWorkerPrivate()->AssertIsOnWorkerThread(); } NS_IMETHOD Run() override { AssertIsOnMainThread(); MutexAutoLock lock(mPromiseProxy->Lock()); if (mPromiseProxy->CleanedUp()) { return NS_OK; } nsCOMPtr window; nsresult rv = OpenWindow(getter_AddRefs(window)); if (NS_SUCCEEDED(rv)) { MOZ_ASSERT(window); WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); MOZ_ASSERT(workerPrivate); WorkerPrivate::LocationInfo& info = workerPrivate->GetLocationInfo(); nsCOMPtr baseURI; nsresult rv = NS_NewURI(getter_AddRefs(baseURI), info.mHref); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_FAILURE; } nsCOMPtr docShell = window->GetDocShell(); nsCOMPtr webProgress = do_GetInterface(docShell); if (!webProgress) { return NS_ERROR_FAILURE; } RefPtr swm = ServiceWorkerManager::GetInstance(); MOZ_ASSERT(swm); nsCOMPtr principal = workerPrivate->GetPrincipal(); MOZ_ASSERT(principal); RefPtr registration = swm->GetRegistration(principal, NS_ConvertUTF16toUTF8(mScope)); if (NS_WARN_IF(!registration)) { return NS_ERROR_FAILURE; } RefPtr serviceWorkerInfo = registration->GetServiceWorkerInfoById(workerPrivate->ServiceWorkerID()); if (NS_WARN_IF(!serviceWorkerInfo)) { return NS_ERROR_FAILURE; } nsCOMPtr listener = new WebProgressListener(mPromiseProxy, serviceWorkerInfo->WorkerPrivate(), window, baseURI); rv = webProgress->AddProgressListener(listener, nsIWebProgress::NOTIFY_STATE_DOCUMENT); MOZ_ASSERT(NS_SUCCEEDED(rv)); return NS_OK; } RefPtr resolveRunnable = new ResolveOpenWindowRunnable(mPromiseProxy, nullptr, rv); AutoJSAPI jsapi; jsapi.Init(); NS_WARN_IF(!resolveRunnable->Dispatch(jsapi.cx())); return NS_OK; } private: nsresult OpenWindow(nsPIDOMWindow** aWindow) { WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); // [[1. Let url be the result of parsing url with entry settings object's API // base URL.]] nsCOMPtr uri; WorkerPrivate::LocationInfo& info = workerPrivate->GetLocationInfo(); nsCOMPtr baseURI; nsresult rv = NS_NewURI(getter_AddRefs(baseURI), info.mHref); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_TYPE_ERR; } rv = NS_NewURI(getter_AddRefs(uri), mUrl, nullptr, baseURI); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_ERROR_TYPE_ERR; } // [[6.1 Open Window]] nsCOMPtr wm = do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (XRE_IsContentProcess()) { // ContentProcess nsCOMPtr wwatch = do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr pwwatch(do_QueryInterface(wwatch)); NS_ENSURE_STATE(pwwatch); nsCString spec; uri->GetSpec(spec); nsCOMPtr newWindow; pwwatch->OpenWindow2(nullptr, spec.get(), nullptr, nullptr, false, false, true, nullptr, nullptr, // Not a spammy popup; we got permission, we swear! /* aIsPopupSpam = */ false, // Don't force noopener. We're not passing in an // opener anyway, and we _do_ want the returned // window. /* aForceNoOpener = */ false, getter_AddRefs(newWindow)); nsCOMPtr pwindow = do_QueryInterface(newWindow); pwindow.forget(aWindow); return NS_OK; } // Find the most recent browser window and open a new tab in it. nsCOMPtr browserWindow = nsContentUtils::GetMostRecentNonPBWindow(); if (!browserWindow) { // It is possible to be running without a browser window on Mac OS, so // we need to open a new chrome window. // TODO(catalinb): open new chrome window. Bug 1218080 return NS_ERROR_FAILURE; } nsCOMPtr chromeWin = do_QueryInterface(browserWindow); if (NS_WARN_IF(!chromeWin)) { return NS_ERROR_FAILURE; } nsCOMPtr bwin; chromeWin->GetBrowserDOMWindow(getter_AddRefs(bwin)); if (NS_WARN_IF(!bwin)) { return NS_ERROR_FAILURE; } nsCOMPtr win; rv = bwin->OpenURI(uri, nullptr, nsIBrowserDOMWindow::OPEN_DEFAULTWINDOW, nsIBrowserDOMWindow::OPEN_NEW, getter_AddRefs(win)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } NS_ENSURE_STATE(win); nsCOMPtr pWin = do_QueryInterface(win); pWin = pWin->GetOuterWindow(); pWin.forget(aWindow); return NS_OK; } }; } // namespace already_AddRefed ServiceWorkerClients::Get(const nsAString& aClientId, ErrorResult& aRv) { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); RefPtr promise = Promise::Create(mWorkerScope, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr promiseProxy = PromiseWorkerProxy::Create(workerPrivate, promise); if (!promiseProxy) { promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); return promise.forget(); } RefPtr r = new GetRunnable(promiseProxy, aClientId); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(r))); return promise.forget(); } already_AddRefed ServiceWorkerClients::MatchAll(const ClientQueryOptions& aOptions, ErrorResult& aRv) { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); nsString scope; mWorkerScope->GetScope(scope); if (aOptions.mType != ClientType::Window) { aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return nullptr; } RefPtr promise = Promise::Create(mWorkerScope, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr promiseProxy = PromiseWorkerProxy::Create(workerPrivate, promise); if (!promiseProxy) { promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); return promise.forget(); } RefPtr r = new MatchAllRunnable(promiseProxy, NS_ConvertUTF16toUTF8(scope), aOptions.mIncludeUncontrolled); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(r))); return promise.forget(); } already_AddRefed ServiceWorkerClients::OpenWindow(const nsAString& aUrl, ErrorResult& aRv) { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); RefPtr promise = Promise::Create(mWorkerScope, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } if (aUrl.EqualsLiteral("about:blank")) { promise->MaybeReject(NS_ERROR_TYPE_ERR); return promise.forget(); } // [[4. If this algorithm is not allowed to show a popup ..]] // In Gecko the service worker is allowed to show a popup only if the user // just clicked on a notification. if (!workerPrivate->GlobalScope()->WindowInteractionAllowed()) { promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR); return promise.forget(); } RefPtr promiseProxy = PromiseWorkerProxy::Create(workerPrivate, promise); if (!promiseProxy) { return nullptr; } nsString scope; mWorkerScope->GetScope(scope); RefPtr r = new OpenWindowRunnable(promiseProxy, aUrl, scope); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(r))); return promise.forget(); } already_AddRefed ServiceWorkerClients::Claim(ErrorResult& aRv) { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); RefPtr promise = Promise::Create(mWorkerScope, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr promiseProxy = PromiseWorkerProxy::Create(workerPrivate, promise); if (!promiseProxy) { promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); return promise.forget(); } nsString scope; mWorkerScope->GetScope(scope); RefPtr runnable = new ClaimRunnable(promiseProxy, NS_ConvertUTF16toUTF8(scope)); MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable))); return promise.forget(); }