Merge mozilla-central to autoland on a CLOSED TREE
[gecko.git] / dom / push / Push.sys.mjs
blobccf9f6128f03bd07084a75fdf98ef49808a4c31f
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 import { DOMRequestIpcHelper } from "resource://gre/modules/DOMRequestHelper.sys.mjs";
9 const lazy = {};
11 XPCOMUtils.defineLazyGetter(lazy, "console", () => {
12   let { ConsoleAPI } = ChromeUtils.importESModule(
13     "resource://gre/modules/Console.sys.mjs"
14   );
15   return new ConsoleAPI({
16     maxLogLevelPref: "dom.push.loglevel",
17     prefix: "Push",
18   });
19 });
21 XPCOMUtils.defineLazyServiceGetter(
22   lazy,
23   "PushService",
24   "@mozilla.org/push/Service;1",
25   "nsIPushService"
28 const PUSH_CID = Components.ID("{cde1d019-fad8-4044-b141-65fb4fb7a245}");
30 /**
31  * The Push component runs in the child process and exposes the Push API
32  * to the web application. The PushService running in the parent process is the
33  * one actually performing all operations.
34  */
35 export function Push() {
36   lazy.console.debug("Push()");
39 Push.prototype = {
40   __proto__: DOMRequestIpcHelper.prototype,
42   contractID: "@mozilla.org/push/PushManager;1",
44   classID: PUSH_CID,
46   QueryInterface: ChromeUtils.generateQI([
47     "nsIDOMGlobalPropertyInitializer",
48     "nsISupportsWeakReference",
49     "nsIObserver",
50   ]),
52   init(win) {
53     lazy.console.debug("init()");
55     this._window = win;
57     this.initDOMRequestHelper(win);
59     // Get the client principal from the window. This won't be null because the
60     // service worker should be available when accessing the push manager.
61     this._principal = win.clientPrincipal;
63     if (!this._principal) {
64       throw new Error(" The client principal of the window is not available");
65     }
67     try {
68       this._topLevelPrincipal = win.top.document.nodePrincipal;
69     } catch (error) {
70       // Accessing the top-level document might fails if cross-origin
71       this._topLevelPrincipal = undefined;
72     }
73   },
75   __init(scope) {
76     this._scope = scope;
77   },
79   askPermission() {
80     lazy.console.debug("askPermission()");
82     let hasValidTransientUserGestureActivation =
83       this._window.document.hasValidTransientUserGestureActivation;
85     return this.createPromise((resolve, reject) => {
86       let permissionDenied = () => {
87         reject(
88           new this._window.DOMException(
89             "User denied permission to use the Push API.",
90             "NotAllowedError"
91           )
92         );
93       };
95       if (
96         Services.prefs.getBoolPref("dom.push.testing.ignorePermission", false)
97       ) {
98         resolve();
99         return;
100       }
102       this._requestPermission(
103         hasValidTransientUserGestureActivation,
104         resolve,
105         permissionDenied
106       );
107     });
108   },
110   subscribe(options) {
111     lazy.console.debug("subscribe()", this._scope);
113     return this.askPermission().then(() =>
114       this.createPromise((resolve, reject) => {
115         let callback = new PushSubscriptionCallback(this, resolve, reject);
117         if (!options || options.applicationServerKey === null) {
118           lazy.PushService.subscribe(this._scope, this._principal, callback);
119           return;
120         }
122         let keyView = this._normalizeAppServerKey(options.applicationServerKey);
123         if (keyView.byteLength === 0) {
124           callback._rejectWithError(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
125           return;
126         }
127         lazy.PushService.subscribeWithKey(
128           this._scope,
129           this._principal,
130           keyView,
131           callback
132         );
133       })
134     );
135   },
137   _normalizeAppServerKey(appServerKey) {
138     let key;
139     if (typeof appServerKey == "string") {
140       try {
141         key = Cu.cloneInto(
142           ChromeUtils.base64URLDecode(appServerKey, {
143             padding: "reject",
144           }),
145           this._window
146         );
147       } catch (e) {
148         throw new this._window.DOMException(
149           "String contains an invalid character",
150           "InvalidCharacterError"
151         );
152       }
153     } else if (this._window.ArrayBuffer.isView(appServerKey)) {
154       key = appServerKey.buffer;
155     } else {
156       // `appServerKey` is an array buffer.
157       key = appServerKey;
158     }
159     return new this._window.Uint8Array(key);
160   },
162   getSubscription() {
163     lazy.console.debug("getSubscription()", this._scope);
165     return this.createPromise((resolve, reject) => {
166       let callback = new PushSubscriptionCallback(this, resolve, reject);
167       lazy.PushService.getSubscription(this._scope, this._principal, callback);
168     });
169   },
171   permissionState() {
172     lazy.console.debug("permissionState()", this._scope);
174     return this.createPromise((resolve, reject) => {
175       let permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
177       try {
178         permission = this._testPermission();
179       } catch (e) {
180         reject();
181         return;
182       }
184       let pushPermissionStatus = "prompt";
185       if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
186         pushPermissionStatus = "granted";
187       } else if (permission == Ci.nsIPermissionManager.DENY_ACTION) {
188         pushPermissionStatus = "denied";
189       }
190       resolve(pushPermissionStatus);
191     });
192   },
194   _testPermission() {
195     let permission = Services.perms.testExactPermissionFromPrincipal(
196       this._principal,
197       "desktop-notification"
198     );
199     if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
200       return permission;
201     }
202     try {
203       if (Services.prefs.getBoolPref("dom.push.testing.ignorePermission")) {
204         permission = Ci.nsIPermissionManager.ALLOW_ACTION;
205       }
206     } catch (e) {}
207     return permission;
208   },
210   _requestPermission(
211     hasValidTransientUserGestureActivation,
212     allowCallback,
213     cancelCallback
214   ) {
215     // Create an array with a single nsIContentPermissionType element.
216     let type = {
217       type: "desktop-notification",
218       options: [],
219       QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionType"]),
220     };
221     let typeArray = Cc["@mozilla.org/array;1"].createInstance(
222       Ci.nsIMutableArray
223     );
224     typeArray.appendElement(type);
226     // create a nsIContentPermissionRequest
227     let request = {
228       QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionRequest"]),
229       types: typeArray,
230       principal: this._principal,
231       hasValidTransientUserGestureActivation,
232       topLevelPrincipal: this._topLevelPrincipal,
233       allow: allowCallback,
234       cancel: cancelCallback,
235       window: this._window,
236     };
238     // Using askPermission from nsIDOMWindowUtils that takes care of the
239     // remoting if needed.
240     let windowUtils = this._window.windowUtils;
241     windowUtils.askPermission(request);
242   },
245 function PushSubscriptionCallback(pushManager, resolve, reject) {
246   this.pushManager = pushManager;
247   this.resolve = resolve;
248   this.reject = reject;
251 PushSubscriptionCallback.prototype = {
252   QueryInterface: ChromeUtils.generateQI(["nsIPushSubscriptionCallback"]),
254   onPushSubscription(ok, subscription) {
255     let { pushManager } = this;
256     if (!Components.isSuccessCode(ok)) {
257       this._rejectWithError(ok);
258       return;
259     }
261     if (!subscription) {
262       this.resolve(null);
263       return;
264     }
266     let p256dhKey = this._getKey(subscription, "p256dh");
267     let authSecret = this._getKey(subscription, "auth");
268     let options = {
269       endpoint: subscription.endpoint,
270       scope: pushManager._scope,
271       p256dhKey,
272       authSecret,
273     };
274     let appServerKey = this._getKey(subscription, "appServer");
275     if (appServerKey) {
276       // Avoid passing null keys to work around bug 1256449.
277       options.appServerKey = appServerKey;
278     }
279     let sub = new pushManager._window.PushSubscription(options);
280     this.resolve(sub);
281   },
283   _getKey(subscription, name) {
284     let rawKey = Cu.cloneInto(
285       subscription.getKey(name),
286       this.pushManager._window
287     );
288     if (!rawKey.length) {
289       return null;
290     }
292     let key = new this.pushManager._window.ArrayBuffer(rawKey.length);
293     let keyView = new this.pushManager._window.Uint8Array(key);
294     keyView.set(rawKey);
295     return key;
296   },
298   _rejectWithError(result) {
299     let error;
300     switch (result) {
301       case Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR:
302         error = new this.pushManager._window.DOMException(
303           "Invalid raw ECDSA P-256 public key.",
304           "InvalidAccessError"
305         );
306         break;
308       case Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR:
309         error = new this.pushManager._window.DOMException(
310           "A subscription with a different application server key already exists.",
311           "InvalidStateError"
312         );
313         break;
315       default:
316         error = new this.pushManager._window.DOMException(
317           "Error retrieving push subscription.",
318           "AbortError"
319         );
320     }
321     this.reject(error);
322   },