From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/push/Push.sys.mjs | 335 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 dom/push/Push.sys.mjs (limited to 'dom/push/Push.sys.mjs') diff --git a/dom/push/Push.sys.mjs b/dom/push/Push.sys.mjs new file mode 100644 index 0000000000..07f763040f --- /dev/null +++ b/dom/push/Push.sys.mjs @@ -0,0 +1,335 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { DOMRequestIpcHelper } from "resource://gre/modules/DOMRequestHelper.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "console", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + return new ConsoleAPI({ + maxLogLevelPref: "dom.push.loglevel", + prefix: "Push", + }); +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "PushService", + "@mozilla.org/push/Service;1", + "nsIPushService" +); + +const PUSH_CID = Components.ID("{cde1d019-fad8-4044-b141-65fb4fb7a245}"); + +/** + * The Push component runs in the child process and exposes the Push API + * to the web application. The PushService running in the parent process is the + * one actually performing all operations. + */ +export function Push() { + lazy.console.debug("Push()"); +} + +Push.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + + contractID: "@mozilla.org/push/PushManager;1", + + classID: PUSH_CID, + + QueryInterface: ChromeUtils.generateQI([ + "nsIDOMGlobalPropertyInitializer", + "nsISupportsWeakReference", + "nsIObserver", + ]), + + init(win) { + lazy.console.debug("init()"); + + this._window = win; + + this.initDOMRequestHelper(win); + + // Get the client principal from the window. This won't be null because the + // service worker should be available when accessing the push manager. + this._principal = win.clientPrincipal; + + if (!this._principal) { + throw new Error(" The client principal of the window is not available"); + } + + try { + this._topLevelPrincipal = win.top.document.nodePrincipal; + } catch (error) { + // Accessing the top-level document might fails if cross-origin + this._topLevelPrincipal = undefined; + } + }, + + __init(scope) { + this._scope = scope; + }, + + askPermission() { + lazy.console.debug("askPermission()"); + + let hasValidTransientUserGestureActivation = + this._window.document.hasValidTransientUserGestureActivation; + + return this.createPromise((resolve, reject) => { + // Test permission before requesting to support GeckoView: + // * GeckoViewPermissionChild wants to return early when requested without user activation + // before doing actual permission check: + // https://searchfox.org/mozilla-central/rev/0ba4632ee85679a1ccaf652df79c971fa7e9b9f7/mobile/android/actors/GeckoViewPermissionChild.sys.mjs#46-56 + // which is partly because: + // * GeckoView test runner has no real permission check but just returns VALUE_ALLOW. + // https://searchfox.org/mozilla-central/rev/6e5b9a5a1edab13a1b2e2e90944b6e06b4d8149c/mobile/android/test_runner/src/main/java/org/mozilla/geckoview/test_runner/TestRunnerActivity.java#108-123 + if (this._testPermission() === Ci.nsIPermissionManager.ALLOW_ACTION) { + resolve(); + return; + } + + let permissionDenied = () => { + reject( + new this._window.DOMException( + "User denied permission to use the Push API.", + "NotAllowedError" + ) + ); + }; + + if ( + Services.prefs.getBoolPref("dom.push.testing.ignorePermission", false) + ) { + resolve(); + return; + } + + this._requestPermission( + hasValidTransientUserGestureActivation, + resolve, + permissionDenied + ); + }); + }, + + subscribe(options) { + lazy.console.debug("subscribe()", this._scope); + + return this.askPermission().then(() => + this.createPromise((resolve, reject) => { + let callback = new PushSubscriptionCallback(this, resolve, reject); + + if (!options || options.applicationServerKey === null) { + lazy.PushService.subscribe(this._scope, this._principal, callback); + return; + } + + let keyView = this._normalizeAppServerKey(options.applicationServerKey); + if (keyView.byteLength === 0) { + callback._rejectWithError(Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR); + return; + } + lazy.PushService.subscribeWithKey( + this._scope, + this._principal, + keyView, + callback + ); + }) + ); + }, + + _normalizeAppServerKey(appServerKey) { + let key; + if (typeof appServerKey == "string") { + try { + key = Cu.cloneInto( + ChromeUtils.base64URLDecode(appServerKey, { + padding: "reject", + }), + this._window + ); + } catch (e) { + throw new this._window.DOMException( + "String contains an invalid character", + "InvalidCharacterError" + ); + } + } else if (this._window.ArrayBuffer.isView(appServerKey)) { + key = appServerKey.buffer; + } else { + // `appServerKey` is an array buffer. + key = appServerKey; + } + return new this._window.Uint8Array(key); + }, + + getSubscription() { + lazy.console.debug("getSubscription()", this._scope); + + return this.createPromise((resolve, reject) => { + let callback = new PushSubscriptionCallback(this, resolve, reject); + lazy.PushService.getSubscription(this._scope, this._principal, callback); + }); + }, + + permissionState() { + lazy.console.debug("permissionState()", this._scope); + + return this.createPromise((resolve, reject) => { + let permission = Ci.nsIPermissionManager.UNKNOWN_ACTION; + + try { + permission = this._testPermission(); + } catch (e) { + reject(); + return; + } + + let pushPermissionStatus = "prompt"; + if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) { + pushPermissionStatus = "granted"; + } else if (permission == Ci.nsIPermissionManager.DENY_ACTION) { + pushPermissionStatus = "denied"; + } + resolve(pushPermissionStatus); + }); + }, + + _testPermission() { + let permission = Services.perms.testExactPermissionFromPrincipal( + this._principal, + "desktop-notification" + ); + if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) { + return permission; + } + try { + if (Services.prefs.getBoolPref("dom.push.testing.ignorePermission")) { + permission = Ci.nsIPermissionManager.ALLOW_ACTION; + } + } catch (e) {} + return permission; + }, + + _requestPermission( + hasValidTransientUserGestureActivation, + allowCallback, + cancelCallback + ) { + // Create an array with a single nsIContentPermissionType element. + let type = { + type: "desktop-notification", + options: [], + QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionType"]), + }; + let typeArray = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + typeArray.appendElement(type); + + // create a nsIContentPermissionRequest + let request = { + QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionRequest"]), + types: typeArray, + principal: this._principal, + hasValidTransientUserGestureActivation, + topLevelPrincipal: this._topLevelPrincipal, + allow: allowCallback, + cancel: cancelCallback, + window: this._window, + }; + + // Using askPermission from nsIDOMWindowUtils that takes care of the + // remoting if needed. + let windowUtils = this._window.windowUtils; + windowUtils.askPermission(request); + }, +}; + +function PushSubscriptionCallback(pushManager, resolve, reject) { + this.pushManager = pushManager; + this.resolve = resolve; + this.reject = reject; +} + +PushSubscriptionCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIPushSubscriptionCallback"]), + + onPushSubscription(ok, subscription) { + let { pushManager } = this; + if (!Components.isSuccessCode(ok)) { + this._rejectWithError(ok); + return; + } + + if (!subscription) { + this.resolve(null); + return; + } + + let p256dhKey = this._getKey(subscription, "p256dh"); + let authSecret = this._getKey(subscription, "auth"); + let options = { + endpoint: subscription.endpoint, + scope: pushManager._scope, + p256dhKey, + authSecret, + }; + let appServerKey = this._getKey(subscription, "appServer"); + if (appServerKey) { + // Avoid passing null keys to work around bug 1256449. + options.appServerKey = appServerKey; + } + let sub = new pushManager._window.PushSubscription(options); + this.resolve(sub); + }, + + _getKey(subscription, name) { + let rawKey = Cu.cloneInto( + subscription.getKey(name), + this.pushManager._window + ); + if (!rawKey.length) { + return null; + } + + let key = new this.pushManager._window.ArrayBuffer(rawKey.length); + let keyView = new this.pushManager._window.Uint8Array(key); + keyView.set(rawKey); + return key; + }, + + _rejectWithError(result) { + let error; + switch (result) { + case Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR: + error = new this.pushManager._window.DOMException( + "Invalid raw ECDSA P-256 public key.", + "InvalidAccessError" + ); + break; + + case Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR: + error = new this.pushManager._window.DOMException( + "A subscription with a different application server key already exists.", + "InvalidStateError" + ); + break; + + default: + error = new this.pushManager._window.DOMException( + "Error retrieving push subscription.", + "AbortError" + ); + } + this.reject(error); + }, +}; -- cgit v1.2.3