Bug 1038811 - Push Notifications - Push implementation changes. r=nsm
authorDoug Turner <doug.turner@gmail.com>
Fri, 10 Apr 2015 20:19:28 -0700
changeset 270103 a33d054b75de9c0ca256d7e4347245f92e0b1aee
parent 270102 b277f825bc837c9beb6f4c8986bd7247b447ecae
child 270104 f75774fac6e2df9e9b673878514e9f94caa8953d
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnsm
bugs1038811
milestone40.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 1038811 - Push Notifications - Push implementation changes. r=nsm
dom/ipc/jar.mn
dom/push/Push.js
dom/push/Push.manifest
dom/push/PushService.jsm
dom/push/PushServiceChildPreload.js
dom/push/PushServiceLauncher.js
--- a/dom/ipc/jar.mn
+++ b/dom/ipc/jar.mn
@@ -4,10 +4,11 @@
 
 toolkit.jar:
         content/global/test-ipc.xul (test.xul)
         content/global/remote-test-ipc.js (remote-test.js)
         content/global/BrowserElementChild.js (../browser-element/BrowserElementChild.js)
         content/global/BrowserElementChildPreload.js (../browser-element/BrowserElementChildPreload.js)
         content/global/BrowserElementPanning.js (../browser-element/BrowserElementPanning.js)
 *       content/global/BrowserElementPanningAPZDisabled.js (../browser-element/BrowserElementPanningAPZDisabled.js)
+        content/global/PushServiceChildPreload.js (../push/PushServiceChildPreload.js)
         content/global/preload.js (preload.js)
         content/global/post-fork-preload.js (post-fork-preload.js)
--- a/dom/push/Push.js
+++ b/dom/push/Push.js
@@ -1,31 +1,119 @@
 /* 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/. */
 
 "use strict";
 
-// Don't modify this, instead set services.push.debug.
-let gDebuggingEnabled = false;
+// Don't modify this, instead set dom.push.debug.
+let gDebuggingEnabled = true;
 
 function debug(s) {
   if (gDebuggingEnabled)
     dump("-*- Push.js: " + s + "\n");
 }
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
+const PUSH_SUBSCRIPTION_CID = Components.ID("{CA86B665-BEDA-4212-8D0F-5C9F65270B58}");
+
+function PushSubscription(pushEndpoint, scope, pageURL) {
+  debug("PushSubscription Constructor");
+  this._pushEndpoint = pushEndpoint;
+  this._scope = scope;
+  this._pageURL = pageURL;
+}
+
+PushSubscription.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+
+  contractID: "@mozilla.org/push/PushSubscription;1",
+
+  classID : PUSH_SUBSCRIPTION_CID,
+
+  QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer,
+                                          Ci.nsISupportsWeakReference,
+                                          Ci.nsIObserver]),
+
+  init: function(aWindow) {
+    debug("PushSubscription init()");
+
+    this.initDOMRequestHelper(aWindow, [
+      "PushService:Unregister:OK",
+      "PushService:Unregister:KO",
+    ]);
+
+    this._cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+                   .getService(Ci.nsISyncMessageSender);
+  },
+
+  __init: function(endpoint, scope, pageURL) {
+    this._pushEndpoint = endpoint;
+    this._scope = scope;
+    this._pageURL = pageURL;
+  },
+
+  get endpoint() {
+    return this._pushEndpoint;
+  },
+
+  get subscriptionId() {
+    // TODO bug 1149271.  Not sure what this is about.
+    return "The twins of Mammon quarrelled.";
+  },
+
+  unsubscribe: function() {
+    debug("unsubscribe! ")
+
+    let promiseInit = function(resolve, reject) {
+      let resolverId = this.getPromiseResolverId({resolve: resolve,
+                                                  reject: reject });
+
+      this._cpmm.sendAsyncMessage("Push:Unregister", {
+                                  pageURL: this._pageURL,
+                                  scope: this._scope,
+                                  pushEndpoint: this._pushEndpoint,
+                                  requestID: resolverId
+                                });
+    }.bind(this);
+
+    return this.createPromise(promiseInit);
+  },
+
+  receiveMessage: function(aMessage) {
+    debug("push subscription receiveMessage(): " + JSON.stringify(aMessage))
+
+    let json = aMessage.data;
+    let resolver = this.takePromiseResolver(json.requestID);
+    if (resolver == null) {
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "PushService:Unregister:OK":
+        resolver.resolve(false);
+        break;
+      case "PushService:Unregister:KO":
+        resolver.reject(true);
+        break;
+      default:
+        debug("NOT IMPLEMENTED! receiveMessage for " + aMessage.name);
+    }
+  },
+
+};
+
 const PUSH_CID = Components.ID("{cde1d019-fad8-4044-b141-65fb4fb7a245}");
 
 /**
  * The Push component runs in the child process and exposes the SimplePush API
  * to the web application. The PushService running in the parent process is the
  * one actually performing all operations.
  */
 function Push() {
@@ -42,106 +130,191 @@ Push.prototype = {
   QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer,
                                           Ci.nsISupportsWeakReference,
                                           Ci.nsIObserver]),
 
   init: function(aWindow) {
     // Set debug first so that all debugging actually works.
     // NOTE: We don't add an observer here like in PushService. Flipping the
     // pref will require a reload of the app/page, which seems acceptable.
-    gDebuggingEnabled = Services.prefs.getBoolPref("services.push.debug");
+    gDebuggingEnabled = Services.prefs.getBoolPref("dom.push.debug");
     debug("init()");
 
-    let principal = aWindow.document.nodePrincipal;
-    let appsService = Cc["@mozilla.org/AppsService;1"]
-                        .getService(Ci.nsIAppsService);
-
-    this._manifestURL = appsService.getManifestURLByLocalId(principal.appId);
-    this._pageURL = principal.URI;
+    this._pageURL = aWindow.document.nodePrincipal.URI;
+    this._window = aWindow;
 
     this.initDOMRequestHelper(aWindow, [
       "PushService:Register:OK",
       "PushService:Register:KO",
-      "PushService:Unregister:OK",
-      "PushService:Unregister:KO",
-      "PushService:Registrations:OK",
-      "PushService:Registrations:KO"
+      "PushService:Registration:OK",
+      "PushService:Registration:KO"
     ]);
 
     this._cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
                    .getService(Ci.nsISyncMessageSender);
   },
 
+  setScope: function(scope){
+    debug('setScope ' + scope);
+    this._scope = scope;
+  },
+
+  askPermission: function (aAllowCallback, aCancelCallback) {
+    debug("askPermission");
+
+    let principal = this._window.document.nodePrincipal;
+    let type = "push";
+    let permValue =
+      Services.perms.testExactPermissionFromPrincipal(principal, type);
+
+    debug("Existing permission " + permValue);
+
+    if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) {
+        aAllowCallback();
+      return;
+    }
+
+    if (permValue == Ci.nsIPermissionManager.DENY_ACTION) {
+      aCancelCallback();
+      return;
+    }
+
+    // Create an array with a single nsIContentPermissionType element.
+    type = {
+      type: "push",
+      access: null,
+      options: [],
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionType])
+    };
+    let typeArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+    typeArray.appendElement(type, false);
+
+    // create a nsIContentPermissionRequest
+    let request = {
+      types: typeArray,
+      principal: principal,
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionRequest]),
+      allow: function() {
+        aAllowCallback();
+      },
+      cancel: function() {
+        aCancelCallback();
+      },
+      window: this._window
+    };
+
+    debug("asking the window utils about permission...")
+    // Using askPermission from nsIDOMWindowUtils that takes care of the
+    // remoting if needed.
+    let windowUtils = this._window.QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIDOMWindowUtils);
+    windowUtils.askPermission(request);
+  },
+
+
+
   receiveMessage: function(aMessage) {
-    debug("receiveMessage()");
-    let request = this.getRequest(aMessage.data.requestID);
+    debug("push receiveMessage(): " + JSON.stringify(aMessage))
+
     let json = aMessage.data;
-    if (!request) {
-      debug("No request " + json.requestID);
+    let resolver = this.takePromiseResolver(json.requestID);
+
+    if (!resolver) {
       return;
     }
 
     switch (aMessage.name) {
       case "PushService:Register:OK":
-        Services.DOMRequest.fireSuccess(request, json.pushEndpoint);
+      {
+        let subscription = new this._window.PushSubscription(json.pushEndpoint,
+                                                             this._scope,
+                                                             this._pageURL.spec);
+        resolver.resolve(subscription);
         break;
+      }
       case "PushService:Register:KO":
-        Services.DOMRequest.fireError(request, json.error);
-        break;
-      case "PushService:Unregister:OK":
-        Services.DOMRequest.fireSuccess(request, json.pushEndpoint);
+        resolver.reject(null);
         break;
-      case "PushService:Unregister:KO":
-        Services.DOMRequest.fireError(request, json.error);
+      case "PushService:Registration:OK":
+      {
+        let subscription = null;
+        try {
+          subscription = new this._window.PushSubscription(json.registration.pushEndpoint,
+                                                          this._scope, this._pageURL.spec);
+        } catch(error) {
+        }
+        resolver.resolve(subscription);
         break;
-      case "PushService:Registrations:OK":
-        Services.DOMRequest.fireSuccess(request, json.registrations);
-        break;
-      case "PushService:Registrations:KO":
-        Services.DOMRequest.fireError(request, json.error);
+      }
+      case "PushService:Registration:KO":
+        resolver.reject(null);
         break;
       default:
         debug("NOT IMPLEMENTED! receiveMessage for " + aMessage.name);
     }
   },
 
-  register: function() {
-    debug("register()");
-    let req = this.createRequest();
-    if (!Services.prefs.getBoolPref("services.push.connection.enabled")) {
-      // If push socket is disabled by the user, immediately error rather than
-      // timing out.
-      Services.DOMRequest.fireErrorAsync(req, "NetworkError");
-      return req;
-    }
+  subscribe: function() {
+    debug("subscribe()");
+    let p = this.createPromise(function(resolve, reject) {
+      let resolverId = this.getPromiseResolverId({ resolve: resolve, reject: reject });
 
-    this._cpmm.sendAsyncMessage("Push:Register", {
-                                  pageURL: this._pageURL.spec,
-                                  manifestURL: this._manifestURL,
-                                  requestID: this.getRequestId(req)
-                                });
-    return req;
+      this.askPermission(
+        function() {
+          this._cpmm.sendAsyncMessage("Push:Register", {
+                                      pageURL: this._pageURL.spec,
+                                      scope: this._scope,
+                                      requestID: resolverId
+                                    });
+        }.bind(this),
+
+        function() {
+          reject("denied");
+        }
+      );
+    }.bind(this));
+    return p;
   },
 
-  unregister: function(aPushEndpoint) {
-    debug("unregister(" + aPushEndpoint + ")");
-    let req = this.createRequest();
-    this._cpmm.sendAsyncMessage("Push:Unregister", {
-                                  pageURL: this._pageURL.spec,
-                                  manifestURL: this._manifestURL,
-                                  requestID: this.getRequestId(req),
-                                  pushEndpoint: aPushEndpoint
-                                });
-    return req;
+  getSubscription: function() {
+    debug("getSubscription()" + this._scope);
+
+    let p = this.createPromise(function(resolve, reject) {
+
+      let resolverId = this.getPromiseResolverId({ resolve: resolve, reject: reject });
+
+      this.askPermission(
+        function() {
+          this._cpmm.sendAsyncMessage("Push:Registration", {
+                                      pageURL: this._pageURL.spec,
+                                      scope: this._scope,
+                                      requestID: resolverId
+                                    });
+        }.bind(this),
+
+        function() {
+          reject("denied");
+        }
+      );
+    }.bind(this));
+    return p;
   },
 
-  registrations: function() {
-    debug("registrations()");
-    let req = this.createRequest();
-    this._cpmm.sendAsyncMessage("Push:Registrations", {
-                                  manifestURL: this._manifestURL,
-                                  requestID: this.getRequestId(req)
-                                });
-    return req;
-  }
+  hasPermission: function() {
+    debug("getSubscription()" + this._scope);
+
+    let p = this.createPromise(function(resolve, reject) {
+      let permissionManager = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager);
+      let permission = permissionManager.testExactPermission(this._pageURL, "push");
+
+      let pushPermissionStatus = "default";
+      if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
+        pushPermissionStatus = "granted";
+      } else if (permission == Ci.nsIPermissionManager.DENY_ACTION) {
+        pushPermissionStatus = "denied";
+      }
+      resolve(pushPermissionStatus);
+    }.bind(this));
+    return p;
+  },
 }
 
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Push]);
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Push, PushSubscription]);
--- a/dom/push/Push.manifest
+++ b/dom/push/Push.manifest
@@ -1,8 +1,11 @@
 # DOM API
 component {cde1d019-fad8-4044-b141-65fb4fb7a245} Push.js
 contract @mozilla.org/push/PushManager;1 {cde1d019-fad8-4044-b141-65fb4fb7a245}
 
+component {CA86B665-BEDA-4212-8D0F-5C9F65270B58} Push.js
+contract @mozilla.org/push/PushSubscription;1 {CA86B665-BEDA-4212-8D0F-5C9F65270B58}
+
 # Component to initialize PushService on startup.
 component {4b8caa3b-3c58-4f3c-a7f5-7bd9cb24c11d} PushServiceLauncher.js
 contract @mozilla.org/push/ServiceLauncher;1 {4b8caa3b-3c58-4f3c-a7f5-7bd9cb24c11d}
 category app-startup PushServiceLauncher @mozilla.org/push/ServiceLauncher;1
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -1,17 +1,17 @@
 /* jshint moz: true, esnext: true */
 /* 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/. */
 
 "use strict";
 
-// Don't modify this, instead set services.push.debug.
-let gDebuggingEnabled = false;
+// Don't modify this, instead set dom.push.debug.
+let gDebuggingEnabled = true;
 
 function debug(s) {
   if (gDebuggingEnabled)
     dump("-*- PushService.jsm: " + s + "\n");
 }
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
@@ -36,30 +36,30 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyServiceGetter(this, "gPowerManagerService",
                                    "@mozilla.org/power/powermanagerservice;1",
                                    "nsIPowerManagerService");
 
 var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
 
 this.EXPORTED_SYMBOLS = ["PushService"];
 
-const prefs = new Preferences("services.push.");
+const prefs = new Preferences("dom.push.");
 // Set debug first so that all debugging actually works.
 gDebuggingEnabled = prefs.get("debug");
 
 const kPUSHDB_DB_NAME = "push";
 const kPUSHDB_DB_VERSION = 1; // Change this if the IndexedDB format changes
 const kPUSHDB_STORE_NAME = "push";
 
 const kUDP_WAKEUP_WS_STATUS_CODE = 4774;  // WebSocket Close status code sent
                                           // by server to signal that it can
                                           // wake client up using UDP.
 
 const kCHILD_PROCESS_MESSAGES = ["Push:Register", "Push:Unregister",
-                                 "Push:Registrations"];
+                                 "Push:Registration"];
 
 const kWS_MAX_WENTDOWN = 2;
 
 // This is a singleton
 this.PushDB = function PushDB() {
   debug("PushDB()");
 
   // set the indexeddb database
@@ -74,32 +74,31 @@ this.PushDB.prototype = {
     debug("PushDB.upgradeSchema()");
 
     let objectStore = aDb.createObjectStore(kPUSHDB_STORE_NAME,
                                             { keyPath: "channelID" });
 
     // index to fetch records based on endpoints. used by unregister
     objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
 
-    // index to fetch records per manifest, so we can identify endpoints
-    // associated with an app. Since an app can have multiple endpoints
-    // uniqueness cannot be enforced
-    objectStore.createIndex("manifestURL", "manifestURL", { unique: false });
+    // index to fetch records per scope, so we can identify endpoints
+    // associated with an app.
+    objectStore.createIndex("scope", "scope", { unique: true });
   },
 
   /*
    * @param aChannelRecord
    *        The record to be added.
    * @param aSuccessCb
    *        Callback function to invoke with result ID.
    * @param aErrorCb [optional]
    *        Callback function to invoke when there was an error.
    */
   put: function(aChannelRecord, aSuccessCb, aErrorCb) {
-    debug("put()");
+    debug("put()" + JSON.stringify(aChannelRecord));
 
     this.newTxn(
       "readwrite",
       kPUSHDB_STORE_NAME,
       function txnCb(aTxn, aStore) {
         debug("Going to put " + aChannelRecord.channelID);
         aStore.put(aChannelRecord).onsuccess = function setTxnResult(aEvent) {
           debug("Request successful. Updated record ID: " +
@@ -168,40 +167,30 @@ this.PushDB.prototype = {
           debug("Fetch successful " + aEvent.target.result);
         }
       },
       aSuccessCb,
       aErrorCb
     );
   },
 
-  getAllByManifestURL: function(aManifestURL, aSuccessCb, aErrorCb) {
-    debug("getAllByManifestURL()");
-    if (!aManifestURL) {
-      if (typeof aErrorCb == "function") {
-        aErrorCb("PushDB.getAllByManifestURL: Got undefined aManifestURL");
-      }
-      return;
-    }
 
-    let self = this;
+  getByScope: function(aScope, aSuccessCb, aErrorCb) {
+    debug("getByScope() " + aScope);
+
     this.newTxn(
       "readonly",
       kPUSHDB_STORE_NAME,
       function txnCb(aTxn, aStore) {
-        let index = aStore.index("manifestURL");
-        let range = IDBKeyRange.only(aManifestURL);
-        aTxn.result = [];
-        index.openCursor(range).onsuccess = function(event) {
-          let cursor = event.target.result;
-          if (cursor) {
-            debug(cursor.value.manifestURL + " " + cursor.value.channelID);
-            aTxn.result.push(cursor.value);
-            cursor.continue();
-          }
+        aTxn.result = undefined;
+
+        let index = aStore.index("scope");
+        index.get(aScope).onsuccess = function setTxnResult(aEvent) {
+          aTxn.result = aEvent.target.result;
+          debug("Fetch successful " + aEvent.target.result);
         }
       },
       aSuccessCb,
       aErrorCb
     );
   },
 
   getAllChannelIDs: function(aSuccessCb, aErrorCb) {
@@ -320,27 +309,27 @@ this.PushService = {
 
         // Try to connect if network-active-changed or the offline-status
         // changed to online.
         if (aTopic === "network-active-changed" || aData === "online") {
           this._startListeningIfChannelsPresent();
         }
         break;
       case "nsPref:changed":
-        if (aData == "services.push.serverURL") {
-          debug("services.push.serverURL changed! websocket. new value " +
+        if (aData == "dom.push.serverURL") {
+          debug("dom.push.serverURL changed! websocket. new value " +
                 prefs.get("serverURL"));
           this._shutdownWS();
-        } else if (aData == "services.push.connection.enabled") {
+        } else if (aData == "dom.push.connection.enabled") {
           if (prefs.get("connection.enabled")) {
             this._startListeningIfChannelsPresent();
           } else {
             this._shutdownWS();
           }
-        } else if (aData == "services.push.debug") {
+        } else if (aData == "dom.push.debug") {
           gDebuggingEnabled = prefs.get("debug");
         }
         break;
       case "timer-callback":
         if (aSubject == this._requestTimeoutTimer) {
           if (Object.keys(this._pendingRequests).length == 0) {
             this._requestTimeoutTimer.cancel();
           }
@@ -377,45 +366,40 @@ this.PushService = {
         debug("webapps-clear-data");
 
         let data = aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
         if (!data) {
           debug("webapps-clear-data: Failed to get information about application");
           return;
         }
 
-        // Only remove push registrations for apps.
-        if (data.browserOnly) {
+        // TODO 1149274.  We should support site permissions as well as a way to go from manifest
+        // url to 'all scopes registered for push in this app'
+        let appsService = Cc["@mozilla.org/AppsService;1"]
+                            .getService(Ci.nsIAppsService);
+        let scope = appsService.getScopeByLocalId(data.appId);
+        if (!scope) {
+          debug("webapps-clear-data: No scope found for " + data.appId);
           return;
         }
 
-        let appsService = Cc["@mozilla.org/AppsService;1"]
-                            .getService(Ci.nsIAppsService);
-        let manifestURL = appsService.getManifestURLByLocalId(data.appId);
-        if (!manifestURL) {
-          debug("webapps-clear-data: No manifest URL found for " + data.appId);
-          return;
-        }
+        this._db.getByScope(scope, function(record) {
+          this._db.delete(records.channelID, null, function() {
+              debug("webapps-clear-data: " + scope +
+                    " Could not delete entry " + records.channelID);
 
-        this._db.getAllByManifestURL(manifestURL, function(records) {
-          debug("Got " + records.length);
-          for (let i = 0; i < records.length; i++) {
-            this._db.delete(records[i].channelID, null, function() {
-              debug("webapps-clear-data: " + manifestURL +
-                    " Could not delete entry " + records[i].channelID);
-            });
             // courtesy, but don't establish a connection
             // just for it
             if (this._ws) {
               debug("Had a connection, so telling the server");
-              this._send("unregister", {channelID: records[i].channelID});
+              this._send("unregister", {channelID: records.channelID});
             }
-          }
-        }.bind(this), function() {
-          debug("webapps-clear-data: Error in getAllByManifestURL(" + manifestURL + ")");
+          }.bind(this), function() {
+            debug("webapps-clear-data: Error in getByScope(" + scope + ")");
+          });
         });
 
         break;
     }
   },
 
   get _UAID() {
     return prefs.get("userAgentID");
@@ -460,17 +444,17 @@ this.PushService = {
    */
   _adaptiveEnabled: false,
 
   /**
    * This saves a flag about if we need to recalculate a new ping, based on:
    *   1) the gap between the maximum working ping and the first ping that
    *      gives an error (timeout) OR
    *   2) we have reached the pref of the maximum value we allow for a ping
-   *      (services.push.adaptive.upperLimit)
+   *      (dom.push.adaptive.upperLimit)
    */
   _recalculatePing: true,
 
   /**
    * This map holds a (pingInterval, triedTimes) of each pingInterval tried.
    * It is used to check if the pingInterval has been tested enough to know that
    * is incorrect and is above the limit the network allow us to keep the
    * connection open.
@@ -507,16 +491,21 @@ this.PushService = {
     this._ws.sendMsg(msg);
   },
 
   init: function() {
     debug("init()");
     if (!prefs.get("enabled"))
         return null;
 
+    var globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
+               .getService(Ci.nsIFrameScriptLoader);
+
+    globalMM.loadFrameScript("chrome://global/content/PushServiceChildPreload.js", true);
+
     this._db = new PushDB();
 
     let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
                  .getService(Ci.nsIMessageBroadcaster);
 
     kCHILD_PROCESS_MESSAGES.forEach(function addMessage(msgName) {
         ppmm.addMessageListener(msgName, this);
     }.bind(this));
@@ -667,17 +656,17 @@ this.PushService = {
    * So a fixed ping is not good for us for two reasons:
    *  1) We might lose the connection, so we need to reconnect again (wasting
    *  resources)
    *  2) We use a lot of network signaling just for pinging.
    *
    * This algorithm tries to search the best value between a disconnection and a
    * valid ping, to ensure better battery life and network resources usage.
    *
-   * The value is saved in services.push.pingInterval
+   * The value is saved in dom.push.pingInterval
    * @param wsWentDown [Boolean] if the WebSocket was closed or it is still alive
    *
    */
   _calculateAdaptivePing: function(wsWentDown) {
     debug('_calculateAdaptivePing()');
     if (!this._adaptiveEnabled) {
       debug('Adaptive ping is disabled');
       return;
@@ -837,25 +826,25 @@ this.PushService = {
 
     if (Services.io.offline) {
       debug("Network is offline.");
       return;
     }
 
     let serverURL = prefs.get("serverURL");
     if (!serverURL) {
-      debug("No services.push.serverURL found!");
+      debug("No dom.push.serverURL found!");
       return;
     }
 
     let uri;
     try {
       uri = Services.io.newURI(serverURL, null, null);
     } catch(e) {
-      debug("Error creating valid URI from services.push.serverURL (" +
+      debug("Error creating valid URI from dom.push.serverURL (" +
             serverURL + ")");
       return;
     }
 
     if (uri.scheme === "wss") {
       this._ws = Cc["@mozilla.org/network/protocol;1?name=wss"]
                    .createInstance(Ci.nsIWebSocketChannel);
 
@@ -1085,17 +1074,17 @@ this.PushService = {
     // accept.
     //
     // If we already had a valid UAID before, we have to ask apps to
     // re-register.
     if (this._UAID && this._UAID != reply.uaid) {
       debug("got new UAID: all re-register");
 
       this._notifyAllAppsRegister()
-          .then(this._dropRegistrations.bind(this))
+          .then(this._dropRegistration.bind(this))
           .then(finishHandshake.bind(this));
 
       return;
     }
 
     // otherwise we are good to go
     finishHandshake.bind(this)();
   },
@@ -1274,80 +1263,91 @@ this.PushService = {
     }
 
     this._db.getByChannelID(aChannelID,
                             compareRecordVersionAndNotify.bind(this),
                             recoverNoSuchChannelID.bind(this));
   },
 
   // Fires a push-register system message to all applications that have
-  // registrations.
+  // registration.
   _notifyAllAppsRegister: function() {
     debug("notifyAllAppsRegister()");
     let deferred = Promise.defer();
 
-    // records are objects describing the registrations as stored in IndexedDB.
+    // records are objects describing the registration as stored in IndexedDB.
     function wakeupRegisteredApps(records) {
       // Pages to be notified.
-      // wakeupTable[manifestURL] -> [ pageURL ]
+      // wakeupTable[scope] -> [ pageURL ]
       let wakeupTable = {};
       for (let i = 0; i < records.length; i++) {
         let record = records[i];
-        if (!(record.manifestURL in wakeupTable))
-          wakeupTable[record.manifestURL] = [];
+        if (!(record.scope in wakeupTable))
+          wakeupTable[record.scope] = [];
 
-        wakeupTable[record.manifestURL].push(record.pageURL);
+        wakeupTable[record.scope].push(record.pageURL);
       }
 
-      let messenger = Cc["@mozilla.org/system-message-internal;1"]
-                        .getService(Ci.nsISystemMessagesInternal);
+      // TODO -- test needed.  E10s support needed.
 
-      for (let manifestURL in wakeupTable) {
-        wakeupTable[manifestURL].forEach(function(pageURL) {
-          messenger.sendMessage('push-register', {},
-                                Services.io.newURI(pageURL, null, null),
-                                Services.io.newURI(manifestURL, null, null));
+      let globalMM = Cc['@mozilla.org/globalmessagemanager;1'].getService(Ci.nsIMessageListenerManager);
+      for (let scope in wakeupTable) {
+        wakeupTable[scope].forEach(function(pageURL) {
+          globalMM.broadcastAsyncMessage('pushsubscriptionchanged', aPushRecord.scope);
         });
       }
-
       deferred.resolve();
     }
 
     this._db.getAllChannelIDs(wakeupRegisteredApps, deferred.reject);
 
     return deferred.promise;
   },
 
   _notifyApp: function(aPushRecord) {
-    if (!aPushRecord || !aPushRecord.pageURL || !aPushRecord.manifestURL) {
-      debug("notifyApp() something is undefined.  Dropping notification");
+    if (!aPushRecord || !aPushRecord.pageURL || !aPushRecord.scope) {
+      debug("notifyApp() something is undefined.  Dropping notification: "
+        + JSON.stringify(aPushRecord) );
       return;
     }
 
     debug("notifyApp() " + aPushRecord.pageURL +
-          "  " + aPushRecord.manifestURL);
+          "  " + aPushRecord.scope);
     let pageURI = Services.io.newURI(aPushRecord.pageURL, null, null);
-    let manifestURI = Services.io.newURI(aPushRecord.manifestURL, null, null);
+    let scopeURI = Services.io.newURI(aPushRecord.scope, null, null);
     let message = {
       pushEndpoint: aPushRecord.pushEndpoint,
       version: aPushRecord.version
     };
-    let messenger = Cc["@mozilla.org/system-message-internal;1"]
-                      .getService(Ci.nsISystemMessagesInternal);
-    messenger.sendMessage('push', message, pageURI, manifestURI);
+
+    // If permission has been revoked, trash the message.
+    if(Services.perms.testExactPermission(scopeURI, "push") != Ci.nsIPermissionManager.ALLOW_ACTION) {
+      debug("Does not have permission for push.")
+      return;
+    }
+
+    // TODO data.
+    let data = {
+      payload: "Short as life is, we make it still shorter by the careless waste of time.",
+      scope: aPushRecord.scope
+    };
+
+    let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
+                 .getService(Ci.nsIMessageListenerManager);
+    globalMM.broadcastAsyncMessage('push', data);
   },
 
   _updatePushRecord: function(aPushRecord) {
     debug("updatePushRecord()");
     let deferred = Promise.defer();
     this._db.put(aPushRecord, deferred.resolve, deferred.reject);
     return deferred.promise;
   },
 
-  _dropRegistrations: function() {
+  _dropRegistration: function() {
     let deferred = Promise.defer();
     this._db.drop(deferred.resolve, deferred.reject);
     return deferred.promise;
   },
 
   receiveMessage: function(aMessage) {
     debug("receiveMessage(): " + aMessage.name);
 
@@ -1360,36 +1360,54 @@ this.PushService = {
     let json = aMessage.data;
     this[aMessage.name.slice("Push:".length).toLowerCase()](json, mm);
   },
 
   /**
    * Called on message from the child process. aPageRecord is an object sent by
    * navigator.push, identifying the sending page and other fields.
    */
-  register: function(aPageRecord, aMessageManager) {
-    debug("register()");
 
+  _registerWithServer: function(aPageRecord, aMessageManager) {
     let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
                           .getService(Ci.nsIUUIDGenerator);
     // generateUUID() gives a UUID surrounded by {...}, slice them off.
     let channelID = uuidGenerator.generateUUID().toString().slice(1, -1);
 
     this._sendRequest("register", {channelID: channelID})
       .then(
         this._onRegisterSuccess.bind(this, aPageRecord, channelID),
         this._onRegisterError.bind(this, aPageRecord, aMessageManager)
       )
       .then(
         function(message) {
           aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
         },
         function(message) {
           aMessageManager.sendAsyncMessage("PushService:Register:KO", message);
-      });
+      }
+    );
+  },
+
+  register: function(aPageRecord, aMessageManager) {
+    debug("register(): " + JSON.stringify(aPageRecord));
+
+    this._db.getByScope(aPageRecord.scope,
+      function(aPageRecord, aMessageManager, pushRecord) {
+        if (pushRecord == null) {
+          this._registerWithServer(aPageRecord, aMessageManager);
+        }
+        else {
+          this._onRegistrationSuccess(aPageRecord, aMessageManager, pushRecord);
+        }
+      }.bind(this, aPageRecord, aMessageManager),
+      function () {
+        debug("getByScope failed");
+      }
+    );
   },
 
   /**
    * Exceptions thrown in _onRegisterSuccess are caught by the promise obtained
    * from _sendRequest, causing the promise to be rejected instead.
    */
   _onRegisterSuccess: function(aPageRecord, generatedChannelID, data) {
     debug("_onRegisterSuccess()");
@@ -1416,32 +1434,34 @@ this.PushService = {
       message["error"] = "Invalid pushEndpoint " + data.pushEndpoint;
       throw message;
     }
 
     let record = {
       channelID: data.channelID,
       pushEndpoint: data.pushEndpoint,
       pageURL: aPageRecord.pageURL,
-      manifestURL: aPageRecord.manifestURL,
+      scope: aPageRecord.scope,
       version: null
     };
 
+    debug("scope in _onRegisterSuccess: " + aPageRecord.scope)
+
     this._updatePushRecord(record)
       .then(
         function() {
           message["pushEndpoint"] = data.pushEndpoint;
           deferred.resolve(message);
         },
         function(error) {
           // Unable to save.
           this._send("unregister", {channelID: record.channelID});
           message["error"] = error;
           deferred.reject(message);
-        }
+        }.bind(this)
       );
 
     return deferred.promise;
   },
 
   /**
    * Exceptions thrown in _onRegisterError are caught by the promise obtained
    * from _sendRequest, causing the promise to be rejected instead.
@@ -1474,33 +1494,33 @@ this.PushService = {
    * application is never informed. In addition the application may retry the
    * unregister when it fails due to timeout at which point the server will say
    * it does not know of this unregistration.  We'll have to make the
    * registration/unregistration phases have retries and attempts to resend
    * messages from the server, and have the client acknowledge. On a server,
    * data is cheap, reliable notification is not.
    */
   unregister: function(aPageRecord, aMessageManager) {
-    debug("unregister()");
+    debug("unregister() " + JSON.stringify(aPageRecord));
 
     let fail = function(error) {
       debug("unregister() fail() error " + error);
       let message = {requestID: aPageRecord.requestID, error: error};
       aMessageManager.sendAsyncMessage("PushService:Unregister:KO", message);
     }
 
     this._db.getByPushEndpoint(aPageRecord.pushEndpoint, function(record) {
       // If the endpoint didn't exist, let's just fail.
       if (record === undefined) {
         fail("NotFoundError");
         return;
       }
 
       // Non-owner tried to unregister, say success, but don't do anything.
-      if (record.manifestURL !== aPageRecord.manifestURL) {
+      if (record.scope !== aPageRecord.scope) {
         aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
           requestID: aPageRecord.requestID,
           pushEndpoint: aPageRecord.pushEndpoint
         });
         return;
       }
 
       this._db.delete(record.channelID, function() {
@@ -1513,48 +1533,45 @@ this.PushService = {
         });
       }.bind(this), fail);
     }.bind(this), fail);
   },
 
   /**
    * Called on message from the child process
    */
-  registrations: function(aPageRecord, aMessageManager) {
-    debug("registrations()");
-
-    if (aPageRecord.manifestURL) {
-      this._db.getAllByManifestURL(aPageRecord.manifestURL,
-        this._onRegistrationsSuccess.bind(this, aPageRecord, aMessageManager),
-        this._onRegistrationsError.bind(this, aPageRecord, aMessageManager));
-    }
-    else {
-      this._onRegistrationsError(aPageRecord, aMessageManager);
-    }
+  registration: function(aPageRecord, aMessageManager) {
+    debug("registration()");
+    this._db.getByScope(aPageRecord.scope,
+      this._onRegistrationSuccess.bind(this, aPageRecord, aMessageManager),
+      this._onRegistrationError.bind(this, aPageRecord, aMessageManager));
   },
 
-  _onRegistrationsSuccess: function(aPageRecord,
-                                    aMessageManager,
-                                    pushRecords) {
-    let registrations = [];
-    pushRecords.forEach(function(pushRecord) {
-      registrations.push({
-          __exposedProps__: { pushEndpoint: 'r', version: 'r' },
-          pushEndpoint: pushRecord.pushEndpoint,
-          version: pushRecord.version
-      });
-    });
-    aMessageManager.sendAsyncMessage("PushService:Registrations:OK", {
+  _onRegistrationSuccess: function(aPageRecord,
+                                   aMessageManager,
+                                   pushRecord) {
+
+
+    let registration = null;
+
+    if (pushRecord) {
+      registration = {
+        pushEndpoint: pushRecord.pushEndpoint,
+        version: pushRecord.version
+      };
+    }
+
+    aMessageManager.sendAsyncMessage("PushService:Registration:OK", {
       requestID: aPageRecord.requestID,
-      registrations: registrations
+      registration: registration
     });
   },
 
-  _onRegistrationsError: function(aPageRecord, aMessageManager) {
-    aMessageManager.sendAsyncMessage("PushService:Registrations:KO", {
+  _onRegistrationError: function(aPageRecord, aMessageManager) {
+    aMessageManager.sendAsyncMessage("PushService:Registration:KO", {
       requestID: aPageRecord.requestID,
       error: "Database error"
     });
   },
 
   // begin Push protocol handshake
   _wsOnStart: function(context) {
     debug("wsOnStart()");
@@ -1728,17 +1745,17 @@ this.PushService = {
 
     if (!prefs.get("udp.wakeupEnabled")) {
       debug("UDP support disabled");
       return;
     }
 
     this._udpServer = Cc["@mozilla.org/network/udp-socket;1"]
                         .createInstance(Ci.nsIUDPSocket);
-    this._udpServer.init(-1, false, Services.scriptSecurityManager.getSystemPrincipal());
+    this._udpServer.init(-1, false);
     this._udpServer.asyncListen(this);
     debug("listenForUDPWakeup listening on " + this._udpServer.port);
 
     return this._udpServer.port;
   },
 
   /**
    * Called by UDP Server Socket. As soon as a ping is recieved via UDP,
new file mode 100644
--- /dev/null
+++ b/dom/push/PushServiceChildPreload.js
@@ -0,0 +1,18 @@
+/* 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/. */
+
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(this,
+                                   "swm",
+                                   "@mozilla.org/serviceworkers/manager;1",
+                                   "nsIServiceWorkerManager");
+
+addMessageListener("push", function (aMessage) {
+  swm.sendPushEvent(aMessage.data.scope, aMessage.data.payload);
+});
+
+addMessageListener("pushsubscriptionchanged", function (aMessage) {
+  swm.sendPushSubscriptionChangedEvent(aMessage.data);
+});
\ No newline at end of file
--- a/dom/push/PushServiceLauncher.js
+++ b/dom/push/PushServiceLauncher.js
@@ -25,19 +25,26 @@ PushServiceLauncher.prototype = {
 
   observe: function observe(subject, topic, data) {
     switch (topic) {
       case "app-startup":
         Services.obs.addObserver(this, "final-ui-startup", true);
         break;
       case "final-ui-startup":
         Services.obs.removeObserver(this, "final-ui-startup");
-        if (!Services.prefs.getBoolPref("services.push.enabled")) {
+        if (!Services.prefs.getBoolPref("dom.push.enabled")) {
           return;
         }
-        Cu.import("resource://gre/modules/PushService.jsm");
-        PushService.init();
+
+        let isParent = Cc["@mozilla.org/xre/runtime;1"]
+                       .getService(Ci.nsIXULRuntime)
+                       .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+        if (isParent) {
+          Cu.import("resource://gre/modules/PushService.jsm");
+          PushService.init();
+        }
         break;
     }
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PushServiceLauncher]);