Bug 1151495 - Support permission prompting from workers for IDB. r=bent, a=lizzard
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 06 May 2015 09:07:57 +0100
changeset 267477 498a842f6d9e5518705ae72a437371be8834d38c
parent 267476 86f093c2d9950478b0911ef386aa567f7080d08d
child 267478 13e298c9841e5d8fdf01f2ef1084b8ba2674ea5c
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbent, lizzard
bugs1151495
milestone39.0
Bug 1151495 - Support permission prompting from workers for IDB. r=bent, a=lizzard
dom/indexedDB/ActorsChild.cpp
dom/indexedDB/test/browser.ini
dom/indexedDB/test/browser_permissionsPromptWorker.js
dom/indexedDB/test/browser_permissionsSharedWorker.html
dom/indexedDB/test/browser_permissionsSharedWorker.js
dom/indexedDB/test/browser_permissionsWorker.html
dom/indexedDB/test/browser_permissionsWorker.js
--- a/dom/indexedDB/ActorsChild.cpp
+++ b/dom/indexedDB/ActorsChild.cpp
@@ -33,16 +33,18 @@
 #include "nsIDOMEvent.h"
 #include "nsIEventTarget.h"
 #include "nsPIDOMWindow.h"
 #include "nsThreadUtils.h"
 #include "nsTraceRefcnt.h"
 #include "PermissionRequestBase.h"
 #include "ProfilerHelpers.h"
 #include "ReportInternalError.h"
+#include "WorkerPrivate.h"
+#include "WorkerRunnable.h"
 
 #ifdef DEBUG
 #include "IndexedDatabaseManager.h"
 #endif
 
 #define GC_ON_IPC_MESSAGES 0
 
 #if defined(DEBUG) || GC_ON_IPC_MESSAGES
@@ -50,17 +52,23 @@
 #include "js/GCAPI.h"
 #include "nsJSEnvironment.h"
 
 #define BUILD_GC_ON_IPC_MESSAGES
 
 #endif // DEBUG || GC_ON_IPC_MESSAGES
 
 namespace mozilla {
+
+using ipc::PrincipalInfo;
+
 namespace dom {
+
+using namespace workers;
+
 namespace indexedDB {
 
 /*******************************************************************************
  * ThreadLocal
  ******************************************************************************/
 
 ThreadLocal::ThreadLocal(const nsID& aBackgroundChildLoggingId)
   : mLoggingInfo(aBackgroundChildLoggingId, 1, -1, 1)
@@ -770,16 +778,245 @@ DispatchSuccessEvent(ResultHelper* aResu
 
   if (transaction &&
       transaction->IsOpen() &&
       internalEvent->mFlags.mExceptionHasBeenRisen) {
     transaction->Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR);
   }
 }
 
+class WorkerPermissionChallenge;
+
+// This class calles WorkerPermissionChallenge::OperationCompleted() in the
+// worker thread.
+class WorkerPermissionOperationCompleted final : public WorkerRunnable
+{
+  nsRefPtr<WorkerPermissionChallenge> mChallenge;
+
+public:
+  WorkerPermissionOperationCompleted(WorkerPrivate* aWorkerPrivate,
+                                     WorkerPermissionChallenge* aChallenge)
+    : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount)
+    , mChallenge(aChallenge)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+  }
+
+  virtual bool
+  WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override;
+};
+
+// This class used to do prompting in the main thread and main process.
+class WorkerPermissionRequest final : public PermissionRequestBase
+{
+  nsRefPtr<WorkerPermissionChallenge> mChallenge;
+
+public:
+  WorkerPermissionRequest(Element* aElement,
+                          nsIPrincipal* aPrincipal,
+                          WorkerPermissionChallenge* aChallenge)
+    : PermissionRequestBase(aElement, aPrincipal)
+    , mChallenge(aChallenge)
+  {
+    MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(aChallenge);
+  }
+
+private:
+  ~WorkerPermissionRequest()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+  }
+
+  virtual void
+  OnPromptComplete(PermissionValue aPermissionValue) override;
+};
+
+// This class is used in the main thread of all child processes.
+class WorkerPermissionRequestChildProcessActor final
+  : public PIndexedDBPermissionRequestChild
+{
+  nsRefPtr<WorkerPermissionChallenge> mChallenge;
+
+public:
+  explicit WorkerPermissionRequestChildProcessActor(
+                                          WorkerPermissionChallenge* aChallenge)
+    : mChallenge(aChallenge)
+  {
+    MOZ_ASSERT(XRE_GetProcessType() != GeckoProcessType_Default);
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(aChallenge);
+  }
+
+protected:
+  ~WorkerPermissionRequestChildProcessActor()
+  {}
+
+  virtual bool
+  Recv__delete__(const uint32_t& aPermission) override;
+};
+
+class WorkerPermissionChallenge final : public nsRunnable
+                                      , public WorkerFeature
+{
+public:
+  WorkerPermissionChallenge(WorkerPrivate* aWorkerPrivate,
+                            BackgroundFactoryRequestChild* aActor,
+                            IDBFactory* aFactory,
+                            const PrincipalInfo& aPrincipalInfo)
+    : mWorkerPrivate(aWorkerPrivate)
+    , mActor(aActor)
+    , mFactory(aFactory)
+    , mPrincipalInfo(aPrincipalInfo)
+  {
+    MOZ_ASSERT(mWorkerPrivate);
+    MOZ_ASSERT(aActor);
+    MOZ_ASSERT(aFactory);
+    mWorkerPrivate->AssertIsOnWorkerThread();
+  }
+
+  NS_IMETHOD
+  Run() override
+  {
+    bool completed = RunInternal();
+    if (completed) {
+      OperationCompleted();
+    }
+
+    return NS_OK;
+  }
+
+  virtual bool
+  Notify(JSContext* aCx, workers::Status aStatus) override
+  {
+    // We don't care about the notification. We just want to keep the
+    // mWorkerPrivate alive.
+    return true;
+  }
+
+  void
+  OperationCompleted()
+  {
+    if (NS_IsMainThread()) {
+      nsRefPtr<WorkerPermissionOperationCompleted> runnable =
+        new WorkerPermissionOperationCompleted(mWorkerPrivate, this);
+
+      if (!runnable->Dispatch(nullptr)) {
+        NS_WARNING("Failed to dispatch a runnable to the worker thread.");
+        return;
+      }
+
+      return;
+    }
+
+    MOZ_ASSERT(mActor);
+    mActor->AssertIsOnOwningThread();
+
+    MaybeCollectGarbageOnIPCMessage();
+
+    nsRefPtr<IDBFactory> factory;
+    mFactory.swap(factory);
+
+    mActor->SendPermissionRetry();
+    mActor = nullptr;
+
+    mWorkerPrivate->AssertIsOnWorkerThread();
+    JSContext* cx = mWorkerPrivate->GetJSContext();
+    mWorkerPrivate->RemoveFeature(cx, this);
+  }
+
+private:
+  bool
+  RunInternal()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    // Walk up to our containing page
+    WorkerPrivate* wp = mWorkerPrivate;
+    while (wp->GetParent()) {
+      wp = wp->GetParent();
+    }
+
+    nsPIDOMWindow* window = wp->GetWindow();
+    if (!window) {
+      return true;
+    }
+
+    nsresult rv;
+    nsCOMPtr<nsIPrincipal> principal =
+      mozilla::ipc::PrincipalInfoToPrincipal(mPrincipalInfo, &rv);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return true;
+    }
+
+    if (XRE_GetProcessType() == GeckoProcessType_Default) {
+      nsCOMPtr<Element> ownerElement =
+        do_QueryInterface(window->GetChromeEventHandler());
+      if (NS_WARN_IF(!ownerElement)) {
+        return true;
+      }
+
+      nsRefPtr<WorkerPermissionRequest> helper =
+        new WorkerPermissionRequest(ownerElement, principal, this);
+
+      PermissionRequestBase::PermissionValue permission;
+      if (NS_WARN_IF(NS_FAILED(helper->PromptIfNeeded(&permission)))) {
+        return true;
+      }
+
+      MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed ||
+                 permission == PermissionRequestBase::kPermissionDenied ||
+                 permission == PermissionRequestBase::kPermissionPrompt);
+
+      return permission != PermissionRequestBase::kPermissionPrompt;
+    }
+
+    TabChild* tabChild = TabChild::GetFrom(window);
+    MOZ_ASSERT(tabChild);
+
+    IPC::Principal ipcPrincipal(principal);
+
+    auto* actor = new WorkerPermissionRequestChildProcessActor(this);
+    tabChild->SendPIndexedDBPermissionRequestConstructor(actor, ipcPrincipal);
+    return false;
+  }
+
+private:
+  WorkerPrivate* mWorkerPrivate;
+  BackgroundFactoryRequestChild* mActor;
+  nsRefPtr<IDBFactory> mFactory;
+  PrincipalInfo mPrincipalInfo;
+};
+
+void
+WorkerPermissionRequest::OnPromptComplete(PermissionValue aPermissionValue)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mChallenge->OperationCompleted();
+}
+
+bool
+WorkerPermissionOperationCompleted::WorkerRun(JSContext* aCx,
+                                              WorkerPrivate* aWorkerPrivate)
+{
+  aWorkerPrivate->AssertIsOnWorkerThread();
+  mChallenge->OperationCompleted();
+  return true;
+}
+
+bool
+WorkerPermissionRequestChildProcessActor::Recv__delete__(
+                                              const uint32_t& /* aPermission */)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mChallenge->OperationCompleted();
+  return true;
+}
+
 } // anonymous namespace
 
 /*******************************************************************************
  * Local class implementations
  ******************************************************************************/
 
 void
 PermissionRequestMainProcessHelper::OnPromptComplete(
@@ -1117,17 +1354,33 @@ bool
 BackgroundFactoryRequestChild::RecvPermissionChallenge(
                                             const PrincipalInfo& aPrincipalInfo)
 {
   AssertIsOnOwningThread();
 
   MaybeCollectGarbageOnIPCMessage();
 
   if (!NS_IsMainThread()) {
-    MOZ_CRASH("Implement me for workers!");
+    WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+    MOZ_ASSERT(workerPrivate);
+    workerPrivate->AssertIsOnWorkerThread();
+
+    nsRefPtr<WorkerPermissionChallenge> challenge =
+      new WorkerPermissionChallenge(workerPrivate, this, mFactory,
+                                    aPrincipalInfo);
+
+    JSContext* cx = workerPrivate->GetJSContext();
+    MOZ_ASSERT(cx);
+
+    if (!workerPrivate->AddFeature(cx, challenge)) {
+      return false;
+    }
+
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(challenge)));
+    return true;
   }
 
   nsresult rv;
   nsCOMPtr<nsIPrincipal> principal =
     mozilla::ipc::PrincipalInfoToPrincipal(aPrincipalInfo, &rv);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return false;
   }
--- a/dom/indexedDB/test/browser.ini
+++ b/dom/indexedDB/test/browser.ini
@@ -1,16 +1,21 @@
 [DEFAULT]
 skip-if = (buildapp != "browser") || e10s
 support-files =
   head.js
   browser_forgetThisSiteAdd.html
   browser_forgetThisSiteGet.html
   browserHelpers.js
   browser_permissionsPrompt.html
+  browser_permissionsSharedWorker.html
+  browser_permissionsSharedWorker.js
+  browser_permissionsWorker.html
+  browser_permissionsWorker.js
   bug839193.js
   bug839193.xul
 
 [browser_forgetThisSite.js]
 [browser_permissionsPromptAllow.js]
 [browser_permissionsPromptDeny.js]
+[browser_permissionsPromptWorker.js]
 [browser_perwindow_privateBrowsing.js]
 [browser_bug839193.js]
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/browser_permissionsPromptWorker.js
@@ -0,0 +1,91 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const testWorkerURL = "http://mochi.test:8888/browser/" +
+  "dom/indexedDB/test/browser_permissionsWorker.html";
+const testSharedWorkerURL = "http://mochi.test:8888/browser/" +
+  "dom/indexedDB/test/browser_permissionsSharedWorker.html";
+const notificationID = "indexedDB-permissions-prompt";
+
+function test()
+{
+  waitForExplicitFinish();
+  executeSoon(test1);
+}
+
+function test1()
+{
+  // We want a prompt.
+  removePermission(testWorkerURL, "indexedDB");
+
+  info("creating tab");
+  gBrowser.selectedTab = gBrowser.addTab();
+
+  gBrowser.selectedBrowser.addEventListener("load", function () {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+
+    setFinishedCallback(function(isIDBDatabase, exception) {
+      ok(isIDBDatabase, "First database creation was successful");
+      ok(!exception, "No exception");
+      is(getPermission(testWorkerURL, "indexedDB"),
+         Components.interfaces.nsIPermissionManager.ALLOW_ACTION,
+         "Correct permission set");
+      gBrowser.removeCurrentTab();
+      executeSoon(test2);
+    });
+
+    registerPopupEventHandler("popupshowing", function () {
+      ok(true, "prompt showing");
+    });
+    registerPopupEventHandler("popupshown", function () {
+      ok(true, "prompt shown");
+      triggerMainCommand(this);
+    });
+    registerPopupEventHandler("popuphidden", function () {
+      ok(true, "prompt hidden");
+    });
+
+  }, true);
+
+  info("loading test page: " + testWorkerURL);
+  content.location = testWorkerURL;
+}
+
+function test2()
+{
+  // We want a prompt.
+  removePermission(testSharedWorkerURL, "indexedDB");
+
+  info("creating tab");
+  gBrowser.selectedTab = gBrowser.addTab();
+
+  gBrowser.selectedBrowser.addEventListener("load", function () {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+
+    setFinishedCallback(function(isIDBDatabase, exception) {
+      ok(!isIDBDatabase, "First database creation was successful");
+      ok(exception, "No exception");
+      is(getPermission(testSharedWorkerURL, "indexedDB"),
+         Components.interfaces.nsIPermissionManager.UNKNOWN_ACTION,
+         "Correct permission set");
+      gBrowser.removeCurrentTab();
+      executeSoon(finish);
+    });
+
+    registerPopupEventHandler("popupshowing", function () {
+      ok(false, "prompt showing");
+    });
+    registerPopupEventHandler("popupshown", function () {
+      ok(false, "prompt shown");
+    });
+    registerPopupEventHandler("popuphidden", function () {
+      ok(false, "prompt hidden");
+    });
+
+  }, true);
+
+  info("loading test page: " + testSharedWorkerURL);
+  content.location = testSharedWorkerURL;
+}
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/browser_permissionsSharedWorker.html
@@ -0,0 +1,34 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+  <head>
+    <title>Indexed Database Test</title>
+
+    <script type="text/javascript;version=1.7">
+    let testIsIDBDatabase;
+    let testException;
+
+    function runTest() {
+      let w = new SharedWorker('browser_permissionsSharedWorker.js');
+      w.port.onmessage = function(e) {
+        if (e.data.status == 'success') {
+          testIsIDBDatabase = e.data.isIDBDatabase;
+        } else {
+          testException = e.data.error;
+        }
+
+        setTimeout(testFinishedCallback, 0, testIsIDBDatabase, testException);
+      }
+
+      const name = window.location.pathname + "_sharedWorker";
+      w.port.postMessage(name);
+    }
+    </script>
+
+  </head>
+
+  <body onload="runTest();"></body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/browser_permissionsSharedWorker.js
@@ -0,0 +1,14 @@
+onconnect = function(e) {
+  e.ports[0].onmessage = function(e) {
+    var request = indexedDB.open(e.data, { version: 1,
+                                           storage: "persistent" });
+    request.onsuccess = function(event) {
+      e.target.postMessage({ status: 'success',
+                             isIDBDatabase: (event.target.result instanceof IDBDatabase) });
+    }
+
+    request.onerror = function(event) {
+      e.target.postMessage({ status: 'error', error: event.target.error.name });
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/browser_permissionsWorker.html
@@ -0,0 +1,34 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+  <head>
+    <title>Indexed Database Test</title>
+
+    <script type="text/javascript;version=1.7">
+    let testIsIDBDatabase;
+    let testException;
+
+    function runTest() {
+      let w = new Worker('browser_permissionsWorker.js');
+      w.onmessage = function(e) {
+        if (e.data.status == 'success') {
+          testIsIDBDatabase = e.data.isIDBDatabase;
+        } else {
+          testException = e.data.error;
+        }
+
+        setTimeout(testFinishedCallback, 0, testIsIDBDatabase, testException);
+      }
+
+      const name = window.location.pathname;
+      w.postMessage(name);
+    }
+    </script>
+
+  </head>
+
+  <body onload="runTest();"></body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/browser_permissionsWorker.js
@@ -0,0 +1,12 @@
+onmessage = function(e) {
+  var request = indexedDB.open(e.data, { version: 1,
+                                         storage: "persistent" });
+  request.onsuccess = function(event) {
+    postMessage({ status: 'success',
+                  isIDBDatabase: (event.target.result instanceof IDBDatabase) });
+  }
+
+  request.onerror = function(event) {
+    postMessage({ status: 'error', error: event.target.error.name });
+  }
+}