Bug 1885602 - Part 5: Implement navigating to the SUMO help topic from the menu heade...
[gecko.git] / dom / push / Push.sys.mjs
blob2e55d1dc892d5215b4895cb9678df03ac7ff9c88
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 const lazy = {};
9 ChromeUtils.defineLazyGetter(lazy, "console", () => {
10   let { ConsoleAPI } = ChromeUtils.importESModule(
11     "resource://gre/modules/Console.sys.mjs"
12   );
13   return new ConsoleAPI({
14     maxLogLevelPref: "dom.push.loglevel",
15     prefix: "Push",
16   });
17 });
19 XPCOMUtils.defineLazyServiceGetter(
20   lazy,
21   "PushService",
22   "@mozilla.org/push/Service;1",
23   "nsIPushService"
26 /**
27  * The Push component runs in the child process and exposes the Push API
28  * to the web application. The PushService running in the parent process is the
29  * one actually performing all operations.
30  */
31 export class Push {
32   constructor() {
33     lazy.console.debug("Push()");
34   }
36   get contractID() {
37     return "@mozilla.org/push/PushManager;1";
38   }
40   get classID() {
41     return Components.ID("{cde1d019-fad8-4044-b141-65fb4fb7a245}");
42   }
44   get QueryInterface() {
45     return ChromeUtils.generateQI([
46       "nsIDOMGlobalPropertyInitializer",
47       "nsISupportsWeakReference",
48       "nsIObserver",
49     ]);
50   }
52   init(win) {
53     lazy.console.debug("init()");
55     this._window = win;
57     // Get the client principal from the window. This won't be null because the
58     // service worker should be available when accessing the push manager.
59     this._principal = win.clientPrincipal;
61     if (!this._principal) {
62       throw new Error(" The client principal of the window is not available");
63     }
65     try {
66       this._topLevelPrincipal = win.top.document.nodePrincipal;
67     } catch (error) {
68       // Accessing the top-level document might fails if cross-origin
69       this._topLevelPrincipal = undefined;
70     }
71   }
73   __init(scope) {
74     this._scope = scope;
75   }
77   askPermission() {
78     lazy.console.debug("askPermission()");
80     let hasValidTransientUserGestureActivation =
81       this._window.document.hasValidTransientUserGestureActivation;
83     return new this._window.Promise((resolve, reject) => {
84       // Test permission before requesting to support GeckoView:
85       // * GeckoViewPermissionChild wants to return early when requested without user activation
86       //   before doing actual permission check:
87       //   https://searchfox.org/mozilla-central/rev/0ba4632ee85679a1ccaf652df79c971fa7e9b9f7/mobile/android/actors/GeckoViewPermissionChild.sys.mjs#46-56
88       //   which is partly because:
89       // * GeckoView test runner has no real permission check but just returns VALUE_ALLOW.
90       //   https://searchfox.org/mozilla-central/rev/6e5b9a5a1edab13a1b2e2e90944b6e06b4d8149c/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java#108-123
91       if (this.#testPermission() === Ci.nsIPermissionManager.ALLOW_ACTION) {
92         resolve();
93         return;
94       }
96       let permissionDenied = () => {
97         reject(
98           new this._window.DOMException(
99             "User denied permission to use the Push API.",
100             "NotAllowedError"
101           )
102         );
103       };
105       if (
106         Services.prefs.getBoolPref("dom.push.testing.ignorePermission", false)
107       ) {
108         resolve();
109         return;
110       }
112       this.#requestPermission(
113         hasValidTransientUserGestureActivation,
114         resolve,
115         permissionDenied
116       );
117     });
118   }
120   subscribe(options) {
121     lazy.console.debug("subscribe()", this._scope);
123     return this.askPermission().then(
124       () =>
125         new this._window.Promise((resolve, reject) => {
126           let callback = new PushSubscriptionCallback(this, resolve, reject);
128           if (!options || options.applicationServerKey === null) {
129             lazy.PushService.subscribe(this._scope, this._principal, callback);
130             return;
131           }
133           let keyView = this.#normalizeAppServerKey(
134             options.applicationServerKey
135           );
136           if (keyView.byteLength === 0) {
137             callback.rejectWithError(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
138             return;
139           }
140           lazy.PushService.subscribeWithKey(
141             this._scope,
142             this._principal,
143             keyView,
144             callback
145           );
146         })
147     );
148   }
150   #normalizeAppServerKey(appServerKey) {
151     let key;
152     if (typeof appServerKey == "string") {
153       try {
154         key = Cu.cloneInto(
155           ChromeUtils.base64URLDecode(appServerKey, {
156             padding: "reject",
157           }),
158           this._window
159         );
160       } catch (e) {
161         throw new this._window.DOMException(
162           "String contains an invalid character",
163           "InvalidCharacterError"
164         );
165       }
166     } else if (this._window.ArrayBuffer.isView(appServerKey)) {
167       key = appServerKey.buffer;
168     } else {
169       // `appServerKey` is an array buffer.
170       key = appServerKey;
171     }
172     return new this._window.Uint8Array(key);
173   }
175   getSubscription() {
176     lazy.console.debug("getSubscription()", this._scope);
178     return new this._window.Promise((resolve, reject) => {
179       let callback = new PushSubscriptionCallback(this, resolve, reject);
180       lazy.PushService.getSubscription(this._scope, this._principal, callback);
181     });
182   }
184   permissionState() {
185     lazy.console.debug("permissionState()", this._scope);
187     return new this._window.Promise((resolve, reject) => {
188       let permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
190       try {
191         permission = this.#testPermission();
192       } catch (e) {
193         reject();
194         return;
195       }
197       let pushPermissionStatus = "prompt";
198       if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
199         pushPermissionStatus = "granted";
200       } else if (permission == Ci.nsIPermissionManager.DENY_ACTION) {
201         pushPermissionStatus = "denied";
202       }
203       resolve(pushPermissionStatus);
204     });
205   }
207   #testPermission() {
208     let permission = Services.perms.testExactPermissionFromPrincipal(
209       this._principal,
210       "desktop-notification"
211     );
212     if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
213       return permission;
214     }
215     try {
216       if (Services.prefs.getBoolPref("dom.push.testing.ignorePermission")) {
217         permission = Ci.nsIPermissionManager.ALLOW_ACTION;
218       }
219     } catch (e) {}
220     return permission;
221   }
223   #requestPermission(
224     hasValidTransientUserGestureActivation,
225     allowCallback,
226     cancelCallback
227   ) {
228     // Create an array with a single nsIContentPermissionType element.
229     let type = {
230       type: "desktop-notification",
231       options: [],
232       QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionType"]),
233     };
234     let typeArray = Cc["@mozilla.org/array;1"].createInstance(
235       Ci.nsIMutableArray
236     );
237     typeArray.appendElement(type);
239     // create a nsIContentPermissionRequest
240     let request = {
241       QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionRequest"]),
242       types: typeArray,
243       principal: this._principal,
244       hasValidTransientUserGestureActivation,
245       topLevelPrincipal: this._topLevelPrincipal,
246       allow: allowCallback,
247       cancel: cancelCallback,
248       window: this._window,
249     };
251     // Using askPermission from nsIDOMWindowUtils that takes care of the
252     // remoting if needed.
253     let windowUtils = this._window.windowUtils;
254     windowUtils.askPermission(request);
255   }
258 class PushSubscriptionCallback {
259   constructor(pushManager, resolve, reject) {
260     this.pushManager = pushManager;
261     this.resolve = resolve;
262     this.reject = reject;
263   }
265   get QueryInterface() {
266     return ChromeUtils.generateQI(["nsIPushSubscriptionCallback"]);
267   }
269   onPushSubscription(ok, subscription) {
270     let { pushManager } = this;
271     if (!Components.isSuccessCode(ok)) {
272       this.rejectWithError(ok);
273       return;
274     }
276     if (!subscription) {
277       this.resolve(null);
278       return;
279     }
281     let p256dhKey = this.#getKey(subscription, "p256dh");
282     let authSecret = this.#getKey(subscription, "auth");
283     let options = {
284       endpoint: subscription.endpoint,
285       scope: pushManager._scope,
286       p256dhKey,
287       authSecret,
288     };
289     let appServerKey = this.#getKey(subscription, "appServer");
290     if (appServerKey) {
291       // Avoid passing null keys to work around bug 1256449.
292       options.appServerKey = appServerKey;
293     }
294     let sub = new pushManager._window.PushSubscription(options);
295     this.resolve(sub);
296   }
298   #getKey(subscription, name) {
299     let rawKey = Cu.cloneInto(
300       subscription.getKey(name),
301       this.pushManager._window
302     );
303     if (!rawKey.length) {
304       return null;
305     }
307     let key = new this.pushManager._window.ArrayBuffer(rawKey.length);
308     let keyView = new this.pushManager._window.Uint8Array(key);
309     keyView.set(rawKey);
310     return key;
311   }
313   rejectWithError(result) {
314     let error;
315     switch (result) {
316       case Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR:
317         error = new this.pushManager._window.DOMException(
318           "Invalid raw ECDSA P-256 public key.",
319           "InvalidAccessError"
320         );
321         break;
323       case Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR:
324         error = new this.pushManager._window.DOMException(
325           "A subscription with a different application server key already exists.",
326           "InvalidStateError"
327         );
328         break;
330       default:
331         error = new this.pushManager._window.DOMException(
332           "Error retrieving push subscription.",
333           "AbortError"
334         );
335     }
336     this.reject(error);
337   }