Push.sys.mjs (9544B)
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/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineLazyGetter(lazy, "console", () => { 10 return console.createInstance({ 11 maxLogLevelPref: "dom.push.loglevel", 12 prefix: "Push", 13 }); 14 }); 15 16 XPCOMUtils.defineLazyServiceGetter( 17 lazy, 18 "PushService", 19 "@mozilla.org/push/Service;1", 20 Ci.nsIPushService 21 ); 22 23 /** 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. 27 */ 28 export class Push { 29 constructor() { 30 lazy.console.debug("Push()"); 31 } 32 33 get contractID() { 34 return "@mozilla.org/push/PushManager;1"; 35 } 36 37 get classID() { 38 return Components.ID("{cde1d019-fad8-4044-b141-65fb4fb7a245}"); 39 } 40 41 get QueryInterface() { 42 return ChromeUtils.generateQI([ 43 "nsIDOMGlobalPropertyInitializer", 44 "nsISupportsWeakReference", 45 "nsIObserver", 46 ]); 47 } 48 49 init(win) { 50 lazy.console.debug("init()"); 51 52 this._window = win; 53 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; 57 58 if (!this._principal) { 59 throw new Error(" The client principal of the window is not available"); 60 } 61 62 try { 63 this._topLevelPrincipal = win.top.document.nodePrincipal; 64 } catch (error) { 65 // Accessing the top-level document might fails if cross-origin 66 this._topLevelPrincipal = undefined; 67 } 68 } 69 70 __init(scope) { 71 this._scope = scope; 72 } 73 74 askPermission() { 75 lazy.console.debug("askPermission()"); 76 77 let hasValidTransientUserGestureActivation = 78 this._window.document.hasValidTransientUserGestureActivation; 79 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) { 89 resolve(); 90 return; 91 } 92 93 let permissionDenied = () => { 94 reject( 95 new this._window.DOMException( 96 "User denied permission to use the Push API.", 97 "NotAllowedError" 98 ) 99 ); 100 }; 101 102 if ( 103 Services.prefs.getBoolPref("dom.push.testing.ignorePermission", false) 104 ) { 105 resolve(); 106 return; 107 } 108 109 this.#requestPermission( 110 hasValidTransientUserGestureActivation, 111 resolve, 112 permissionDenied 113 ); 114 }); 115 } 116 117 subscribe(options) { 118 lazy.console.debug("subscribe()", this._scope); 119 120 return this.askPermission().then( 121 () => 122 new this._window.Promise((resolve, reject) => { 123 let callback = new PushSubscriptionCallback(this, resolve, reject); 124 125 if (!options || options.applicationServerKey === null) { 126 lazy.PushService.subscribe(this._scope, this._principal, callback); 127 return; 128 } 129 130 let keyView = this.#normalizeAppServerKey( 131 options.applicationServerKey 132 ); 133 if (keyView.byteLength === 0) { 134 callback.rejectWithError(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR); 135 return; 136 } 137 lazy.PushService.subscribeWithKey( 138 this._scope, 139 this._principal, 140 keyView, 141 callback 142 ); 143 }) 144 ); 145 } 146 147 #normalizeAppServerKey(appServerKey) { 148 let key; 149 if (typeof appServerKey == "string") { 150 try { 151 key = Cu.cloneInto( 152 ChromeUtils.base64URLDecode(appServerKey, { 153 padding: "reject", 154 }), 155 this._window 156 ); 157 } catch (e) { 158 throw new this._window.DOMException( 159 "String contains an invalid character", 160 "InvalidCharacterError" 161 ); 162 } 163 } else if (this._window.ArrayBuffer.isView(appServerKey)) { 164 key = appServerKey.buffer; 165 } else { 166 // `appServerKey` is an array buffer. 167 key = appServerKey; 168 } 169 return new this._window.Uint8Array(key); 170 } 171 172 getSubscription() { 173 lazy.console.debug("getSubscription()", this._scope); 174 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); 178 }); 179 } 180 181 permissionState() { 182 lazy.console.debug("permissionState()", this._scope); 183 184 return new this._window.Promise((resolve, reject) => { 185 let permission = Ci.nsIPermissionManager.UNKNOWN_ACTION; 186 187 try { 188 permission = this.#testPermission(); 189 } catch (e) { 190 reject(); 191 return; 192 } 193 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"; 199 } 200 resolve(pushPermissionStatus); 201 }); 202 } 203 204 #testPermission() { 205 let permission = Services.perms.testExactPermissionFromPrincipal( 206 this._principal, 207 "desktop-notification" 208 ); 209 if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) { 210 return permission; 211 } 212 try { 213 if (Services.prefs.getBoolPref("dom.push.testing.ignorePermission")) { 214 permission = Ci.nsIPermissionManager.ALLOW_ACTION; 215 } 216 } catch (e) {} 217 return permission; 218 } 219 220 #requestPermission( 221 hasValidTransientUserGestureActivation, 222 allowCallback, 223 cancelCallback 224 ) { 225 // Create an array with a single nsIContentPermissionType element. 226 let type = { 227 type: "desktop-notification", 228 options: [], 229 QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionType"]), 230 }; 231 let typeArray = Cc["@mozilla.org/array;1"].createInstance( 232 Ci.nsIMutableArray 233 ); 234 typeArray.appendElement(type); 235 236 // create a nsIContentPermissionRequest 237 let request = { 238 QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionRequest"]), 239 types: typeArray, 240 principal: this._principal, 241 hasValidTransientUserGestureActivation, 242 topLevelPrincipal: this._topLevelPrincipal, 243 allow: allowCallback, 244 cancel: cancelCallback, 245 window: this._window, 246 }; 247 248 // Using askPermission from nsIDOMWindowUtils that takes care of the 249 // remoting if needed. 250 let windowUtils = this._window.windowUtils; 251 windowUtils.askPermission(request); 252 } 253 } 254 255 class PushSubscriptionCallback { 256 constructor(pushManager, resolve, reject) { 257 this.pushManager = pushManager; 258 this.resolve = resolve; 259 this.reject = reject; 260 } 261 262 get QueryInterface() { 263 return ChromeUtils.generateQI(["nsIPushSubscriptionCallback"]); 264 } 265 266 onPushSubscription(ok, subscription) { 267 let { pushManager } = this; 268 if (!Components.isSuccessCode(ok)) { 269 this.rejectWithError(ok); 270 return; 271 } 272 273 if (!subscription) { 274 this.resolve(null); 275 return; 276 } 277 278 let p256dhKey = this.#getKey(subscription, "p256dh"); 279 let authSecret = this.#getKey(subscription, "auth"); 280 let options = { 281 endpoint: subscription.endpoint, 282 scope: pushManager._scope, 283 p256dhKey, 284 authSecret, 285 }; 286 let appServerKey = this.#getKey(subscription, "appServer"); 287 if (appServerKey) { 288 // Avoid passing null keys to work around bug 1256449. 289 options.appServerKey = appServerKey; 290 } 291 let sub = new pushManager._window.PushSubscription(options); 292 this.resolve(sub); 293 } 294 295 #getKey(subscription, name) { 296 let rawKey = Cu.cloneInto( 297 subscription.getKey(name), 298 this.pushManager._window 299 ); 300 if (!rawKey.length) { 301 return null; 302 } 303 304 let key = new this.pushManager._window.ArrayBuffer(rawKey.length); 305 let keyView = new this.pushManager._window.Uint8Array(key); 306 keyView.set(rawKey); 307 return key; 308 } 309 310 rejectWithError(result) { 311 let error; 312 switch (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.", 316 "InvalidAccessError" 317 ); 318 break; 319 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.", 323 "InvalidStateError" 324 ); 325 break; 326 327 default: 328 error = new this.pushManager._window.DOMException( 329 "Error retrieving push subscription.", 330 "AbortError" 331 ); 332 } 333 this.reject(error); 334 } 335 }