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";
9 ChromeUtils.defineLazyGetter(lazy, "console", () => {
10 return console.createInstance({
11 maxLogLevelPref: "dom.push.loglevel",
16 XPCOMUtils.defineLazyServiceGetter(
19 "@mozilla.org/push/Service;1",
24 * The Push component runs in the child process and exposes the Push API
25 * to the web application. The PushService running in the parent process is the
26 * one actually performing all operations.
30 lazy.console.debug("Push()");
34 return "@mozilla.org/push/PushManager;1";
38 return Components.ID("{cde1d019-fad8-4044-b141-65fb4fb7a245}");
41 get QueryInterface() {
42 return ChromeUtils.generateQI([
43 "nsIDOMGlobalPropertyInitializer",
44 "nsISupportsWeakReference",
50 lazy.console.debug("init()");
54 // Get the client principal from the window. This won't be null because the
55 // service worker should be available when accessing the push manager.
56 this._principal = win.clientPrincipal;
58 if (!this._principal) {
59 throw new Error(" The client principal of the window is not available");
63 this._topLevelPrincipal = win.top.document.nodePrincipal;
65 // Accessing the top-level document might fails if cross-origin
66 this._topLevelPrincipal = undefined;
75 lazy.console.debug("askPermission()");
77 let hasValidTransientUserGestureActivation =
78 this._window.document.hasValidTransientUserGestureActivation;
80 return new this._window.Promise((resolve, reject) => {
81 // Test permission before requesting to support GeckoView:
82 // * GeckoViewPermissionChild wants to return early when requested without user activation
83 // before doing actual permission check:
84 // https://searchfox.org/mozilla-central/rev/0ba4632ee85679a1ccaf652df79c971fa7e9b9f7/mobile/android/actors/GeckoViewPermissionChild.sys.mjs#46-56
85 // which is partly because:
86 // * GeckoView test runner has no real permission check but just returns VALUE_ALLOW.
87 // https://searchfox.org/mozilla-central/rev/6e5b9a5a1edab13a1b2e2e90944b6e06b4d8149c/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java#108-123
88 if (this.#testPermission() === Ci.nsIPermissionManager.ALLOW_ACTION) {
93 let permissionDenied = () => {
95 new this._window.DOMException(
96 "User denied permission to use the Push API.",
103 Services.prefs.getBoolPref("dom.push.testing.ignorePermission", false)
109 this.#requestPermission(
110 hasValidTransientUserGestureActivation,
118 lazy.console.debug("subscribe()", this._scope);
120 return this.askPermission().then(
122 new this._window.Promise((resolve, reject) => {
123 let callback = new PushSubscriptionCallback(this, resolve, reject);
125 if (!options || options.applicationServerKey === null) {
126 lazy.PushService.subscribe(this._scope, this._principal, callback);
130 let keyView = this.#normalizeAppServerKey(
131 options.applicationServerKey
133 if (keyView.byteLength === 0) {
134 callback.rejectWithError(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
137 lazy.PushService.subscribeWithKey(
147 #normalizeAppServerKey(appServerKey) {
149 if (typeof appServerKey == "string") {
152 ChromeUtils.base64URLDecode(appServerKey, {
158 throw new this._window.DOMException(
159 "String contains an invalid character",
160 "InvalidCharacterError"
163 } else if (this._window.ArrayBuffer.isView(appServerKey)) {
164 key = appServerKey.buffer;
166 // `appServerKey` is an array buffer.
169 return new this._window.Uint8Array(key);
173 lazy.console.debug("getSubscription()", this._scope);
175 return new this._window.Promise((resolve, reject) => {
176 let callback = new PushSubscriptionCallback(this, resolve, reject);
177 lazy.PushService.getSubscription(this._scope, this._principal, callback);
182 lazy.console.debug("permissionState()", this._scope);
184 return new this._window.Promise((resolve, reject) => {
185 let permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
188 permission = this.#testPermission();
194 let pushPermissionStatus = "prompt";
195 if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
196 pushPermissionStatus = "granted";
197 } else if (permission == Ci.nsIPermissionManager.DENY_ACTION) {
198 pushPermissionStatus = "denied";
200 resolve(pushPermissionStatus);
205 let permission = Services.perms.testExactPermissionFromPrincipal(
207 "desktop-notification"
209 if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
213 if (Services.prefs.getBoolPref("dom.push.testing.ignorePermission")) {
214 permission = Ci.nsIPermissionManager.ALLOW_ACTION;
221 hasValidTransientUserGestureActivation,
225 // Create an array with a single nsIContentPermissionType element.
227 type: "desktop-notification",
229 QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionType"]),
231 let typeArray = Cc["@mozilla.org/array;1"].createInstance(
234 typeArray.appendElement(type);
236 // create a nsIContentPermissionRequest
238 QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionRequest"]),
240 principal: this._principal,
241 hasValidTransientUserGestureActivation,
242 topLevelPrincipal: this._topLevelPrincipal,
243 allow: allowCallback,
244 cancel: cancelCallback,
245 window: this._window,
248 // Using askPermission from nsIDOMWindowUtils that takes care of the
249 // remoting if needed.
250 let windowUtils = this._window.windowUtils;
251 windowUtils.askPermission(request);
255 class PushSubscriptionCallback {
256 constructor(pushManager, resolve, reject) {
257 this.pushManager = pushManager;
258 this.resolve = resolve;
259 this.reject = reject;
262 get QueryInterface() {
263 return ChromeUtils.generateQI(["nsIPushSubscriptionCallback"]);
266 onPushSubscription(ok, subscription) {
267 let { pushManager } = this;
268 if (!Components.isSuccessCode(ok)) {
269 this.rejectWithError(ok);
278 let p256dhKey = this.#getKey(subscription, "p256dh");
279 let authSecret = this.#getKey(subscription, "auth");
281 endpoint: subscription.endpoint,
282 scope: pushManager._scope,
286 let appServerKey = this.#getKey(subscription, "appServer");
288 // Avoid passing null keys to work around bug 1256449.
289 options.appServerKey = appServerKey;
291 let sub = new pushManager._window.PushSubscription(options);
295 #getKey(subscription, name) {
296 let rawKey = Cu.cloneInto(
297 subscription.getKey(name),
298 this.pushManager._window
300 if (!rawKey.length) {
304 let key = new this.pushManager._window.ArrayBuffer(rawKey.length);
305 let keyView = new this.pushManager._window.Uint8Array(key);
310 rejectWithError(result) {
313 case Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR:
314 error = new this.pushManager._window.DOMException(
315 "Invalid raw ECDSA P-256 public key.",
320 case Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR:
321 error = new this.pushManager._window.DOMException(
322 "A subscription with a different application server key already exists.",
328 error = new this.pushManager._window.DOMException(
329 "Error retrieving push subscription.",