summaryrefslogtreecommitdiffstats
path: root/dom/push/PushComponents.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/push/PushComponents.js')
-rw-r--r--dom/push/PushComponents.js558
1 files changed, 558 insertions, 0 deletions
diff --git a/dom/push/PushComponents.js b/dom/push/PushComponents.js
new file mode 100644
index 000000000..214e9fc47
--- /dev/null
+++ b/dom/push/PushComponents.js
@@ -0,0 +1,558 @@
+/* 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";
+
+/**
+ * This file exports XPCOM components for C++ and chrome JavaScript callers to
+ * interact with the Push service.
+ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+var isParent = Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+// The default Push service implementation.
+XPCOMUtils.defineLazyGetter(this, "PushService", function() {
+ const {PushService} = Cu.import("resource://gre/modules/PushService.jsm",
+ {});
+ PushService.init();
+ return PushService;
+});
+
+// Observer notification topics for push messages and subscription status
+// changes. These are duplicated and used in `nsIPushNotifier`. They're exposed
+// on `nsIPushService` so that JS callers only need to import this service.
+const OBSERVER_TOPIC_PUSH = "push-message";
+const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
+const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified";
+
+/**
+ * `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively
+ * implement the `nsIPushService` interface. This interface provides calls
+ * similar to the Push DOM API, but does not require service workers.
+ *
+ * Push service methods may be called from the parent or content process. The
+ * parent process implementation loads `PushService.jsm` at app startup, and
+ * calls its methods directly. The content implementation forwards calls to
+ * the parent Push service via IPC.
+ *
+ * The implementations share a class and contract ID.
+ */
+function PushServiceBase() {
+ this.wrappedJSObject = this;
+ this._addListeners();
+}
+
+PushServiceBase.prototype = {
+ classID: Components.ID("{daaa8d73-677e-4233-8acd-2c404bd01658}"),
+ contractID: "@mozilla.org/push/Service;1",
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIPushService,
+ Ci.nsIPushQuotaManager,
+ Ci.nsIPushErrorReporter,
+ ]),
+
+ pushTopic: OBSERVER_TOPIC_PUSH,
+ subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
+ subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
+
+ _handleReady() {},
+
+ _addListeners() {
+ for (let message of this._messages) {
+ this._mm.addMessageListener(message, this);
+ }
+ },
+
+ _isValidMessage(message) {
+ return this._messages.includes(message.name);
+ },
+
+ observe(subject, topic, data) {
+ if (topic === "app-startup") {
+ Services.obs.addObserver(this, "sessionstore-windows-restored", true);
+ return;
+ }
+ if (topic === "sessionstore-windows-restored") {
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ this._handleReady();
+ return;
+ }
+ if (topic === "android-push-service") {
+ // Load PushService immediately.
+ this._handleReady();
+ return;
+ }
+ },
+
+ _deliverSubscription(request, props) {
+ if (!props) {
+ request.onPushSubscription(Cr.NS_OK, null);
+ return;
+ }
+ request.onPushSubscription(Cr.NS_OK, new PushSubscription(props));
+ },
+
+ _deliverSubscriptionError(request, error) {
+ let result = typeof error.result == "number" ?
+ error.result : Cr.NS_ERROR_FAILURE;
+ request.onPushSubscription(result, null);
+ },
+};
+
+/**
+ * The parent process implementation of `nsIPushService`. This version loads
+ * `PushService.jsm` at startup and calls its methods directly. It also
+ * receives and responds to requests from the content process.
+ */
+function PushServiceParent() {
+ PushServiceBase.call(this);
+}
+
+PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
+
+XPCOMUtils.defineLazyServiceGetter(PushServiceParent.prototype, "_mm",
+ "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
+
+Object.assign(PushServiceParent.prototype, {
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceParent),
+
+ _messages: [
+ "Push:Register",
+ "Push:Registration",
+ "Push:Unregister",
+ "Push:Clear",
+ "Push:NotificationForOriginShown",
+ "Push:NotificationForOriginClosed",
+ "Push:ReportError",
+ ],
+
+ // nsIPushService methods
+
+ subscribe(scope, principal, callback) {
+ this.subscribeWithKey(scope, principal, 0, null, callback);
+ },
+
+ subscribeWithKey(scope, principal, keyLen, key, callback) {
+ this._handleRequest("Push:Register", principal, {
+ scope: scope,
+ appServerKey: key,
+ }).then(result => {
+ this._deliverSubscription(callback, result);
+ }, error => {
+ this._deliverSubscriptionError(callback, error);
+ }).catch(Cu.reportError);
+ },
+
+ unsubscribe(scope, principal, callback) {
+ this._handleRequest("Push:Unregister", principal, {
+ scope: scope,
+ }).then(result => {
+ callback.onUnsubscribe(Cr.NS_OK, result);
+ }, error => {
+ callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
+ }).catch(Cu.reportError);
+ },
+
+ getSubscription(scope, principal, callback) {
+ return this._handleRequest("Push:Registration", principal, {
+ scope: scope,
+ }).then(result => {
+ this._deliverSubscription(callback, result);
+ }, error => {
+ this._deliverSubscriptionError(callback, error);
+ }).catch(Cu.reportError);
+ },
+
+ clearForDomain(domain, callback) {
+ return this._handleRequest("Push:Clear", null, {
+ domain: domain,
+ }).then(result => {
+ callback.onClear(Cr.NS_OK);
+ }, error => {
+ callback.onClear(Cr.NS_ERROR_FAILURE);
+ }).catch(Cu.reportError);
+ },
+
+ // nsIPushQuotaManager methods
+
+ notificationForOriginShown(origin) {
+ this.service.notificationForOriginShown(origin);
+ },
+
+ notificationForOriginClosed(origin) {
+ this.service.notificationForOriginClosed(origin);
+ },
+
+ // nsIPushErrorReporter methods
+
+ reportDeliveryError(messageId, reason) {
+ this.service.reportDeliveryError(messageId, reason);
+ },
+
+ receiveMessage(message) {
+ if (!this._isValidMessage(message)) {
+ return;
+ }
+ let {name, principal, target, data} = message;
+ if (name === "Push:NotificationForOriginShown") {
+ this.notificationForOriginShown(data);
+ return;
+ }
+ if (name === "Push:NotificationForOriginClosed") {
+ this.notificationForOriginClosed(data);
+ return;
+ }
+ if (!target.assertPermission("push")) {
+ return;
+ }
+ if (name === "Push:ReportError") {
+ this.reportDeliveryError(data.messageId, data.reason);
+ return;
+ }
+ let sender = target.QueryInterface(Ci.nsIMessageSender);
+ return this._handleRequest(name, principal, data).then(result => {
+ sender.sendAsyncMessage(this._getResponseName(name, "OK"), {
+ requestID: data.requestID,
+ result: result
+ });
+ }, error => {
+ sender.sendAsyncMessage(this._getResponseName(name, "KO"), {
+ requestID: data.requestID,
+ result: error.result,
+ });
+ }).catch(Cu.reportError);
+ },
+
+ _handleReady() {
+ this.service.init();
+ },
+
+ _toPageRecord(principal, data) {
+ if (!data.scope) {
+ throw new Error("Invalid page record: missing scope");
+ }
+ if (!principal) {
+ throw new Error("Invalid page record: missing principal");
+ }
+ if (principal.isNullPrincipal || principal.isExpandedPrincipal) {
+ throw new Error("Invalid page record: unsupported principal");
+ }
+
+ // System subscriptions can only be created by chrome callers, and are
+ // exempt from the background message quota and permission checks. They
+ // also do not fire service worker events.
+ data.systemRecord = principal.isSystemPrincipal;
+
+ data.originAttributes =
+ ChromeUtils.originAttributesToSuffix(principal.originAttributes);
+
+ return data;
+ },
+
+ _handleRequest(name, principal, data) {
+ if (name == "Push:Clear") {
+ return this.service.clear(data);
+ }
+
+ let pageRecord;
+ try {
+ pageRecord = this._toPageRecord(principal, data);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+
+ if (name === "Push:Register") {
+ return this.service.register(pageRecord);
+ }
+ if (name === "Push:Registration") {
+ return this.service.registration(pageRecord);
+ }
+ if (name === "Push:Unregister") {
+ return this.service.unregister(pageRecord);
+ }
+
+ return Promise.reject(new Error("Invalid request: unknown name"));
+ },
+
+ _getResponseName(requestName, suffix) {
+ let name = requestName.slice("Push:".length);
+ return "PushService:" + name + ":" + suffix;
+ },
+
+ // Methods used for mocking in tests.
+
+ replaceServiceBackend(options) {
+ return this.service.changeTestServer(options.serverURI, options);
+ },
+
+ restoreServiceBackend() {
+ var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
+ return this.service.changeTestServer(defaultServerURL);
+ },
+});
+
+// Used to replace the implementation with a mock.
+Object.defineProperty(PushServiceParent.prototype, "service", {
+ get() {
+ return this._service || PushService;
+ },
+ set(impl) {
+ this._service = impl;
+ },
+});
+
+/**
+ * The content process implementation of `nsIPushService`. This version
+ * uses the child message manager to forward calls to the parent process.
+ * The parent Push service instance handles the request, and responds with a
+ * message containing the result.
+ */
+function PushServiceContent() {
+ PushServiceBase.apply(this, arguments);
+ this._requests = new Map();
+ this._requestId = 0;
+}
+
+PushServiceContent.prototype = Object.create(PushServiceBase.prototype);
+
+XPCOMUtils.defineLazyServiceGetter(PushServiceContent.prototype,
+ "_mm", "@mozilla.org/childprocessmessagemanager;1",
+ "nsISyncMessageSender");
+
+Object.assign(PushServiceContent.prototype, {
+ _xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceContent),
+
+ _messages: [
+ "PushService:Register:OK",
+ "PushService:Register:KO",
+ "PushService:Registration:OK",
+ "PushService:Registration:KO",
+ "PushService:Unregister:OK",
+ "PushService:Unregister:KO",
+ "PushService:Clear:OK",
+ "PushService:Clear:KO",
+ ],
+
+ // nsIPushService methods
+
+ subscribe(scope, principal, callback) {
+ this.subscribeWithKey(scope, principal, 0, null, callback);
+ },
+
+ subscribeWithKey(scope, principal, keyLen, key, callback) {
+ let requestId = this._addRequest(callback);
+ this._mm.sendAsyncMessage("Push:Register", {
+ scope: scope,
+ appServerKey: key,
+ requestID: requestId,
+ }, null, principal);
+ },
+
+ unsubscribe(scope, principal, callback) {
+ let requestId = this._addRequest(callback);
+ this._mm.sendAsyncMessage("Push:Unregister", {
+ scope: scope,
+ requestID: requestId,
+ }, null, principal);
+ },
+
+ getSubscription(scope, principal, callback) {
+ let requestId = this._addRequest(callback);
+ this._mm.sendAsyncMessage("Push:Registration", {
+ scope: scope,
+ requestID: requestId,
+ }, null, principal);
+ },
+
+ clearForDomain(domain, callback) {
+ let requestId = this._addRequest(callback);
+ this._mm.sendAsyncMessage("Push:Clear", {
+ domain: domain,
+ requestID: requestId,
+ });
+ },
+
+ // nsIPushQuotaManager methods
+
+ notificationForOriginShown(origin) {
+ this._mm.sendAsyncMessage("Push:NotificationForOriginShown", origin);
+ },
+
+ notificationForOriginClosed(origin) {
+ this._mm.sendAsyncMessage("Push:NotificationForOriginClosed", origin);
+ },
+
+ // nsIPushErrorReporter methods
+
+ reportDeliveryError(messageId, reason) {
+ this._mm.sendAsyncMessage("Push:ReportError", {
+ messageId: messageId,
+ reason: reason,
+ });
+ },
+
+ _addRequest(data) {
+ let id = ++this._requestId;
+ this._requests.set(id, data);
+ return id;
+ },
+
+ _takeRequest(requestId) {
+ let d = this._requests.get(requestId);
+ this._requests.delete(requestId);
+ return d;
+ },
+
+ receiveMessage(message) {
+ if (!this._isValidMessage(message)) {
+ return;
+ }
+ let {name, data} = message;
+ let request = this._takeRequest(data.requestID);
+
+ if (!request) {
+ return;
+ }
+
+ switch (name) {
+ case "PushService:Register:OK":
+ case "PushService:Registration:OK":
+ this._deliverSubscription(request, data.result);
+ break;
+
+ case "PushService:Register:KO":
+ case "PushService:Registration:KO":
+ this._deliverSubscriptionError(request, data);
+ break;
+
+ case "PushService:Unregister:OK":
+ if (typeof data.result === "boolean") {
+ request.onUnsubscribe(Cr.NS_OK, data.result);
+ } else {
+ request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
+ }
+ break;
+
+ case "PushService:Unregister:KO":
+ request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
+ break;
+
+ case "PushService:Clear:OK":
+ request.onClear(Cr.NS_OK);
+ break;
+
+ case "PushService:Clear:KO":
+ request.onClear(Cr.NS_ERROR_FAILURE);
+ break;
+
+ default:
+ break;
+ }
+ },
+});
+
+/** `PushSubscription` instances are passed to all subscription callbacks. */
+function PushSubscription(props) {
+ this._props = props;
+}
+
+PushSubscription.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPushSubscription]),
+
+ /** The URL for sending messages to this subscription. */
+ get endpoint() {
+ return this._props.endpoint;
+ },
+
+ /** The last time a message was sent to this subscription. */
+ get lastPush() {
+ return this._props.lastPush;
+ },
+
+ /** The total number of messages sent to this subscription. */
+ get pushCount() {
+ return this._props.pushCount;
+ },
+
+ /** The number of remaining background messages that can be sent to this
+ * subscription, or -1 of the subscription is exempt from the quota.
+ */
+ get quota() {
+ return this._props.quota;
+ },
+
+ /**
+ * Indicates whether this subscription was created with the system principal.
+ * System subscriptions are exempt from the background message quota and
+ * permission checks.
+ */
+ get isSystemSubscription() {
+ return !!this._props.systemRecord;
+ },
+
+ /** The private key used to decrypt incoming push messages, in JWK format */
+ get p256dhPrivateKey() {
+ return this._props.p256dhPrivateKey;
+ },
+
+ /**
+ * Indicates whether this subscription is subject to the background message
+ * quota.
+ */
+ quotaApplies() {
+ return this.quota >= 0;
+ },
+
+ /**
+ * Indicates whether this subscription exceeded the background message quota,
+ * or the user revoked the notification permission. The caller must request a
+ * new subscription to continue receiving push messages.
+ */
+ isExpired() {
+ return this.quota === 0;
+ },
+
+ /**
+ * Returns a key for encrypting messages sent to this subscription. JS
+ * callers receive the key buffer as a return value, while C++ callers
+ * receive the key size and buffer as out parameters.
+ */
+ getKey(name, outKeyLen) {
+ switch (name) {
+ case "p256dh":
+ return this._getRawKey(this._props.p256dhKey, outKeyLen);
+
+ case "auth":
+ return this._getRawKey(this._props.authenticationSecret, outKeyLen);
+
+ case "appServer":
+ return this._getRawKey(this._props.appServerKey, outKeyLen);
+ }
+ return null;
+ },
+
+ _getRawKey(key, outKeyLen) {
+ if (!key) {
+ return null;
+ }
+ let rawKey = new Uint8Array(key);
+ if (outKeyLen) {
+ outKeyLen.value = rawKey.length;
+ }
+ return rawKey;
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
+ // Export the correct implementation depending on whether we're running in
+ // the parent or content process.
+ isParent ? PushServiceParent : PushServiceContent,
+]);