Bug 1172870 - Implement service workers clients.openWindow for desktop (non-e10s). r=baku,smaug
authorCatalin Badea <catalin.badea392@gmail.com>
Sat, 24 Oct 2015 15:16:23 +0300
changeset 304556 65dd5a0d18aed3346670d8e3b0f2db60c7fbf950
parent 304555 a8adf1f9d366b936e1f25dbd3deb7a83b8cfa0cd
child 304557 a59b9742c81e77a54ee8c7e726f45113553d190d
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, smaug
bugs1172870
milestone44.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1172870 - Implement service workers clients.openWindow for desktop (non-e10s). r=baku,smaug Based on patches from: Nikhil Marathe <nsm.nikhil@gmail.com> Alberto Crespell Perez <alberto.crespell@gmail.com>
dom/webidl/Clients.webidl
dom/workers/ServiceWorkerClients.cpp
dom/workers/ServiceWorkerClients.h
dom/workers/ServiceWorkerManager.cpp
dom/workers/ServiceWorkerManager.h
dom/workers/ServiceWorkerPrivate.cpp
dom/workers/ServiceWorkerPrivate.h
dom/workers/test/serviceworkers/mochitest.ini
dom/workers/test/serviceworkers/openWindow_worker.js
dom/workers/test/serviceworkers/redirect.sjs
dom/workers/test/serviceworkers/test_openWindow.html
--- a/dom/webidl/Clients.webidl
+++ b/dom/webidl/Clients.webidl
@@ -8,16 +8,17 @@
  *
  */
 
 [Exposed=ServiceWorker]
 interface Clients {
   // The objects returned will be new instances every time
   [Throws]
   Promise<sequence<Client>?> matchAll(optional ClientQueryOptions options);
+  [Throws]
   Promise<WindowClient> openWindow(USVString url);
   [Throws]
   Promise<void> claim();
 };
 
 dictionary ClientQueryOptions {
   boolean includeUncontrolled = false;
   ClientType type = "window";
--- a/dom/workers/ServiceWorkerClients.cpp
+++ b/dom/workers/ServiceWorkerClients.cpp
@@ -11,16 +11,25 @@
 #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 "nsIWindowMediator.h"
+#include "nsIWebProgress.h"
+#include "nsIWebProgressListener.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)
 
@@ -194,16 +203,347 @@ public:
 
     AutoJSAPI jsapi;
     jsapi.Init();
     r->Dispatch(jsapi.cx());
     return NS_OK;
   }
 };
 
+class ResolveOpenWindowRunnable final : public WorkerRunnable
+{
+public:
+  ResolveOpenWindowRunnable(PromiseWorkerProxy* aPromiseProxy,
+                            UniquePtr<ServiceWorkerClientInfo>&& 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<ServiceWorkerWindowClient> client =
+        new ServiceWorkerWindowClient(promise->GetParentObject(),
+                                      *mClientInfo);
+      promise->MaybeResolve(client);
+    } else {
+      promise->MaybeResolve(JS::NullHandleValue);
+    }
+
+    mPromiseProxy->CleanUp(aCx);
+    return true;
+  }
+
+private:
+  RefPtr<PromiseWorkerProxy> mPromiseProxy;
+  UniquePtr<ServiceWorkerClientInfo> 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<nsIWebProgressListener*>(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<nsIWebProgressListener*>(this));
+    aWebProgress->RemoveProgressListener(this);
+
+    MutexAutoLock lock(mPromiseProxy->Lock());
+    if (mPromiseProxy->CleanedUp()) {
+      return NS_OK;
+    }
+
+    nsCOMPtr<nsIDocument> doc = mWindow->GetExtantDoc();
+    UniquePtr<ServiceWorkerClientInfo> clientInfo;
+    if (doc) {
+      // Check same origin.
+      nsCOMPtr<nsIScriptSecurityManager> securityManager =
+        nsContentUtils::GetSecurityManager();
+      nsresult rv = securityManager->CheckSameOriginURI(doc->GetOriginalURI(),
+                                                        mBaseURI, false);
+      if (NS_SUCCEEDED(rv)) {
+        clientInfo.reset(new ServiceWorkerClientInfo(doc));
+      }
+    }
+
+    RefPtr<ResolveOpenWindowRunnable> 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<PromiseWorkerProxy> mPromiseProxy;
+  RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate;
+  nsCOMPtr<nsPIDOMWindow> mWindow;
+  nsCOMPtr<nsIURI> 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<PromiseWorkerProxy> 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<nsPIDOMWindow> 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<nsIURI> baseURI;
+      nsresult rv = NS_NewURI(getter_AddRefs(baseURI), info.mOrigin);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return NS_ERROR_FAILURE;
+      }
+
+      nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
+      nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell);
+
+      if (!webProgress) {
+        return NS_ERROR_FAILURE;
+      }
+
+      RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+      MOZ_ASSERT(swm);
+
+      nsCOMPtr<nsIPrincipal> principal = workerPrivate->GetPrincipal();
+      MOZ_ASSERT(principal);
+      RefPtr<ServiceWorkerRegistrationInfo> registration =
+        swm->GetRegistration(principal, NS_ConvertUTF16toUTF8(mScope));
+      if (NS_WARN_IF(!registration)) {
+        return NS_ERROR_FAILURE;
+      }
+      RefPtr<ServiceWorkerInfo> serviceWorkerInfo =
+        registration->GetServiceWorkerInfoById(workerPrivate->ServiceWorkerID());
+      if (NS_WARN_IF(!serviceWorkerInfo)) {
+        return NS_ERROR_FAILURE;
+      }
+
+      nsCOMPtr<nsIWebProgressListener> 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<ResolveOpenWindowRunnable> 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<nsIURI> uri;
+    WorkerPrivate::LocationInfo& info = workerPrivate->GetLocationInfo();
+
+    nsCOMPtr<nsIURI> baseURI;
+    nsresult rv = NS_NewURI(getter_AddRefs(baseURI), info.mOrigin);
+    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<nsIWindowMediator> wm = do_GetService(NS_WINDOWMEDIATOR_CONTRACTID,
+                                                   &rv);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    // Find the most recent browser window and open a new tab in it.
+    nsCOMPtr<nsIDOMWindow> browserWindow;
+    rv = wm->GetMostRecentWindow(MOZ_UTF16("navigator:browser"),
+                                 getter_AddRefs(browserWindow));
+    if (NS_WARN_IF(NS_FAILED(rv)) || !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<nsIDOMChromeWindow> chromeWin = do_QueryInterface(browserWindow);
+    if (NS_WARN_IF(!chromeWin)) {
+      return NS_ERROR_FAILURE;
+    }
+
+    nsCOMPtr<nsIBrowserDOMWindow> bwin;
+    chromeWin->GetBrowserDOMWindow(getter_AddRefs(bwin));
+
+    if (NS_WARN_IF(!bwin)) {
+      return NS_ERROR_FAILURE;
+    }
+
+    nsCOMPtr<nsIDOMWindow> 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<nsPIDOMWindow> pWin = do_QueryInterface(win);
+    pWin = pWin->GetOuterWindow();
+    pWin.forget(aWindow);
+
+    return NS_OK;
+  }
+};
+
 } // namespace
 
 already_AddRefed<Promise>
 ServiceWorkerClients::MatchAll(const ClientQueryOptions& aOptions,
                                ErrorResult& aRv)
 {
   WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
   MOZ_ASSERT(workerPrivate);
@@ -232,25 +572,61 @@ ServiceWorkerClients::MatchAll(const Cli
   RefPtr<MatchAllRunnable> r =
     new MatchAllRunnable(promiseProxy,
                          NS_ConvertUTF16toUTF8(scope));
   MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(r)));
   return promise.forget();
 }
 
 already_AddRefed<Promise>
-ServiceWorkerClients::OpenWindow(const nsAString& aUrl)
+ServiceWorkerClients::OpenWindow(const nsAString& aUrl,
+                                 ErrorResult& aRv)
 {
-  ErrorResult result;
-  RefPtr<Promise> promise = Promise::Create(mWorkerScope, result);
-  if (NS_WARN_IF(result.Failed())) {
+  // XXXcatalinb: This works only on non-multiprocess for now, bail if we're
+  // running in a content process.
+  if (XRE_IsContentProcess()) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return nullptr;
+  }
+
+  WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+  MOZ_ASSERT(workerPrivate);
+
+  RefPtr<Promise> promise = Promise::Create(mWorkerScope, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
-  promise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
+  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<PromiseWorkerProxy> promiseProxy =
+    PromiseWorkerProxy::Create(workerPrivate, promise);
+
+  if (!promiseProxy) {
+    return nullptr;
+  }
+
+  nsString scope;
+  mWorkerScope->GetScope(scope);
+
+  RefPtr<OpenWindowRunnable> r = new OpenWindowRunnable(promiseProxy,
+                                                          aUrl, scope);
+  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(r)));
+
   return promise.forget();
 }
 
 already_AddRefed<Promise>
 ServiceWorkerClients::Claim(ErrorResult& aRv)
 {
   WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
   MOZ_ASSERT(workerPrivate);
--- a/dom/workers/ServiceWorkerClients.h
+++ b/dom/workers/ServiceWorkerClients.h
@@ -28,17 +28,17 @@ public:
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ServiceWorkerClients)
 
   explicit ServiceWorkerClients(ServiceWorkerGlobalScope* aWorkerScope);
 
   already_AddRefed<Promise>
   MatchAll(const ClientQueryOptions& aOptions, ErrorResult& aRv);
 
   already_AddRefed<Promise>
-  OpenWindow(const nsAString& aUrl);
+  OpenWindow(const nsAString& aUrl, ErrorResult& aRv);
 
   already_AddRefed<Promise>
   Claim(ErrorResult& aRv);
 
   JSObject*
   WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   ServiceWorkerGlobalScope*
--- a/dom/workers/ServiceWorkerManager.cpp
+++ b/dom/workers/ServiceWorkerManager.cpp
@@ -377,16 +377,31 @@ ServiceWorkerRegistrationInfo::ServiceWo
 
 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.");
   }
 }
 
+already_AddRefed<ServiceWorkerInfo>
+ServiceWorkerRegistrationInfo::GetServiceWorkerInfoById(uint64_t aId)
+{
+  RefPtr<ServiceWorkerInfo> 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)
--- a/dom/workers/ServiceWorkerManager.h
+++ b/dom/workers/ServiceWorkerManager.h
@@ -91,16 +91,19 @@ public:
       newest = mWaitingWorker;
     } else {
       newest = mActiveWorker;
     }
 
     return newest.forget();
   }
 
+  already_AddRefed<ServiceWorkerInfo>
+  GetServiceWorkerInfoById(uint64_t aId);
+
   void
   StartControllingADocument()
   {
     ++mControlledDocumentsCounter;
   }
 
   void
   StopControllingADocument()
--- a/dom/workers/ServiceWorkerPrivate.cpp
+++ b/dom/workers/ServiceWorkerPrivate.cpp
@@ -8,17 +8,23 @@
 
 #include "ServiceWorkerManager.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 BEGIN_WORKERS_NAMESPACE
 
-NS_IMPL_ISUPPORTS0(ServiceWorkerPrivate)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ServiceWorkerPrivate)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ServiceWorkerPrivate)
+NS_IMPL_CYCLE_COLLECTION(ServiceWorkerPrivate, mSupportsArray)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerPrivate)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
 
 // Tracks the "dom.disable_open_click_delay" preference.  Modified on main
 // thread, read on worker threads.
 // It is updated every time a "notificationclick" event is dispatched. While
 // this is done without synchronization, at the worst, the thread will just get
 // an older value within which a popup is allowed to be displayed, which will
 // still be a valid value since it was set prior to dispatching the runnable.
 Atomic<uint32_t> gDOMDisableOpenClickDelay(0);
@@ -62,16 +68,17 @@ ServiceWorkerPrivate::ServiceWorkerPriva
   MOZ_ASSERT(mIdleWorkerTimer);
 }
 
 ServiceWorkerPrivate::~ServiceWorkerPrivate()
 {
   MOZ_ASSERT(!mWorkerPrivate);
   MOZ_ASSERT(!mTokenCount);
   MOZ_ASSERT(!mInfo);
+  MOZ_ASSERT(mSupportsArray.IsEmpty());
 
   mIdleWorkerTimer->Cancel();
 }
 
 nsresult
 ServiceWorkerPrivate::SendMessageEvent(JSContext* aCx,
                                        JS::Handle<JS::Value> aMessage,
                                        const Optional<Sequence<JS::Value>>& aTransferable,
@@ -1144,16 +1151,20 @@ ServiceWorkerPrivate::SpawnWorkerIfNeede
 
   if (mWorkerPrivate) {
     mWorkerPrivate->UpdateOverridenLoadGroup(aLoadGroup);
     ResetIdleTimeout(aWhy);
 
     return NS_OK;
   }
 
+  // Sanity check: mSupportsArray should be empty if we're about to
+  // spin up a new worker.
+  MOZ_ASSERT(mSupportsArray.IsEmpty());
+
   if (NS_WARN_IF(!mInfo)) {
     NS_WARNING("Trying to wake up a dead service worker.");
     return NS_ERROR_FAILURE;
   }
 
   // TODO(catalinb): Bug 1192138 - Add telemetry for service worker wake-ups.
 
   // Ensure that the IndexedDatabaseManager is initialized
@@ -1221,16 +1232,33 @@ ServiceWorkerPrivate::SpawnWorkerIfNeede
 
   mIsPushWorker = false;
   ResetIdleTimeout(aWhy);
 
   return NS_OK;
 }
 
 void
+ServiceWorkerPrivate::StoreISupports(nsISupports* aSupports)
+{
+  AssertIsOnMainThread();
+  MOZ_ASSERT(mWorkerPrivate);
+  MOZ_ASSERT(!mSupportsArray.Contains(aSupports));
+
+  mSupportsArray.AppendElement(aSupports);
+}
+
+void
+ServiceWorkerPrivate::RemoveISupports(nsISupports* aSupports)
+{
+  AssertIsOnMainThread();
+  mSupportsArray.RemoveElement(aSupports);
+}
+
+void
 ServiceWorkerPrivate::TerminateWorker()
 {
   AssertIsOnMainThread();
 
   mIdleWorkerTimer->Cancel();
   mKeepAliveToken = nullptr;
   if (mWorkerPrivate) {
     if (Preferences::GetBool("dom.serviceWorkers.testing.enabled")) {
@@ -1239,16 +1267,17 @@ ServiceWorkerPrivate::TerminateWorker()
         os->NotifyObservers(this, "service-worker-shutdown", nullptr);
       }
     }
 
     AutoJSAPI jsapi;
     jsapi.Init();
     NS_WARN_IF(!mWorkerPrivate->Terminate(jsapi.cx()));
     mWorkerPrivate = nullptr;
+    mSupportsArray.Clear();
   }
 }
 
 void
 ServiceWorkerPrivate::NoteDeadServiceWorkerInfo()
 {
   AssertIsOnMainThread();
   mInfo = nullptr;
--- a/dom/workers/ServiceWorkerPrivate.h
+++ b/dom/workers/ServiceWorkerPrivate.h
@@ -56,17 +56,18 @@ public:
 // with an appropriate reason before any runnable is dispatched to the worker.
 // If the event is extendable then the runnable should inherit
 // ExtendableEventWorkerRunnable.
 class ServiceWorkerPrivate final : public nsISupports
 {
   friend class KeepAliveToken;
 
 public:
-  NS_DECL_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS(ServiceWorkerPrivate)
 
   explicit ServiceWorkerPrivate(ServiceWorkerInfo* aInfo);
 
   nsresult
   SendMessageEvent(JSContext* aCx, JS::Handle<JS::Value> aMessage,
                    const Optional<Sequence<JS::Value>>& aTransferable,
                    UniquePtr<ServiceWorkerClientInfo>&& aClientInfo);
 
@@ -101,16 +102,22 @@ public:
                              const nsAString& aScope);
 
   nsresult
   SendFetchEvent(nsIInterceptedChannel* aChannel,
                  nsILoadGroup* aLoadGroup,
                  UniquePtr<ServiceWorkerClientInfo>&& aClientInfo,
                  bool aIsReload);
 
+  void
+  StoreISupports(nsISupports* aSupports);
+
+  void
+  RemoveISupports(nsISupports* aSupports);
+
   // This will terminate the current running worker thread and drop the
   // workerPrivate reference.
   // Called by ServiceWorkerInfo when [[Clear Registration]] is invoked
   // or whenever the spec mandates that we terminate the worker.
   // This is a no-op if the worker has already been stopped.
   void
   TerminateWorker();
 
@@ -172,15 +179,21 @@ private:
   // is created.
   bool mIsPushWorker;
 
   // We keep a token for |dom.serviceWorkers.idle_timeout| seconds to give the
   // worker a grace period after each event.
   RefPtr<KeepAliveToken> mKeepAliveToken;
 
   uint64_t mTokenCount;
+
+  // Meant for keeping objects alive while handling requests from the worker
+  // on the main thread. Access to this array is provided through
+  // |StoreISupports| and |RemoveISupports|. Note that the array is also
+  // cleared whenever the worker is terminated.
+  nsTArray<nsCOMPtr<nsISupports>> mSupportsArray;
 };
 
 } // namespace workers
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_workers_serviceworkerprivate_h
--- a/dom/workers/test/serviceworkers/mochitest.ini
+++ b/dom/workers/test/serviceworkers/mochitest.ini
@@ -179,16 +179,18 @@ support-files =
   fetch/plugin/plugins.html
   eventsource/*
   sw_clients/file_blob_upload_frame.html
   redirect_post.sjs
   xslt_worker.js
   xslt/*
   unresolved_fetch_worker.js
   header_checker.sjs
+  openWindow_worker.js
+  redirect.sjs
 
 [test_app_protocol.html]
 skip-if = release_build
 [test_bug1151916.html]
 [test_claim.html]
 [test_claim_fetch.html]
 [test_claim_oninstall.html]
 [test_close.html]
@@ -278,8 +280,10 @@ skip-if = toolkit == "android" || toolki
 [test_not_intercept_plugin.html]
 [test_file_blob_upload.html]
 [test_unresolved_fetch_interception.html]
 [test_hsts_upgrade_intercept.html]
 skip-if = e10s # Bug 1214305
 [test_csp_upgrade-insecure_intercept.html]
 skip-if = e10s # Bug 1214305
 [test_serviceworker_header.html]
+[test_openWindow.html]
+skip-if = toolkit == "android" || toolkit == "gonk" || e10s
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/openWindow_worker.js
@@ -0,0 +1,90 @@
+// the worker won't shut down between events because we increased
+// the timeout values.
+var client;
+
+function testForUrl(url, throwType, clientProperties, resultsArray) {
+  return clients.openWindow(url)
+    .then(function(e) {
+      if (throwType != null) {
+        resultsArray.push({
+          result: false,
+          message: "openWindow should throw " + throwType
+        });
+      } else if (clientProperties) {
+        resultsArray.push({
+          result: (e instanceof WindowClient),
+          message: "openWindow should resolve to a WindowClient"
+        });
+        resultsArray.push({
+          result: e.url == clientProperties.url,
+          message: "Client url should be " + clientProperties.url
+        });
+        // Add more properties
+      } else {
+        resultsArray.push({
+          result: e == null,
+          message: "Open window should resolve to null. Got: " + e
+        });
+      }
+    })
+    .catch(function(err) {
+      if (throwType == null) {
+        resultsArray.push({
+          result: false,
+          message: "Unexpected throw: " + err
+        });
+      } else {
+        resultsArray.push({
+          result: err.toString().indexOf(throwType) >= 0,
+          message: "openWindow should throw: " + err
+        });
+      }
+    })
+}
+
+onmessage = function(event) {
+  client = event.source;
+
+  var results = [];
+  var promises = [];
+  promises.push(testForUrl("about:blank", "TypeError", null, results));
+  promises.push(testForUrl("http://example.com", "InvalidAccessError", null, results));
+  promises.push(testForUrl("_._*`InvalidURL", "InvalidAccessError", null, results));
+  Promise.all(promises).then(function(e) {
+    client.postMessage(results);
+  });
+}
+
+onnotificationclick = function(e) {
+  var results = [];
+  var promises = [];
+
+  promises.push(testForUrl("about:blank", "TypeError", null, results));
+  promises.push(testForUrl("http://example.com", null, null, results));
+  promises.push(testForUrl("http://mochi.test:8888/same_origin.html", null,
+                           {url: "http://mochi.test:8888/same_origin.html"}, results));
+
+  // redirect tests
+  var redirect = "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/redirect.sjs?"
+  var baseURL = "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/"
+  promises.push(testForUrl(redirect + "same_origin_redirect.html", null,
+			   {url: baseURL + "same_origin_redirect.html"}, results));
+  promises.push(testForUrl(redirect + "http://example.com/redirect_to_other_origin.html", null,
+			   null, results));
+
+  var redirect_xorigin = "http://example.com/tests/dom/workers/test/serviceworkers/redirect.sjs?"
+  promises.push(testForUrl(redirect_xorigin + "xorigin_redirect.html", null,
+			   null, results));
+  promises.push(testForUrl(redirect_xorigin + "http://mochi.test:8888/xorigin_to_same_origin.html", null,
+			   {url: "http://mochi.test:8888/xorigin_to_same_origin.html"}, results));
+
+  Promise.all(promises).then(function(e) {
+    client.postMessage(results);
+  });
+}
+
+onfetch = function(e) {
+  if (e.request.url.indexOf(same_origin) >= 0) {
+    e.respondWith(new Response("same_origin_window"));
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/redirect.sjs
@@ -0,0 +1,5 @@
+function handleRequest(request, response)
+{
+  response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+  response.setHeader("Location", request.queryString, false);
+}
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/test_openWindow.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1172870
+-->
+<head>
+  <title>Bug 1172870 - Test clients.openWindow</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script>
+  <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1172870">Bug 1172870</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<script type="text/javascript">
+  SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events.");
+
+  function setup(ctx) {
+    MockServices.register();
+
+    return navigator.serviceWorker.register("openWindow_worker.js", {scope: "./"})
+      .then(function(swr) {
+        ok(swr, "Registration successful");
+        ctx.registration = swr;
+        return ctx;
+      });
+  }
+
+  function waitForActiveServiceWorker(ctx) {
+    return navigator.serviceWorker.ready.then(function(result) {
+      ok(ctx.registration.active, "Service Worker is active");
+      return ctx;
+    });
+  }
+
+  function setupMessageHandler(ctx) {
+    return new Promise(function(res, rej) {
+      navigator.serviceWorker.onmessage = function(event) {
+        navigator.serviceWorker.onmessage = null;
+        for (i = 0; i < event.data.length; i++) {
+          ok(event.data[i].result, event.data[i].message);
+        }
+        res(ctx);
+      }
+    });
+  }
+
+  function testPopupNotAllowed(ctx) {
+    var p = setupMessageHandler(ctx);
+    ok(ctx.registration.active, "Worker is active.");
+    ctx.registration.active.postMessage("testNoPopup");
+
+    return p;
+  }
+
+  function testPopupAllowed(ctx) {
+    var p = setupMessageHandler(ctx);
+    ctx.registration.showNotification("testPopup");
+
+    return p;
+  }
+
+  function clear(ctx) {
+    MockServices.unregister();
+
+    var browser = SpecialPowers.Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
+
+    ok(browser.tabs.length == 7, "Total number of tabs is correct.");
+    while (browser.tabs.length > 1) {
+      browser.removeTab(browser.tabs[1]);
+    }
+
+    return ctx.registration.unregister().then(function(result) {
+      ctx.registration = null;
+      ok(result, "Unregister was successful.");
+    });
+  }
+
+  function runTest() {
+    setup({})
+      .then(waitForActiveServiceWorker)
+      // Permission to allow popups persists for some time after a notification
+      // click event, so the order here is important.
+      .then(testPopupNotAllowed)
+      .then(testPopupAllowed)
+      .then(clear)
+      .catch(function(e) {
+        ok(false, "Some test failed with error " + e);
+      }).then(SimpleTest.finish);
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  SpecialPowers.pushPrefEnv({"set": [
+    ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+    ["dom.serviceWorkers.enabled", true],
+    ["dom.serviceWorkers.testing.enabled", true],
+    ["dom.webnotifications.workers.enabled", true],
+    ["dom.webnotifications.serviceworker.enabled", true],
+    ["notification.prompt.testing", true],
+    ["dom.disable_open_click_delay", 1000],
+    ["dom.serviceWorkers.idle_timeout", 299999],
+    ["dom.serviceWorkers.idle_extended_timeout", 299999]
+  ]}, runTest);
+</script>
+</body>
+</html>