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 let { ConsoleAPI } = ChromeUtils.importESModule(
11 "resource://gre/modules/Console.sys.mjs"
13 return new ConsoleAPI({
14 maxLogLevelPref: "dom.push.loglevel",
19 XPCOMUtils.defineLazyServiceGetter(
22 "@mozilla.org/push/Service;1",
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.
33 lazy.console.debug("Push()");
37 return "@mozilla.org/push/PushManager;1";
41 return Components.ID("{cde1d019-fad8-4044-b141-65fb4fb7a245}");
44 get QueryInterface() {
45 return ChromeUtils.generateQI([
46 "nsIDOMGlobalPropertyInitializer",
47 "nsISupportsWeakReference",
53 lazy.console.debug("init()");
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");
66 this._topLevelPrincipal = win.top.document.nodePrincipal;
68 // Accessing the top-level document might fails if cross-origin
69 this._topLevelPrincipal = undefined;
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) {
96 let permissionDenied = () => {
98 new this._window.DOMException(
99 "User denied permission to use the Push API.",
106 Services.prefs.getBoolPref("dom.push.testing.ignorePermission", false)
112 this.#requestPermission(
113 hasValidTransientUserGestureActivation,
121 lazy.console.debug("subscribe()", this._scope);
123 return this.askPermission().then(
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);
133 let keyView = this.#normalizeAppServerKey(
134 options.applicationServerKey
136 if (keyView.byteLength === 0) {
137 callback.rejectWithError(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR);
140 lazy.PushService.subscribeWithKey(
150 #normalizeAppServerKey(appServerKey) {
152 if (typeof appServerKey == "string") {
155 ChromeUtils.base64URLDecode(appServerKey, {
161 throw new this._window.DOMException(
162 "String contains an invalid character",
163 "InvalidCharacterError"
166 } else if (this._window.ArrayBuffer.isView(appServerKey)) {
167 key = appServerKey.buffer;
169 // `appServerKey` is an array buffer.
172 return new this._window.Uint8Array(key);
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);
185 lazy.console.debug("permissionState()", this._scope);
187 return new this._window.Promise((resolve, reject) => {
188 let permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
191 permission = this.#testPermission();
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";
203 resolve(pushPermissionStatus);
208 let permission = Services.perms.testExactPermissionFromPrincipal(
210 "desktop-notification"
212 if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
216 if (Services.prefs.getBoolPref("dom.push.testing.ignorePermission")) {
217 permission = Ci.nsIPermissionManager.ALLOW_ACTION;
224 hasValidTransientUserGestureActivation,
228 // Create an array with a single nsIContentPermissionType element.
230 type: "desktop-notification",
232 QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionType"]),
234 let typeArray = Cc["@mozilla.org/array;1"].createInstance(
237 typeArray.appendElement(type);
239 // create a nsIContentPermissionRequest
241 QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionRequest"]),
243 principal: this._principal,
244 hasValidTransientUserGestureActivation,
245 topLevelPrincipal: this._topLevelPrincipal,
246 allow: allowCallback,
247 cancel: cancelCallback,
248 window: this._window,
251 // Using askPermission from nsIDOMWindowUtils that takes care of the
252 // remoting if needed.
253 let windowUtils = this._window.windowUtils;
254 windowUtils.askPermission(request);
258 class PushSubscriptionCallback {
259 constructor(pushManager, resolve, reject) {
260 this.pushManager = pushManager;
261 this.resolve = resolve;
262 this.reject = reject;
265 get QueryInterface() {
266 return ChromeUtils.generateQI(["nsIPushSubscriptionCallback"]);
269 onPushSubscription(ok, subscription) {
270 let { pushManager } = this;
271 if (!Components.isSuccessCode(ok)) {
272 this.rejectWithError(ok);
281 let p256dhKey = this.#getKey(subscription, "p256dh");
282 let authSecret = this.#getKey(subscription, "auth");
284 endpoint: subscription.endpoint,
285 scope: pushManager._scope,
289 let appServerKey = this.#getKey(subscription, "appServer");
291 // Avoid passing null keys to work around bug 1256449.
292 options.appServerKey = appServerKey;
294 let sub = new pushManager._window.PushSubscription(options);
298 #getKey(subscription, name) {
299 let rawKey = Cu.cloneInto(
300 subscription.getKey(name),
301 this.pushManager._window
303 if (!rawKey.length) {
307 let key = new this.pushManager._window.ArrayBuffer(rawKey.length);
308 let keyView = new this.pushManager._window.Uint8Array(key);
313 rejectWithError(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.",
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.",
331 error = new this.pushManager._window.DOMException(
332 "Error retrieving push subscription.",