Bug 1205109 - Make `pushsubscriptionchange` extendable. r=mt,nsm
authorKit Cambridge <kcambridge@mozilla.com>
Thu, 17 Sep 2015 05:15:45 -0700
changeset 295995 02d5b9e9a3486b5dfcfa4db774db0aae5307e211
parent 295994 9fafe1d5f32c636106e3adf3d9dd17e41779f7b0
child 295996 a0afac66eb9808b55a87c3015e069b17911e414c
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmt, nsm
bugs1205109
milestone43.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 1205109 - Make `pushsubscriptionchange` extendable. r=mt,nsm
dom/push/PushService.jsm
dom/push/test/frame.html
dom/push/test/mochitest.ini
dom/push/test/test_data.html
dom/push/test/test_register.html
dom/push/test/test_subscription_change.html
dom/push/test/worker.js
dom/workers/ServiceWorkerManager.cpp
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -39,17 +39,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 this.EXPORTED_SYMBOLS = ["PushService"];
 
 const prefs = new Preferences("dom.push.");
 // Set debug first so that all debugging actually works.
 gDebuggingEnabled = prefs.get("debug");
 
 const kCHILD_PROCESS_MESSAGES = ["Push:Register", "Push:Unregister",
                                  "Push:Registration", "Push:RegisterEventNotificationListener",
-                                 "child-process-shutdown"];
+                                 "Push:Reset", "child-process-shutdown"];
 
 const PUSH_SERVICE_UNINIT = 0;
 const PUSH_SERVICE_INIT = 1; // No serverURI
 const PUSH_SERVICE_ACTIVATING = 2;//activating db
 const PUSH_SERVICE_CONNECTION_DISABLE = 3;
 const PUSH_SERVICE_ACTIVE_OFFLINE = 4;
 const PUSH_SERVICE_RUNNING = 5;
 
@@ -1042,44 +1042,58 @@ this.PushService = {
       return;
     }
 
     if (!aMessage.target.assertPermission("push")) {
       debug("Got message from a child process that does not have 'push' permission.");
       return null;
     }
 
+    let name = aMessage.name.slice("Push:".length);
     let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender);
     let pageRecord = aMessage.data;
 
     let principal = aMessage.principal;
     if (!principal) {
       debug("No principal passed!");
       let message = {
         requestID: pageRecord.requestID,
         error: "SecurityError"
       };
-      mm.sendAsyncMessage("PushService:Register:KO", message);
+      mm.sendAsyncMessage("PushService:" + name + ":KO", message);
       return;
     }
 
     pageRecord.originAttributes =
       ChromeUtils.originAttributesToSuffix(principal.originAttributes);
 
     if (!pageRecord.scope || pageRecord.originAttributes === undefined) {
       debug("Incorrect identifier values set! " + JSON.stringify(pageRecord));
       let message = {
         requestID: pageRecord.requestID,
         error: "SecurityError"
       };
-      mm.sendAsyncMessage("PushService:Register:KO", message);
+      mm.sendAsyncMessage("PushService:" + name + ":KO", message);
       return;
     }
 
-    this[aMessage.name.slice("Push:".length).toLowerCase()](pageRecord, mm);
+    this[name.toLowerCase()](pageRecord, mm);
+  },
+
+  reset: function(aPageRecord, aMessageManager) {
+    this.dropRegistrations().then(() => {
+      aMessageManager.sendAsyncMessage("PushService:Reset:OK", {
+        requestID: aPageRecord.requestID,
+      });
+    }, error => {
+      aMessageManager.sendAsyncMessage("PushService:Reset:KO", {
+        requestID: aPageRecord.requestID,
+        error: error,
+      });
+    });
   },
 
   register: function(aPageRecord, aMessageManager) {
     debug("register(): " + JSON.stringify(aPageRecord));
 
     this._register(aPageRecord)
       .then(record => {
         let message = record.toRegister();
--- a/dom/push/test/frame.html
+++ b/dom/push/test/frame.html
@@ -1,23 +1,24 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
   <script>
 
-
-    function waitOnPushMessage(pushSubscription)
+    function waitOnWorkerMessage(type)
     {
       var p = new Promise(function(res, rej) {
-        navigator.serviceWorker.onmessage = function(e) {
-          if (e.data.type == "finished") {
+        function onMessage(e) {
+          if (e.data.type == type) {
+            navigator.serviceWorker.removeEventListener("message", onMessage);
             (e.data.okay == "yes" ? res : rej)(e.data);
           }
-        };
+        }
+        navigator.serviceWorker.addEventListener("message", onMessage);
       });
       return p;
     }
 
 
   </script>
 </head>
 <body>
--- a/dom/push/test/mochitest.ini
+++ b/dom/push/test/mochitest.ini
@@ -15,13 +15,15 @@ skip-if = os == "android" || toolkit == 
 [test_multiple_register.html]
 skip-if = os == "android" || toolkit == "gonk"
 [test_multiple_register_during_service_activation.html]
 skip-if = os == "android" || toolkit == "gonk"
 [test_unregister.html]
 skip-if = os == "android" || toolkit == "gonk"
 [test_multiple_register_different_scope.html]
 skip-if = os == "android" || toolkit == "gonk"
+[test_subscription_change.html]
+skip-if = os == "android" || toolkit == "gonk"
 [test_data.html]
 skip-if = os == "android" || toolkit == "gonk"
 # Disabled for too many intermittent failures (bug 1164432)
 #  [test_try_registering_offline_disabled.html]
 #  skip-if = os == "android" || toolkit == "gonk"
--- a/dom/push/test/test_data.html
+++ b/dom/push/test/test_data.html
@@ -31,29 +31,27 @@ http://creativecommons.org/licenses/publ
     .then(swr => { registration = swr; return swr; });
   }
 
   var controlledFrame;
   function createControlledIFrame(swr) {
     var p = new Promise(function(res, rej) {
       var iframe = document.createElement('iframe');
       iframe.id = "controlledFrame";
-      iframe.src = "http://mochi.test:8888/tests/dom/push/test/frame.html";
+      iframe.src = "/tests/dom/push/test/frame.html";
 
-      iframe.onload = function() {
-        res(swr)
-      }
+      iframe.onload = () => res();
       controlledFrame = iframe;
       document.body.appendChild(iframe);
     });
     return p;
   }
 
-  function subscribe(swr) {
-    return swr.pushManager.subscribe();
+  function subscribe() {
+    return registration.pushManager.subscribe();
   }
 
   function sendRequestToWorker(request) {
     return new Promise((resolve, reject) => {
       var channel = new MessageChannel();
       channel.port1.onmessage = e => {
         (e.data.error ? reject : resolve)(e.data);
       };
@@ -74,17 +72,17 @@ http://creativecommons.org/licenses/publ
           return pushSubscription;
       });
     });
     */
   }
 
   function waitForMessage(pushSubscription, message) {
     return Promise.all([
-      controlledFrame.contentWindow.waitOnPushMessage(pushSubscription),
+      controlledFrame.contentWindow.waitOnWorkerMessage("finished"),
       webpush(pushSubscription, message),
     ]).then(([message]) => message);
   }
 
   function sendPushMessageFromPage(pushSubscription) {
     var typedArray = new Uint8Array([226, 130, 40, 240, 40, 140, 188]);
     var json = { hello: "world" };
     return waitForMessage(pushSubscription, "Text message from page")
@@ -130,18 +128,18 @@ http://creativecommons.org/licenses/publ
           };
           reader.readAsText(message.data.blob);
         });
       }).then(text => {
         is(text, "Hi! \ud83d\udc40", "Wrong blob data for message with emoji");
         is(text, "Hi! \ud83d\udc40", "Wrong blob data for message with emoji");
         // Send a blank message.
         return Promise.all([
-          controlledFrame.contentWindow.waitOnPushMessage(pushSubscription),
-          fetch("http://mochi.test:8888/tests/dom/push/test/push-server.sjs", {
+          controlledFrame.contentWindow.waitOnWorkerMessage("finished"),
+          fetch("/tests/dom/push/test/push-server.sjs", {
             method: "PUT",
             headers: {
               "X-Push-Method": "POST",
               "X-Push-Server": pushSubscription.endpoint,
             },
           }),
         ]).then(([message]) => message);
       }).then(message => {
--- a/dom/push/test/test_register.html
+++ b/dom/push/test/test_register.html
@@ -27,21 +27,19 @@ http://creativecommons.org/licenses/publ
   //  console.log(str + "\n");
   }
 
   var controlledFrame;
   function createControlledIFrame(swr) {
     var p = new Promise(function(res, rej) {
       var iframe = document.createElement('iframe');
       iframe.id = "controlledFrame";
-      iframe.src = "http://mochi.test:8888/tests/dom/push/test/frame.html";
+      iframe.src = "/tests/dom/push/test/frame.html";
 
-      iframe.onload = function() {
-        res(swr)
-      }
+      iframe.onload = () => res(swr);
       controlledFrame = iframe;
       document.body.appendChild(iframe);
     });
     return p;
   }
 
   function checkPermissionState(swr) {
     return swr.pushManager.permissionState().then(function(state) {
@@ -100,17 +98,17 @@ http://creativecommons.org/licenses/publ
 
   function unregisterPushNotification(pushSubscription) {
     controlledFrame.parentNode.removeChild(controlledFrame);
     controlledFrame = null;
     return pushSubscription.unsubscribe();
   }
 
   function waitForPushNotification(pushSubscription) {
-    var p = controlledFrame.contentWindow.waitOnPushMessage();
+    var p = controlledFrame.contentWindow.waitOnWorkerMessage("finished");
     sendPushToPushServer(pushSubscription.endpoint);
     return p.then(function() {
       return pushSubscription;
     });
   }
 
   function runTest() {
     start()
new file mode 100644
--- /dev/null
+++ b/dom/push/test/test_subscription_change.html
@@ -0,0 +1,136 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1205109: Make `pushsubscriptionchange` extendable.
+
+Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/licenses/publicdomain/
+
+-->
+<head>
+  <title>Test for Bug 1205109</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205109">Mozilla Bug 1205109</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+  var registration;
+
+  function start() {
+    return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."})
+    .then(swr => { registration = swr; return swr; });
+  }
+
+  var controlledFrame;
+  function createControlledIFrame(swr) {
+    var p = new Promise(function(res, rej) {
+      var iframe = document.createElement('iframe');
+      iframe.id = "controlledFrame";
+      iframe.src = "/tests/dom/push/test/frame.html";
+
+      iframe.onload = () => res();
+      controlledFrame = iframe;
+      document.body.appendChild(iframe);
+    });
+    return p;
+  }
+
+  function subscribe() {
+    return registration.pushManager.subscribe();
+  }
+
+  function unsubscribe(pushSubscription) {
+    controlledFrame.parentNode.removeChild(controlledFrame);
+    controlledFrame = null;
+    return pushSubscription.unsubscribe();
+  }
+
+  function unregister() {
+    return registration.unregister();
+  }
+
+  function resetSubscriptions() {
+    return new Promise((resolve, reject) => {
+      var requestID = SpecialPowers.Cc["@mozilla.org/uuid-generator;1"]
+                                   .getService(SpecialPowers.Ci
+                                               .nsIUUIDGenerator)
+                                   .generateUUID().toString();
+      var cpmm = SpecialPowers.Cc["@mozilla.org/childprocessmessagemanager;1"]
+                              .getService(SpecialPowers.Ci
+                                          .nsISyncMessageSender);
+      var listener = {
+        receiveMessage: function(message) {
+          message = SpecialPowers.Cu.waiveXrays(message);
+          var isSuccess = message.name == "PushService:Reset:OK";
+          var isFailure = !isSuccess && message.name == "PushService:Reset:KO";
+          if ((isSuccess || isFailure) &&
+              message.data.requestID == requestID) {
+
+            cpmm.removeMessageListener("PushService:Reset:OK", listener);
+            cpmm.removeMessageListener("PushService:Reset:KO", listener);
+            (isSuccess ? resolve : reject)(message.data);
+          }
+        },
+      };
+      cpmm.addMessageListener("PushService:Reset:OK", listener);
+      cpmm.addMessageListener("PushService:Reset:KO", listener);
+
+      cpmm.sendAsyncMessage("Push:Reset", {
+        requestID: requestID,
+        scope: ".",
+      }, null, SpecialPowers.Services.scriptSecurityManager
+                            .getSystemPrincipal());
+    });
+  }
+
+  function changeSubscription(oldSubscription) {
+    return Promise.all([
+      controlledFrame.contentWindow.waitOnWorkerMessage("changed"),
+      resetSubscriptions(),
+    ]).then(() =>
+      registration.pushManager.getSubscription()
+    ).then(newSubscription => {
+      ok(newSubscription.endpoint != oldSubscription.endpoint,
+        "Expected new push endpoint");
+
+      var oldKey = new Uint8Array(oldSubscription.getKey("p256dh"));
+      var newKey = new Uint8Array(newSubscription.getKey("p256dh"));
+      ok(!oldKey.every((byte, index) => byte == newKey[index]),
+        "Expected new key share");
+
+      return newSubscription;
+    });
+  }
+
+  function runTest() {
+    start()
+    .then(createControlledIFrame)
+    .then(subscribe)
+    .then(changeSubscription)
+    .then(unsubscribe)
+    .then(unregister)
+    .catch(function(e) {
+      ok(false, "Some test failed with error " + e);
+    }).then(SimpleTest.finish);
+  }
+
+  SpecialPowers.pushPrefEnv({"set": [
+    ["dom.push.enabled", true],
+    ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+    ["dom.serviceWorkers.enabled", true],
+    ["dom.serviceWorkers.testing.enabled", true]
+    ]}, runTest);
+  SpecialPowers.addPermission('push', true, document);
+  SimpleTest.waitForExplicitFinish();
+</script>
+</body>
+</html>
--- a/dom/push/test/worker.js
+++ b/dom/push/test/worker.js
@@ -1,13 +1,14 @@
 // Any copyright is dedicated to the Public Domain.
 // http://creativecommons.org/licenses/publicdomain/
 
 this.onpush = handlePush;
 this.onmessage = handleMessage;
+this.onpushsubscriptionchange = handleSubscriptionChange;
 
 function getJSON(data) {
   var result = {
     ok: false,
   };
   try {
     result.value = data.json();
     result.ok = true;
@@ -50,8 +51,18 @@ function handleMessage(event) {
   /*
   if (event.data.type == "publicKey") {
     self.registration.pushManager.getSubscription().then(subscription => {
       event.ports[0].postMessage(subscription.getKey("p256dh"));
     });
   }
   */
 }
+
+function handleSubscriptionChange(event) {
+  event.waitUntil(self.registration.pushManager.subscribe().then(subscription =>
+    self.clients.matchAll()
+  ).then(([client]) => {
+    client.postMessage({type: "changed", okay: "yes"});
+  }).catch(err => {
+    dump("handleSubscriptionChange: Error notifying client: " + err + "\n");
+  }));
+}
--- a/dom/workers/ServiceWorkerManager.cpp
+++ b/dom/workers/ServiceWorkerManager.cpp
@@ -2385,29 +2385,37 @@ public:
     MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
   }
 
   bool
   WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
   {
     MOZ_ASSERT(aWorkerPrivate);
 
-    WorkerGlobalScope* globalScope = aWorkerPrivate->GlobalScope();
-
-    nsRefPtr<Event> event = NS_NewDOMEvent(globalScope, nullptr, nullptr);
-
-    nsresult rv = event->InitEvent(NS_LITERAL_STRING("pushsubscriptionchange"),
-                                   false, false);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return false;
-    }
+    nsRefPtr<EventTarget> target = aWorkerPrivate->GlobalScope();
+
+    ExtendableEventInit init;
+    init.mBubbles = false;
+    init.mCancelable = false;
+
+    ErrorResult result;
+    nsRefPtr<ExtendableEvent> event =
+      ExtendableEvent::Constructor(target,
+                                   NS_LITERAL_STRING("pushsubscriptionchange"),
+                                   init);
 
     event->SetTrusted(true);
 
-    globalScope->DispatchDOMEvent(nullptr, event, nullptr, nullptr);
+    nsRefPtr<Promise> waitUntilPromise =
+      DispatchExtendableEventOnWorkerScope(aCx, aWorkerPrivate->GlobalScope(), event);
+    if (waitUntilPromise) {
+      nsRefPtr<KeepAliveHandler> handler = new KeepAliveHandler(mServiceWorker);
+      waitUntilPromise->AppendNativeHandler(handler);
+    }
+
     return true;
   }
 };
 
 #endif /* ! MOZ_SIMPLEPUSH */
 
 NS_IMETHODIMP
 ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes,