Bug 1685822 [wpt PR 27117] - [Import Maps] Add tests for rejecting multiple import...
[gecko.git] / dom / push / PushComponents.jsm
blob3f8633d807d9038ea7245461947e5fabc47c1345
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/. */
5 "use strict";
7 /**
8  * This file exports XPCOM components for C++ and chrome JavaScript callers to
9  * interact with the Push service.
10  */
12 const { XPCOMUtils } = ChromeUtils.import(
13   "resource://gre/modules/XPCOMUtils.jsm"
15 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
17 var isParent =
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"
25     );
26     PushService.init();
27     return PushService;
28   }
30   throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
31 });
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";
40 /**
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.
44  *
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.
49  *
50  * The implementations share a class and contract ID.
51  */
52 function PushServiceBase() {
53   this.wrappedJSObject = this;
54   this._addListeners();
57 PushServiceBase.prototype = {
58   classID: Components.ID("{daaa8d73-677e-4233-8acd-2c404bd01658}"),
59   contractID: "@mozilla.org/push/Service;1",
60   QueryInterface: ChromeUtils.generateQI([
61     "nsIObserver",
62     "nsISupportsWeakReference",
63     "nsIPushService",
64     "nsIPushQuotaManager",
65     "nsIPushErrorReporter",
66   ]),
68   pushTopic: OBSERVER_TOPIC_PUSH,
69   subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
70   subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
72   ensureReady() {},
74   _addListeners() {
75     for (let message of this._messages) {
76       this._mm.addMessageListener(message, this);
77     }
78   },
80   _isValidMessage(message) {
81     return this._messages.includes(message.name);
82   },
84   observe(subject, topic, data) {
85     if (topic === "android-push-service") {
86       // Load PushService immediately.
87       this.ensureReady();
88     }
89   },
91   _deliverSubscription(request, props) {
92     if (!props) {
93       request.onPushSubscription(Cr.NS_OK, null);
94       return;
95     }
96     request.onPushSubscription(Cr.NS_OK, new PushSubscription(props));
97   },
99   _deliverSubscriptionError(request, error) {
100     let result =
101       typeof error.result == "number" ? error.result : Cr.NS_ERROR_FAILURE;
102     request.onPushSubscription(result, null);
103   },
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.
110  */
111 let parentInstance;
112 function PushServiceParent() {
113   if (parentInstance) {
114     return parentInstance;
115   }
116   parentInstance = this;
118   PushServiceBase.call(this);
121 PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
123 XPCOMUtils.defineLazyServiceGetter(
124   PushServiceParent.prototype,
125   "_mm",
126   "@mozilla.org/parentprocessmessagemanager;1",
127   "nsISupports"
130 Object.assign(PushServiceParent.prototype, {
131   _messages: [
132     "Push:Register",
133     "Push:Registration",
134     "Push:Unregister",
135     "Push:Clear",
136     "Push:NotificationForOriginShown",
137     "Push:NotificationForOriginClosed",
138     "Push:ReportError",
139   ],
141   // nsIPushService methods
143   subscribe(scope, principal, callback) {
144     this.subscribeWithKey(scope, principal, [], callback);
145   },
147   subscribeWithKey(scope, principal, key, callback) {
148     this._handleRequest("Push:Register", principal, {
149       scope,
150       appServerKey: key,
151     })
152       .then(
153         result => {
154           this._deliverSubscription(callback, result);
155         },
156         error => {
157           this._deliverSubscriptionError(callback, error);
158         }
159       )
160       .catch(Cu.reportError);
161   },
163   unsubscribe(scope, principal, callback) {
164     this._handleRequest("Push:Unregister", principal, {
165       scope,
166     })
167       .then(
168         result => {
169           callback.onUnsubscribe(Cr.NS_OK, result);
170         },
171         error => {
172           callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
173         }
174       )
175       .catch(Cu.reportError);
176   },
178   getSubscription(scope, principal, callback) {
179     return this._handleRequest("Push:Registration", principal, {
180       scope,
181     })
182       .then(
183         result => {
184           this._deliverSubscription(callback, result);
185         },
186         error => {
187           this._deliverSubscriptionError(callback, error);
188         }
189       )
190       .catch(Cu.reportError);
191   },
193   clearForDomain(domain, callback) {
194     return this._handleRequest("Push:Clear", null, {
195       domain,
196     })
197       .then(
198         result => {
199           callback.onClear(Cr.NS_OK);
200         },
201         error => {
202           callback.onClear(Cr.NS_ERROR_FAILURE);
203         }
204       )
205       .catch(Cu.reportError);
206   },
208   // nsIPushQuotaManager methods
210   notificationForOriginShown(origin) {
211     this.service.notificationForOriginShown(origin);
212   },
214   notificationForOriginClosed(origin) {
215     this.service.notificationForOriginClosed(origin);
216   },
218   // nsIPushErrorReporter methods
220   reportDeliveryError(messageId, reason) {
221     this.service.reportDeliveryError(messageId, reason);
222   },
224   receiveMessage(message) {
225     if (!this._isValidMessage(message)) {
226       return;
227     }
228     let { name, target, data } = message;
229     if (name === "Push:NotificationForOriginShown") {
230       this.notificationForOriginShown(data);
231       return;
232     }
233     if (name === "Push:NotificationForOriginClosed") {
234       this.notificationForOriginClosed(data);
235       return;
236     }
237     if (name === "Push:ReportError") {
238       this.reportDeliveryError(data.messageId, data.reason);
239       return;
240     }
241     this._handleRequest(name, data.principal, data)
242       .then(
243         result => {
244           target.sendAsyncMessage(this._getResponseName(name, "OK"), {
245             requestID: data.requestID,
246             result,
247           });
248         },
249         error => {
250           target.sendAsyncMessage(this._getResponseName(name, "KO"), {
251             requestID: data.requestID,
252             result: error.result,
253           });
254         }
255       )
256       .catch(Cu.reportError);
257   },
259   ensureReady() {
260     this.service.init();
261   },
263   _toPageRecord(principal, data) {
264     if (!data.scope) {
265       throw new Error("Invalid page record: missing scope");
266     }
267     if (!principal) {
268       throw new Error("Invalid page record: missing principal");
269     }
270     if (principal.isNullPrincipal || principal.isExpandedPrincipal) {
271       throw new Error("Invalid page record: unsupported principal");
272     }
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
281     );
283     return data;
284   },
286   async _handleRequest(name, principal, data) {
287     if (name == "Push:Clear") {
288       return this.service.clear(data);
289     }
291     let pageRecord;
292     try {
293       pageRecord = this._toPageRecord(principal, data);
294     } catch (e) {
295       return Promise.reject(e);
296     }
298     if (name === "Push:Register") {
299       return this.service.register(pageRecord);
300     }
301     if (name === "Push:Registration") {
302       return this.service.registration(pageRecord);
303     }
304     if (name === "Push:Unregister") {
305       return this.service.unregister(pageRecord);
306     }
308     return Promise.reject(new Error("Invalid request: unknown name"));
309   },
311   _getResponseName(requestName, suffix) {
312     let name = requestName.slice("Push:".length);
313     return "PushService:" + name + ":" + suffix;
314   },
316   // Methods used for mocking in tests.
318   replaceServiceBackend(options) {
319     return this.service.changeTestServer(options.serverURI, options);
320   },
322   restoreServiceBackend() {
323     var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
324     return this.service.changeTestServer(defaultServerURL);
325   },
328 // Used to replace the implementation with a mock.
329 Object.defineProperty(PushServiceParent.prototype, "service", {
330   get() {
331     return this._service || PushService;
332   },
333   set(impl) {
334     this._service = impl;
335   },
338 let contentInstance;
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.
344  */
345 function PushServiceContent() {
346   if (contentInstance) {
347     return contentInstance;
348   }
349   contentInstance = this;
351   PushServiceBase.apply(this, arguments);
352   this._requests = new Map();
353   this._requestId = 0;
356 PushServiceContent.prototype = Object.create(PushServiceBase.prototype);
358 XPCOMUtils.defineLazyServiceGetter(
359   PushServiceContent.prototype,
360   "_mm",
361   "@mozilla.org/childprocessmessagemanager;1",
362   "nsISupports"
365 Object.assign(PushServiceContent.prototype, {
366   _messages: [
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",
375   ],
377   // nsIPushService methods
379   subscribe(scope, principal, callback) {
380     this.subscribeWithKey(scope, principal, [], callback);
381   },
383   subscribeWithKey(scope, principal, key, callback) {
384     let requestID = this._addRequest(callback);
385     this._mm.sendAsyncMessage("Push:Register", {
386       scope,
387       appServerKey: key,
388       requestID,
389       principal,
390     });
391   },
393   unsubscribe(scope, principal, callback) {
394     let requestID = this._addRequest(callback);
395     this._mm.sendAsyncMessage("Push:Unregister", {
396       scope,
397       requestID,
398       principal,
399     });
400   },
402   getSubscription(scope, principal, callback) {
403     let requestID = this._addRequest(callback);
404     this._mm.sendAsyncMessage("Push:Registration", {
405       scope,
406       requestID,
407       principal,
408     });
409   },
411   clearForDomain(domain, callback) {
412     let requestID = this._addRequest(callback);
413     this._mm.sendAsyncMessage("Push:Clear", {
414       domain,
415       requestID,
416     });
417   },
419   // nsIPushQuotaManager methods
421   notificationForOriginShown(origin) {
422     this._mm.sendAsyncMessage("Push:NotificationForOriginShown", origin);
423   },
425   notificationForOriginClosed(origin) {
426     this._mm.sendAsyncMessage("Push:NotificationForOriginClosed", origin);
427   },
429   // nsIPushErrorReporter methods
431   reportDeliveryError(messageId, reason) {
432     this._mm.sendAsyncMessage("Push:ReportError", {
433       messageId,
434       reason,
435     });
436   },
438   _addRequest(data) {
439     let id = ++this._requestId;
440     this._requests.set(id, data);
441     return id;
442   },
444   _takeRequest(requestId) {
445     let d = this._requests.get(requestId);
446     this._requests.delete(requestId);
447     return d;
448   },
450   receiveMessage(message) {
451     if (!this._isValidMessage(message)) {
452       return;
453     }
454     let { name, data } = message;
455     let request = this._takeRequest(data.requestID);
457     if (!request) {
458       return;
459     }
461     switch (name) {
462       case "PushService:Register:OK":
463       case "PushService:Registration:OK":
464         this._deliverSubscription(request, data.result);
465         break;
467       case "PushService:Register:KO":
468       case "PushService:Registration:KO":
469         this._deliverSubscriptionError(request, data);
470         break;
472       case "PushService:Unregister:OK":
473         if (typeof data.result === "boolean") {
474           request.onUnsubscribe(Cr.NS_OK, data.result);
475         } else {
476           request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
477         }
478         break;
480       case "PushService:Unregister:KO":
481         request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false);
482         break;
484       case "PushService:Clear:OK":
485         request.onClear(Cr.NS_OK);
486         break;
488       case "PushService:Clear:KO":
489         request.onClear(Cr.NS_ERROR_FAILURE);
490         break;
492       default:
493         break;
494     }
495   },
498 /** `PushSubscription` instances are passed to all subscription callbacks. */
499 function PushSubscription(props) {
500   this._props = props;
503 PushSubscription.prototype = {
504   QueryInterface: ChromeUtils.generateQI(["nsIPushSubscription"]),
506   /** The URL for sending messages to this subscription. */
507   get endpoint() {
508     return this._props.endpoint;
509   },
511   /** The last time a message was sent to this subscription. */
512   get lastPush() {
513     return this._props.lastPush;
514   },
516   /** The total number of messages sent to this subscription. */
517   get pushCount() {
518     return this._props.pushCount;
519   },
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.
523    */
524   get quota() {
525     return this._props.quota;
526   },
528   /**
529    * Indicates whether this subscription was created with the system principal.
530    * System subscriptions are exempt from the background message quota and
531    * permission checks.
532    */
533   get isSystemSubscription() {
534     return !!this._props.systemRecord;
535   },
537   /** The private key used to decrypt incoming push messages, in JWK format */
538   get p256dhPrivateKey() {
539     return this._props.p256dhPrivateKey;
540   },
542   /**
543    * Indicates whether this subscription is subject to the background message
544    * quota.
545    */
546   quotaApplies() {
547     return this.quota >= 0;
548   },
550   /**
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.
554    */
555   isExpired() {
556     return this.quota === 0;
557   },
559   /**
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.
563    */
564   getKey(name) {
565     switch (name) {
566       case "p256dh":
567         return this._getRawKey(this._props.p256dhKey);
569       case "auth":
570         return this._getRawKey(this._props.authenticationSecret);
572       case "appServer":
573         return this._getRawKey(this._props.appServerKey);
574     }
575     return [];
576   },
578   _getRawKey(key) {
579     if (!key) {
580       return [];
581     }
582     return new Uint8Array(key);
583   },
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"];