1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * This file exports XPCOM components for C++ and chrome JavaScript callers to
7 * interact with the Push service.
10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
13 Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
17 // The default Push service implementation.
18 XPCOMUtils.defineLazyGetter(lazy, "PushService", function () {
19 if (Services.prefs.getBoolPref("dom.push.enabled")) {
20 const { PushService } = ChromeUtils.importESModule(
21 "resource://gre/modules/PushService.sys.mjs"
27 throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
30 // Observer notification topics for push messages and subscription status
31 // changes. These are duplicated and used in `nsIPushNotifier`. They're exposed
32 // on `nsIPushService` so that JS callers only need to import this service.
33 const OBSERVER_TOPIC_PUSH = "push-message";
34 const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
35 const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified";
38 * `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively
39 * implement the `nsIPushService` interface. This interface provides calls
40 * similar to the Push DOM API, but does not require service workers.
42 * Push service methods may be called from the parent or content process. The
43 * parent process implementation loads `PushService.jsm` at app startup, and
44 * calls its methods directly. The content implementation forwards calls to
45 * the parent Push service via IPC.
47 * The implementations share a class and contract ID.
49 function PushServiceBase() {
50 this.wrappedJSObject = this;
54 PushServiceBase.prototype = {
55 classID: Components.ID("{daaa8d73-677e-4233-8acd-2c404bd01658}"),
56 contractID: "@mozilla.org/push/Service;1",
57 QueryInterface: ChromeUtils.generateQI([
59 "nsISupportsWeakReference",
61 "nsIPushQuotaManager",
62 "nsIPushErrorReporter",
65 pushTopic: OBSERVER_TOPIC_PUSH,
66 subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
67 subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
72 for (let message of this._messages) {
73 this._mm.addMessageListener(message, this);
77 _isValidMessage(message) {
78 return this._messages.includes(message.name);
81 observe(subject, topic, data) {
82 if (topic === "android-push-service") {
83 // Load PushService immediately.
88 _deliverSubscription(request, props) {
90 request.onPushSubscription(Cr.NS_OK, null);
93 request.onPushSubscription(Cr.NS_OK, new PushSubscription(props));
96 _deliverSubscriptionError(request, error) {
98 typeof error.result == "number" ? error.result : Cr.NS_ERROR_FAILURE;
99 request.onPushSubscription(result, null);
104 * The parent process implementation of `nsIPushService`. This version loads
105 * `PushService.jsm` at startup and calls its methods directly. It also
106 * receives and responds to requests from the content process.
109 function PushServiceParent() {
110 if (parentInstance) {
111 return parentInstance;
113 parentInstance = this;
115 PushServiceBase.call(this);
118 PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
120 XPCOMUtils.defineLazyServiceGetter(
121 PushServiceParent.prototype,
123 "@mozilla.org/parentprocessmessagemanager;1",
127 Object.assign(PushServiceParent.prototype, {
133 "Push:NotificationForOriginShown",
134 "Push:NotificationForOriginClosed",
138 // nsIPushService methods
140 subscribe(scope, principal, callback) {
141 this.subscribeWithKey(scope, principal, [], callback);
144 subscribeWithKey(scope, principal, key, callback) {
145 this._handleRequest("Push:Register", principal, {
151 this._deliverSubscription(callback, result);
154 this._deliverSubscriptionError(callback, error);
157 .catch(console.error);
160 unsubscribe(scope, principal, callback) {
161 this._handleRequest("Push:Unregister", principal, {
166 callback.onUnsubscribe(Cr.NS_OK, result);
169 callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
172 .catch(console.error);
175 getSubscription(scope, principal, callback) {
176 return this._handleRequest("Push:Registration", principal, {
181 this._deliverSubscription(callback, result);
184 this._deliverSubscriptionError(callback, error);
187 .catch(console.error);
190 clearForDomain(domain, callback) {
191 return this._handleRequest("Push:Clear", null, {
196 callback.onClear(Cr.NS_OK);
199 callback.onClear(Cr.NS_ERROR_FAILURE);
202 .catch(console.error);
205 // nsIPushQuotaManager methods
207 notificationForOriginShown(origin) {
208 this.service.notificationForOriginShown(origin);
211 notificationForOriginClosed(origin) {
212 this.service.notificationForOriginClosed(origin);
215 // nsIPushErrorReporter methods
217 reportDeliveryError(messageId, reason) {
218 this.service.reportDeliveryError(messageId, reason);
221 receiveMessage(message) {
222 if (!this._isValidMessage(message)) {
225 let { name, target, data } = message;
226 if (name === "Push:NotificationForOriginShown") {
227 this.notificationForOriginShown(data);
230 if (name === "Push:NotificationForOriginClosed") {
231 this.notificationForOriginClosed(data);
234 if (name === "Push:ReportError") {
235 this.reportDeliveryError(data.messageId, data.reason);
238 this._handleRequest(name, data.principal, data)
241 target.sendAsyncMessage(this._getResponseName(name, "OK"), {
242 requestID: data.requestID,
247 target.sendAsyncMessage(this._getResponseName(name, "KO"), {
248 requestID: data.requestID,
249 result: error.result,
253 .catch(console.error);
260 _toPageRecord(principal, data) {
262 throw new Error("Invalid page record: missing scope");
265 throw new Error("Invalid page record: missing principal");
267 if (principal.isNullPrincipal || principal.isExpandedPrincipal) {
268 throw new Error("Invalid page record: unsupported principal");
271 // System subscriptions can only be created by chrome callers, and are
272 // exempt from the background message quota and permission checks. They
273 // also do not fire service worker events.
274 data.systemRecord = principal.isSystemPrincipal;
276 data.originAttributes = ChromeUtils.originAttributesToSuffix(
277 principal.originAttributes
283 async _handleRequest(name, principal, data) {
284 if (name == "Push:Clear") {
285 return this.service.clear(data);
290 pageRecord = this._toPageRecord(principal, data);
292 return Promise.reject(e);
295 if (name === "Push:Register") {
296 return this.service.register(pageRecord);
298 if (name === "Push:Registration") {
299 return this.service.registration(pageRecord);
301 if (name === "Push:Unregister") {
302 return this.service.unregister(pageRecord);
305 return Promise.reject(new Error("Invalid request: unknown name"));
308 _getResponseName(requestName, suffix) {
309 let name = requestName.slice("Push:".length);
310 return "PushService:" + name + ":" + suffix;
313 // Methods used for mocking in tests.
315 replaceServiceBackend(options) {
316 return this.service.changeTestServer(options.serverURI, options);
319 restoreServiceBackend() {
320 var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
321 return this.service.changeTestServer(defaultServerURL);
325 // Used to replace the implementation with a mock.
326 Object.defineProperty(PushServiceParent.prototype, "service", {
328 return this._service || lazy.PushService;
331 this._service = impl;
337 * The content process implementation of `nsIPushService`. This version
338 * uses the child message manager to forward calls to the parent process.
339 * The parent Push service instance handles the request, and responds with a
340 * message containing the result.
342 function PushServiceContent() {
343 if (contentInstance) {
344 return contentInstance;
346 contentInstance = this;
348 PushServiceBase.apply(this, arguments);
349 this._requests = new Map();
353 PushServiceContent.prototype = Object.create(PushServiceBase.prototype);
355 XPCOMUtils.defineLazyServiceGetter(
356 PushServiceContent.prototype,
358 "@mozilla.org/childprocessmessagemanager;1",
362 Object.assign(PushServiceContent.prototype, {
364 "PushService:Register:OK",
365 "PushService:Register:KO",
366 "PushService:Registration:OK",
367 "PushService:Registration:KO",
368 "PushService:Unregister:OK",
369 "PushService:Unregister:KO",
370 "PushService:Clear:OK",
371 "PushService:Clear:KO",
374 // nsIPushService methods
376 subscribe(scope, principal, callback) {
377 this.subscribeWithKey(scope, principal, [], callback);
380 subscribeWithKey(scope, principal, key, callback) {
381 let requestID = this._addRequest(callback);
382 this._mm.sendAsyncMessage("Push:Register", {
390 unsubscribe(scope, principal, callback) {
391 let requestID = this._addRequest(callback);
392 this._mm.sendAsyncMessage("Push:Unregister", {
399 getSubscription(scope, principal, callback) {
400 let requestID = this._addRequest(callback);
401 this._mm.sendAsyncMessage("Push:Registration", {
408 clearForDomain(domain, callback) {
409 let requestID = this._addRequest(callback);
410 this._mm.sendAsyncMessage("Push:Clear", {
416 // nsIPushQuotaManager methods
418 notificationForOriginShown(origin) {
419 this._mm.sendAsyncMessage("Push:NotificationForOriginShown", origin);
422 notificationForOriginClosed(origin) {
423 this._mm.sendAsyncMessage("Push:NotificationForOriginClosed", origin);
426 // nsIPushErrorReporter methods
428 reportDeliveryError(messageId, reason) {
429 this._mm.sendAsyncMessage("Push:ReportError", {
436 let id = ++this._requestId;
437 this._requests.set(id, data);
441 _takeRequest(requestId) {
442 let d = this._requests.get(requestId);
443 this._requests.delete(requestId);
447 receiveMessage(message) {
448 if (!this._isValidMessage(message)) {
451 let { name, data } = message;
452 let request = this._takeRequest(data.requestID);
459 case "PushService:Register:OK":
460 case "PushService:Registration:OK":
461 this._deliverSubscription(request, data.result);
464 case "PushService:Register:KO":
465 case "PushService:Registration:KO":
466 this._deliverSubscriptionError(request, data);
469 case "PushService:Unregister:OK":
470 if (typeof data.result === "boolean") {
471 request.onUnsubscribe(Cr.NS_OK, data.result);
473 request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
477 case "PushService:Unregister:KO":
478 request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
481 case "PushService:Clear:OK":
482 request.onClear(Cr.NS_OK);
485 case "PushService:Clear:KO":
486 request.onClear(Cr.NS_ERROR_FAILURE);
495 /** `PushSubscription` instances are passed to all subscription callbacks. */
496 function PushSubscription(props) {
500 PushSubscription.prototype = {
501 QueryInterface: ChromeUtils.generateQI(["nsIPushSubscription"]),
503 /** The URL for sending messages to this subscription. */
505 return this._props.endpoint;
508 /** The last time a message was sent to this subscription. */
510 return this._props.lastPush;
513 /** The total number of messages sent to this subscription. */
515 return this._props.pushCount;
518 /** The number of remaining background messages that can be sent to this
519 * subscription, or -1 of the subscription is exempt from the quota.
522 return this._props.quota;
526 * Indicates whether this subscription was created with the system principal.
527 * System subscriptions are exempt from the background message quota and
530 get isSystemSubscription() {
531 return !!this._props.systemRecord;
534 /** The private key used to decrypt incoming push messages, in JWK format */
535 get p256dhPrivateKey() {
536 return this._props.p256dhPrivateKey;
540 * Indicates whether this subscription is subject to the background message
544 return this.quota >= 0;
548 * Indicates whether this subscription exceeded the background message quota,
549 * or the user revoked the notification permission. The caller must request a
550 * new subscription to continue receiving push messages.
553 return this.quota === 0;
557 * Returns a key for encrypting messages sent to this subscription. JS
558 * callers receive the key buffer as a return value, while C++ callers
559 * receive the key size and buffer as out parameters.
564 return this._getRawKey(this._props.p256dhKey);
567 return this._getRawKey(this._props.authenticationSecret);
570 return this._getRawKey(this._props.appServerKey);
579 return new Uint8Array(key);
583 // Export the correct implementation depending on whether we're running in
584 // the parent or content process.
585 export let Service = isParent ? PushServiceParent : PushServiceContent;