summaryrefslogtreecommitdiffstats
path: root/dom/push/PushComponents.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'dom/push/PushComponents.sys.mjs')
-rw-r--r--dom/push/PushComponents.sys.mjs585
1 files changed, 585 insertions, 0 deletions
diff --git a/dom/push/PushComponents.sys.mjs b/dom/push/PushComponents.sys.mjs
new file mode 100644
index 0000000000..0deb43e5fb
--- /dev/null
+++ b/dom/push/PushComponents.sys.mjs
@@ -0,0 +1,585 @@
+/* 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/. */
+
+/**
+ * This file exports XPCOM components for C++ and chrome JavaScript callers to
+ * interact with the Push service.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+var isParent =
+ Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+const lazy = {};
+
+// The default Push service implementation.
+XPCOMUtils.defineLazyGetter(lazy, "PushService", function () {
+ if (Services.prefs.getBoolPref("dom.push.enabled")) {
+ const { PushService } = ChromeUtils.importESModule(
+ "resource://gre/modules/PushService.sys.mjs"
+ );
+ PushService.init();
+ return PushService;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
+});
+
+// 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: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ "nsIPushService",
+ "nsIPushQuotaManager",
+ "nsIPushErrorReporter",
+ ]),
+
+ pushTopic: OBSERVER_TOPIC_PUSH,
+ subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
+ subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
+
+ ensureReady() {},
+
+ _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 === "android-push-service") {
+ // Load PushService immediately.
+ this.ensureReady();
+ }
+ },
+
+ _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.
+ */
+let parentInstance;
+function PushServiceParent() {
+ if (parentInstance) {
+ return parentInstance;
+ }
+ parentInstance = this;
+
+ PushServiceBase.call(this);
+}
+
+PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
+
+XPCOMUtils.defineLazyServiceGetter(
+ PushServiceParent.prototype,
+ "_mm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsISupports"
+);
+
+Object.assign(PushServiceParent.prototype, {
+ _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, [], callback);
+ },
+
+ subscribeWithKey(scope, principal, key, callback) {
+ this._handleRequest("Push:Register", principal, {
+ scope,
+ appServerKey: key,
+ })
+ .then(
+ result => {
+ this._deliverSubscription(callback, result);
+ },
+ error => {
+ this._deliverSubscriptionError(callback, error);
+ }
+ )
+ .catch(console.error);
+ },
+
+ unsubscribe(scope, principal, callback) {
+ this._handleRequest("Push:Unregister", principal, {
+ scope,
+ })
+ .then(
+ result => {
+ callback.onUnsubscribe(Cr.NS_OK, result);
+ },
+ error => {
+ callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
+ }
+ )
+ .catch(console.error);
+ },
+
+ getSubscription(scope, principal, callback) {
+ return this._handleRequest("Push:Registration", principal, {
+ scope,
+ })
+ .then(
+ result => {
+ this._deliverSubscription(callback, result);
+ },
+ error => {
+ this._deliverSubscriptionError(callback, error);
+ }
+ )
+ .catch(console.error);
+ },
+
+ clearForDomain(domain, callback) {
+ return this._handleRequest("Push:Clear", null, {
+ domain,
+ })
+ .then(
+ result => {
+ callback.onClear(Cr.NS_OK);
+ },
+ error => {
+ callback.onClear(Cr.NS_ERROR_FAILURE);
+ }
+ )
+ .catch(console.error);
+ },
+
+ // 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, target, data } = message;
+ if (name === "Push:NotificationForOriginShown") {
+ this.notificationForOriginShown(data);
+ return;
+ }
+ if (name === "Push:NotificationForOriginClosed") {
+ this.notificationForOriginClosed(data);
+ return;
+ }
+ if (name === "Push:ReportError") {
+ this.reportDeliveryError(data.messageId, data.reason);
+ return;
+ }
+ this._handleRequest(name, data.principal, data)
+ .then(
+ result => {
+ target.sendAsyncMessage(this._getResponseName(name, "OK"), {
+ requestID: data.requestID,
+ result,
+ });
+ },
+ error => {
+ target.sendAsyncMessage(this._getResponseName(name, "KO"), {
+ requestID: data.requestID,
+ result: error.result,
+ });
+ }
+ )
+ .catch(console.error);
+ },
+
+ ensureReady() {
+ 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;
+ },
+
+ async _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 || lazy.PushService;
+ },
+ set(impl) {
+ this._service = impl;
+ },
+});
+
+let contentInstance;
+/**
+ * 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() {
+ if (contentInstance) {
+ return contentInstance;
+ }
+ contentInstance = this;
+
+ 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",
+ "nsISupports"
+);
+
+Object.assign(PushServiceContent.prototype, {
+ _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, [], callback);
+ },
+
+ subscribeWithKey(scope, principal, key, callback) {
+ let requestID = this._addRequest(callback);
+ this._mm.sendAsyncMessage("Push:Register", {
+ scope,
+ appServerKey: key,
+ requestID,
+ principal,
+ });
+ },
+
+ unsubscribe(scope, principal, callback) {
+ let requestID = this._addRequest(callback);
+ this._mm.sendAsyncMessage("Push:Unregister", {
+ scope,
+ requestID,
+ principal,
+ });
+ },
+
+ getSubscription(scope, principal, callback) {
+ let requestID = this._addRequest(callback);
+ this._mm.sendAsyncMessage("Push:Registration", {
+ scope,
+ requestID,
+ principal,
+ });
+ },
+
+ clearForDomain(domain, callback) {
+ let requestID = this._addRequest(callback);
+ this._mm.sendAsyncMessage("Push:Clear", {
+ domain,
+ 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,
+ 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: ChromeUtils.generateQI(["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) {
+ switch (name) {
+ case "p256dh":
+ return this._getRawKey(this._props.p256dhKey);
+
+ case "auth":
+ return this._getRawKey(this._props.authenticationSecret);
+
+ case "appServer":
+ return this._getRawKey(this._props.appServerKey);
+ }
+ return [];
+ },
+
+ _getRawKey(key) {
+ if (!key) {
+ return [];
+ }
+ return new Uint8Array(key);
+ },
+};
+
+// Export the correct implementation depending on whether we're running in
+// the parent or content process.
+export let Service = isParent ? PushServiceParent : PushServiceContent;