PushComponents.sys.mjs (12969B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * This file exports XPCOM components for C++ and chrome JavaScript callers to 7 * interact with the Push service. 8 */ 9 10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 11 import { ChromePushSubscription } from "./ChromePushSubscription.sys.mjs"; 12 13 var isParent = 14 Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; 15 16 const lazy = {}; 17 18 // The default Push service implementation. 19 ChromeUtils.defineLazyGetter(lazy, "PushService", function () { 20 if (Services.prefs.getBoolPref("dom.push.enabled")) { 21 const { PushService } = ChromeUtils.importESModule( 22 "resource://gre/modules/PushService.sys.mjs" 23 ); 24 PushService.init(); 25 return PushService; 26 } 27 28 throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); 29 }); 30 31 // Observer notification topics for push messages and subscription status 32 // changes. These are duplicated and used in `nsIPushNotifier`. They're exposed 33 // on `nsIPushService` so that JS callers only need to import this service. 34 const OBSERVER_TOPIC_PUSH = "push-message"; 35 const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change"; 36 const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified"; 37 38 /** 39 * `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively 40 * implement the `nsIPushService` interface. This interface provides calls 41 * similar to the Push DOM API, but does not require service workers. 42 * 43 * Push service methods may be called from the parent or content process. The 44 * parent process implementation loads `PushService.sys.mjs` at app startup, and 45 * calls its methods directly. The content implementation forwards calls to 46 * the parent Push service via IPC. 47 * 48 * The implementations share a class and contract ID. 49 */ 50 function PushServiceBase() { 51 this.wrappedJSObject = this; 52 this._addListeners(); 53 } 54 55 PushServiceBase.prototype = { 56 classID: Components.ID("{daaa8d73-677e-4233-8acd-2c404bd01658}"), 57 contractID: "@mozilla.org/push/Service;1", 58 QueryInterface: ChromeUtils.generateQI([ 59 "nsIObserver", 60 "nsISupportsWeakReference", 61 "nsIPushService", 62 "nsIPushQuotaManager", 63 "nsIPushErrorReporter", 64 ]), 65 66 pushTopic: OBSERVER_TOPIC_PUSH, 67 subscriptionChangeTopic: OBSERVER_TOPIC_SUBSCRIPTION_CHANGE, 68 subscriptionModifiedTopic: OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED, 69 70 ensureReady() {}, 71 72 _addListeners() { 73 for (let message of this._messages) { 74 this._mm.addMessageListener(message, this); 75 } 76 }, 77 78 _isValidMessage(message) { 79 return this._messages.includes(message.name); 80 }, 81 82 observe(subject, topic) { 83 if (topic === "android-push-service") { 84 // Load PushService immediately. 85 this.ensureReady(); 86 } 87 }, 88 89 _deliverSubscription(request, props) { 90 if (!props) { 91 request.onPushSubscription(Cr.NS_OK, null); 92 return; 93 } 94 request.onPushSubscription(Cr.NS_OK, new ChromePushSubscription(props)); 95 }, 96 97 _deliverSubscriptionError(request, error) { 98 let result = 99 typeof error.result == "number" ? error.result : Cr.NS_ERROR_FAILURE; 100 request.onPushSubscription(result, null); 101 }, 102 }; 103 104 /** 105 * The parent process implementation of `nsIPushService`. This version loads 106 * `PushService.sys.mjs` at startup and calls its methods directly. It also 107 * receives and responds to requests from the content process. 108 */ 109 let parentInstance; 110 function PushServiceParent() { 111 if (parentInstance) { 112 return parentInstance; 113 } 114 parentInstance = this; 115 116 PushServiceBase.call(this); 117 } 118 119 PushServiceParent.prototype = Object.create(PushServiceBase.prototype); 120 121 XPCOMUtils.defineLazyServiceGetter( 122 PushServiceParent.prototype, 123 "_mm", 124 "@mozilla.org/parentprocessmessagemanager;1", 125 Ci.nsISupports 126 ); 127 128 Object.assign(PushServiceParent.prototype, { 129 _messages: [ 130 "Push:Register", 131 "Push:Registration", 132 "Push:Unregister", 133 "Push:Clear", 134 "Push:ReportError", 135 ], 136 137 // nsIPushService methods 138 139 subscribe(scope, principal, callback) { 140 this.subscribeWithKey(scope, principal, [], callback); 141 }, 142 143 subscribeWithKey(scope, principal, key, callback) { 144 this._handleRequest("Push:Register", principal, { 145 scope, 146 appServerKey: key, 147 }) 148 .then( 149 result => { 150 this._deliverSubscription(callback, result); 151 }, 152 error => { 153 this._deliverSubscriptionError(callback, error); 154 } 155 ) 156 .catch(console.error); 157 }, 158 159 unsubscribe(scope, principal, callback) { 160 this._handleRequest("Push:Unregister", principal, { 161 scope, 162 }) 163 .then( 164 result => { 165 callback.onUnsubscribe(Cr.NS_OK, result); 166 }, 167 () => { 168 callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false); 169 } 170 ) 171 .catch(console.error); 172 }, 173 174 getSubscription(scope, principal, callback) { 175 return this._handleRequest("Push:Registration", principal, { 176 scope, 177 }) 178 .then( 179 result => { 180 this._deliverSubscription(callback, result); 181 }, 182 error => { 183 this._deliverSubscriptionError(callback, error); 184 } 185 ) 186 .catch(console.error); 187 }, 188 189 clearForDomain(domain, originAttributesPattern, callback) { 190 return this._handleRequest("Push:Clear", null, { 191 domain, 192 originAttributesPattern, 193 }) 194 .then( 195 () => { 196 callback.onClear(Cr.NS_OK); 197 }, 198 () => { 199 callback.onClear(Cr.NS_ERROR_FAILURE); 200 } 201 ) 202 .catch(console.error); 203 }, 204 205 clearForPrincipal(principal, callback) { 206 return this._handleRequest("Push:Clear", null, { 207 principal, 208 }) 209 .then( 210 () => { 211 callback.onClear(Cr.NS_OK); 212 }, 213 () => { 214 callback.onClear(Cr.NS_ERROR_FAILURE); 215 } 216 ) 217 .catch(console.error); 218 }, 219 220 // nsIPushQuotaManager methods 221 222 notificationForOriginShown(origin) { 223 this.service.notificationForOriginShown(origin); 224 }, 225 226 notificationForOriginClosed(origin) { 227 this.service.notificationForOriginClosed(origin); 228 }, 229 230 // nsIPushErrorReporter methods 231 232 reportDeliveryError(messageId, reason) { 233 this.service.reportDeliveryError(messageId, reason); 234 }, 235 236 receiveMessage(message) { 237 if (!this._isValidMessage(message)) { 238 return; 239 } 240 let { name, target, data } = message; 241 if (name === "Push:ReportError") { 242 this.reportDeliveryError(data.messageId, data.reason); 243 return; 244 } 245 this._handleRequest(name, data.principal, data) 246 .then( 247 result => { 248 target.sendAsyncMessage(this._getResponseName(name, "OK"), { 249 requestID: data.requestID, 250 result, 251 }); 252 }, 253 error => { 254 target.sendAsyncMessage(this._getResponseName(name, "KO"), { 255 requestID: data.requestID, 256 result: error.result, 257 }); 258 } 259 ) 260 .catch(console.error); 261 }, 262 263 ensureReady() { 264 this.service.init(); 265 }, 266 267 _toPageRecord(principal, data) { 268 if (!data.scope) { 269 throw new Error("Invalid page record: missing scope"); 270 } 271 if (!principal) { 272 throw new Error("Invalid page record: missing principal"); 273 } 274 if (principal.isNullPrincipal || principal.isExpandedPrincipal) { 275 throw new Error("Invalid page record: unsupported principal"); 276 } 277 278 // System subscriptions can only be created by chrome callers, and are 279 // exempt from the background message quota and permission checks. They 280 // also do not fire service worker events. 281 data.systemRecord = principal.isSystemPrincipal; 282 283 data.originAttributes = ChromeUtils.originAttributesToSuffix( 284 principal.originAttributes 285 ); 286 287 return data; 288 }, 289 290 async _handleRequest(name, principal, data) { 291 if (name == "Push:Clear") { 292 return this.service.clear(data); 293 } 294 295 let pageRecord; 296 try { 297 pageRecord = this._toPageRecord(principal, data); 298 } catch (e) { 299 return Promise.reject(e); 300 } 301 302 if (name === "Push:Register") { 303 return this.service.register(pageRecord); 304 } 305 if (name === "Push:Registration") { 306 return this.service.registration(pageRecord); 307 } 308 if (name === "Push:Unregister") { 309 return this.service.unregister(pageRecord); 310 } 311 312 return Promise.reject(new Error("Invalid request: unknown name")); 313 }, 314 315 _getResponseName(requestName, suffix) { 316 let name = requestName.slice("Push:".length); 317 return "PushService:" + name + ":" + suffix; 318 }, 319 320 // Methods used for mocking in tests. 321 322 replaceServiceBackend(options) { 323 return this.service.changeTestServer(options.serverURI, options); 324 }, 325 326 restoreServiceBackend() { 327 var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL"); 328 return this.service.changeTestServer(defaultServerURL); 329 }, 330 }); 331 332 // Used to replace the implementation with a mock. 333 Object.defineProperty(PushServiceParent.prototype, "service", { 334 get() { 335 return this._service || lazy.PushService; 336 }, 337 set(impl) { 338 this._service = impl; 339 }, 340 }); 341 342 let contentInstance; 343 /** 344 * The content process implementation of `nsIPushService`. This version 345 * uses the child message manager to forward calls to the parent process. 346 * The parent Push service instance handles the request, and responds with a 347 * message containing the result. 348 */ 349 function PushServiceContent() { 350 if (contentInstance) { 351 return contentInstance; 352 } 353 contentInstance = this; 354 355 PushServiceBase.apply(this, arguments); 356 this._requests = new Map(); 357 this._requestId = 0; 358 } 359 360 PushServiceContent.prototype = Object.create(PushServiceBase.prototype); 361 362 XPCOMUtils.defineLazyServiceGetter( 363 PushServiceContent.prototype, 364 "_mm", 365 "@mozilla.org/childprocessmessagemanager;1", 366 Ci.nsISupports 367 ); 368 369 Object.assign(PushServiceContent.prototype, { 370 _messages: [ 371 "PushService:Register:OK", 372 "PushService:Register:KO", 373 "PushService:Registration:OK", 374 "PushService:Registration:KO", 375 "PushService:Unregister:OK", 376 "PushService:Unregister:KO", 377 "PushService:Clear:OK", 378 "PushService:Clear:KO", 379 ], 380 381 // nsIPushService methods 382 383 subscribe(scope, principal, callback) { 384 this.subscribeWithKey(scope, principal, [], callback); 385 }, 386 387 subscribeWithKey(scope, principal, key, callback) { 388 let requestID = this._addRequest(callback); 389 this._mm.sendAsyncMessage("Push:Register", { 390 scope, 391 appServerKey: key, 392 requestID, 393 principal, 394 }); 395 }, 396 397 unsubscribe(scope, principal, callback) { 398 let requestID = this._addRequest(callback); 399 this._mm.sendAsyncMessage("Push:Unregister", { 400 scope, 401 requestID, 402 principal, 403 }); 404 }, 405 406 getSubscription(scope, principal, callback) { 407 let requestID = this._addRequest(callback); 408 this._mm.sendAsyncMessage("Push:Registration", { 409 scope, 410 requestID, 411 principal, 412 }); 413 }, 414 415 clearForDomain(domain, callback) { 416 let requestID = this._addRequest(callback); 417 this._mm.sendAsyncMessage("Push:Clear", { 418 domain, 419 requestID, 420 }); 421 }, 422 423 // nsIPushErrorReporter methods 424 425 reportDeliveryError(messageId, reason) { 426 this._mm.sendAsyncMessage("Push:ReportError", { 427 messageId, 428 reason, 429 }); 430 }, 431 432 _addRequest(data) { 433 let id = ++this._requestId; 434 this._requests.set(id, data); 435 return id; 436 }, 437 438 _takeRequest(requestId) { 439 let d = this._requests.get(requestId); 440 this._requests.delete(requestId); 441 return d; 442 }, 443 444 receiveMessage(message) { 445 if (!this._isValidMessage(message)) { 446 return; 447 } 448 let { name, data } = message; 449 let request = this._takeRequest(data.requestID); 450 451 if (!request) { 452 return; 453 } 454 455 switch (name) { 456 case "PushService:Register:OK": 457 case "PushService:Registration:OK": 458 this._deliverSubscription(request, data.result); 459 break; 460 461 case "PushService:Register:KO": 462 case "PushService:Registration:KO": 463 this._deliverSubscriptionError(request, data); 464 break; 465 466 case "PushService:Unregister:OK": 467 if (typeof data.result === "boolean") { 468 request.onUnsubscribe(Cr.NS_OK, data.result); 469 } else { 470 request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false); 471 } 472 break; 473 474 case "PushService:Unregister:KO": 475 request.onUnsubscribe(Cr.NS_ERROR_FAILURE, false); 476 break; 477 478 case "PushService:Clear:OK": 479 request.onClear(Cr.NS_OK); 480 break; 481 482 case "PushService:Clear:KO": 483 request.onClear(Cr.NS_ERROR_FAILURE); 484 break; 485 486 default: 487 break; 488 } 489 }, 490 }); 491 492 // Export the correct implementation depending on whether we're running in 493 // the parent or content process. 494 export let Service = isParent ? PushServiceParent : PushServiceContent;