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/. */
8 * This file exports XPCOM components for C++ and chrome JavaScript callers to
9 * interact with the Push service.
12 const { XPCOMUtils } = ChromeUtils.import(
13 "resource://gre/modules/XPCOMUtils.jsm"
15 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
18 Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
20 // The default Push service implementation.
21 XPCOMUtils.defineLazyGetter(this, "PushService", function() {
22 if (Services.prefs.getBoolPref("dom.push.enabled")) {
23 const { PushService } = ChromeUtils.import(
24 "resource://gre/modules/PushService.jsm"
30 throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
33 // Observer notification topics for push messages and subscription status
34 // changes. These are duplicated and used in `nsIPushNotifier`. They're exposed
35 // on `nsIPushService` so that JS callers only need to import this service.
36 const OBSERVER_TOPIC_PUSH = "push-message";
37 const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
38 const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified";
41 * `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively
42 * implement the `nsIPushService` interface. This interface provides calls
43 * similar to the Push DOM API, but does not require service workers.
45 * Push service methods may be called from the parent or content process. The
46 * parent process implementation loads `PushService.jsm` at app startup, and
47 * calls its methods directly. The content implementation forwards calls to
48 * the parent Push service via IPC.
50 * The implementations share a class and contract ID.
52 function PushServiceBase() {
53 this.wrappedJSObject = this;
57 PushServiceBase.prototype = {
58 classID: Components.ID("{daaa8d73-677e-4233-8acd-2c404bd01658}"),
59 contractID: "@mozilla.org/push/Service;1",
60 QueryInterface: ChromeUtils.generateQI([
62 "nsISupportsWeakReference",
64 "nsIPushQuotaManager",
65 "nsIPushErrorReporter",
68 pushTopic: OBSERVER_TOPIC_PUSH,
69 subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
70 subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
75 for (let message of this._messages) {
76 this._mm.addMessageListener(message, this);
80 _isValidMessage(message) {
81 return this._messages.includes(message.name);
84 observe(subject, topic, data) {
85 if (topic === "android-push-service") {
86 // Load PushService immediately.
91 _deliverSubscription(request, props) {
93 request.onPushSubscription(Cr.NS_OK, null);
96 request.onPushSubscription(Cr.NS_OK, new PushSubscription(props));
99 _deliverSubscriptionError(request, error) {
101 typeof error.result == "number" ? error.result : Cr.NS_ERROR_FAILURE;
102 request.onPushSubscription(result, null);
107 * The parent process implementation of `nsIPushService`. This version loads
108 * `PushService.jsm` at startup and calls its methods directly. It also
109 * receives and responds to requests from the content process.
112 function PushServiceParent() {
113 if (parentInstance) {
114 return parentInstance;
116 parentInstance = this;
118 PushServiceBase.call(this);
121 PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
123 XPCOMUtils.defineLazyServiceGetter(
124 PushServiceParent.prototype,
126 "@mozilla.org/parentprocessmessagemanager;1",
130 Object.assign(PushServiceParent.prototype, {
136 "Push:NotificationForOriginShown",
137 "Push:NotificationForOriginClosed",
141 // nsIPushService methods
143 subscribe(scope, principal, callback) {
144 this.subscribeWithKey(scope, principal, [], callback);
147 subscribeWithKey(scope, principal, key, callback) {
148 this._handleRequest("Push:Register", principal, {
154 this._deliverSubscription(callback, result);
157 this._deliverSubscriptionError(callback, error);
160 .catch(Cu.reportError);
163 unsubscribe(scope, principal, callback) {
164 this._handleRequest("Push:Unregister", principal, {
169 callback.onUnsubscribe(Cr.NS_OK, result);
172 callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
175 .catch(Cu.reportError);
178 getSubscription(scope, principal, callback) {
179 return this._handleRequest("Push:Registration", principal, {
184 this._deliverSubscription(callback, result);
187 this._deliverSubscriptionError(callback, error);
190 .catch(Cu.reportError);
193 clearForDomain(domain, callback) {
194 return this._handleRequest("Push:Clear", null, {
199 callback.onClear(Cr.NS_OK);
202 callback.onClear(Cr.NS_ERROR_FAILURE);
205 .catch(Cu.reportError);
208 // nsIPushQuotaManager methods
210 notificationForOriginShown(origin) {
211 this.service.notificationForOriginShown(origin);
214 notificationForOriginClosed(origin) {
215 this.service.notificationForOriginClosed(origin);
218 // nsIPushErrorReporter methods
220 reportDeliveryError(messageId, reason) {
221 this.service.reportDeliveryError(messageId, reason);
224 receiveMessage(message) {
225 if (!this._isValidMessage(message)) {
228 let { name, target, data } = message;
229 if (name === "Push:NotificationForOriginShown") {
230 this.notificationForOriginShown(data);
233 if (name === "Push:NotificationForOriginClosed") {
234 this.notificationForOriginClosed(data);
237 if (name === "Push:ReportError") {
238 this.reportDeliveryError(data.messageId, data.reason);
241 this._handleRequest(name, data.principal, data)
244 target.sendAsyncMessage(this._getResponseName(name, "OK"), {
245 requestID: data.requestID,
250 target.sendAsyncMessage(this._getResponseName(name, "KO"), {
251 requestID: data.requestID,
252 result: error.result,
256 .catch(Cu.reportError);
263 _toPageRecord(principal, data) {
265 throw new Error("Invalid page record: missing scope");
268 throw new Error("Invalid page record: missing principal");
270 if (principal.isNullPrincipal || principal.isExpandedPrincipal) {
271 throw new Error("Invalid page record: unsupported principal");
274 // System subscriptions can only be created by chrome callers, and are
275 // exempt from the background message quota and permission checks. They
276 // also do not fire service worker events.
277 data.systemRecord = principal.isSystemPrincipal;
279 data.originAttributes = ChromeUtils.originAttributesToSuffix(
280 principal.originAttributes
286 async _handleRequest(name, principal, data) {
287 if (name == "Push:Clear") {
288 return this.service.clear(data);
293 pageRecord = this._toPageRecord(principal, data);
295 return Promise.reject(e);
298 if (name === "Push:Register") {
299 return this.service.register(pageRecord);
301 if (name === "Push:Registration") {
302 return this.service.registration(pageRecord);
304 if (name === "Push:Unregister") {
305 return this.service.unregister(pageRecord);
308 return Promise.reject(new Error("Invalid request: unknown name"));
311 _getResponseName(requestName, suffix) {
312 let name = requestName.slice("Push:".length);
313 return "PushService:" + name + ":" + suffix;
316 // Methods used for mocking in tests.
318 replaceServiceBackend(options) {
319 return this.service.changeTestServer(options.serverURI, options);
322 restoreServiceBackend() {
323 var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
324 return this.service.changeTestServer(defaultServerURL);
328 // Used to replace the implementation with a mock.
329 Object.defineProperty(PushServiceParent.prototype, "service", {
331 return this._service || PushService;
334 this._service = impl;
340 * The content process implementation of `nsIPushService`. This version
341 * uses the child message manager to forward calls to the parent process.
342 * The parent Push service instance handles the request, and responds with a
343 * message containing the result.
345 function PushServiceContent() {
346 if (contentInstance) {
347 return contentInstance;
349 contentInstance = this;
351 PushServiceBase.apply(this, arguments);
352 this._requests = new Map();
356 PushServiceContent.prototype = Object.create(PushServiceBase.prototype);
358 XPCOMUtils.defineLazyServiceGetter(
359 PushServiceContent.prototype,
361 "@mozilla.org/childprocessmessagemanager;1",
365 Object.assign(PushServiceContent.prototype, {
367 "PushService:Register:OK",
368 "PushService:Register:KO",
369 "PushService:Registration:OK",
370 "PushService:Registration:KO",
371 "PushService:Unregister:OK",
372 "PushService:Unregister:KO",
373 "PushService:Clear:OK",
374 "PushService:Clear:KO",
377 // nsIPushService methods
379 subscribe(scope, principal, callback) {
380 this.subscribeWithKey(scope, principal, [], callback);
383 subscribeWithKey(scope, principal, key, callback) {
384 let requestID = this._addRequest(callback);
385 this._mm.sendAsyncMessage("Push:Register", {
393 unsubscribe(scope, principal, callback) {
394 let requestID = this._addRequest(callback);
395 this._mm.sendAsyncMessage("Push:Unregister", {
402 getSubscription(scope, principal, callback) {
403 let requestID = this._addRequest(callback);
404 this._mm.sendAsyncMessage("Push:Registration", {
411 clearForDomain(domain, callback) {
412 let requestID = this._addRequest(callback);
413 this._mm.sendAsyncMessage("Push:Clear", {
419 // nsIPushQuotaManager methods
421 notificationForOriginShown(origin) {
422 this._mm.sendAsyncMessage("Push:NotificationForOriginShown", origin);
425 notificationForOriginClosed(origin) {
426 this._mm.sendAsyncMessage("Push:NotificationForOriginClosed", origin);
429 // nsIPushErrorReporter methods
431 reportDeliveryError(messageId, reason) {
432 this._mm.sendAsyncMessage("Push:ReportError", {
439 let id = ++this._requestId;
440 this._requests.set(id, data);
444 _takeRequest(requestId) {
445 let d = this._requests.get(requestId);
446 this._requests.delete(requestId);
450 receiveMessage(message) {
451 if (!this._isValidMessage(message)) {
454 let { name, data } = message;
455 let request = this._takeRequest(data.requestID);
462 case "PushService:Register:OK":
463 case "PushService:Registration:OK":
464 this._deliverSubscription(request, data.result);
467 case "PushService:Register:KO":
468 case "PushService:Registration:KO":
469 this._deliverSubscriptionError(request, data);
472 case "PushService:Unregister:OK":
473 if (typeof data.result === "boolean") {
474 request.onUnsubscribe(Cr.NS_OK, data.result);
476 request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
480 case "PushService:Unregister:KO":
481 request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
484 case "PushService:Clear:OK":
485 request.onClear(Cr.NS_OK);
488 case "PushService:Clear:KO":
489 request.onClear(Cr.NS_ERROR_FAILURE);
498 /** `PushSubscription` instances are passed to all subscription callbacks. */
499 function PushSubscription(props) {
503 PushSubscription.prototype = {
504 QueryInterface: ChromeUtils.generateQI(["nsIPushSubscription"]),
506 /** The URL for sending messages to this subscription. */
508 return this._props.endpoint;
511 /** The last time a message was sent to this subscription. */
513 return this._props.lastPush;
516 /** The total number of messages sent to this subscription. */
518 return this._props.pushCount;
521 /** The number of remaining background messages that can be sent to this
522 * subscription, or -1 of the subscription is exempt from the quota.
525 return this._props.quota;
529 * Indicates whether this subscription was created with the system principal.
530 * System subscriptions are exempt from the background message quota and
533 get isSystemSubscription() {
534 return !!this._props.systemRecord;
537 /** The private key used to decrypt incoming push messages, in JWK format */
538 get p256dhPrivateKey() {
539 return this._props.p256dhPrivateKey;
543 * Indicates whether this subscription is subject to the background message
547 return this.quota >= 0;
551 * Indicates whether this subscription exceeded the background message quota,
552 * or the user revoked the notification permission. The caller must request a
553 * new subscription to continue receiving push messages.
556 return this.quota === 0;
560 * Returns a key for encrypting messages sent to this subscription. JS
561 * callers receive the key buffer as a return value, while C++ callers
562 * receive the key size and buffer as out parameters.
567 return this._getRawKey(this._props.p256dhKey);
570 return this._getRawKey(this._props.authenticationSecret);
573 return this._getRawKey(this._props.appServerKey);
582 return new Uint8Array(key);
586 // Export the correct implementation depending on whether we're running in
587 // the parent or content process.
588 let Service = isParent ? PushServiceParent : PushServiceContent;
590 const EXPORTED_SYMBOLS = ["Service"];