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