diff --git a/dom/interfaces/base/nsIServiceWorkerManager.idl b/dom/interfaces/base/nsIServiceWorkerManager.idl index a6d855b1c43e..6d42505b5036 100644 --- a/dom/interfaces/base/nsIServiceWorkerManager.idl +++ b/dom/interfaces/base/nsIServiceWorkerManager.idl @@ -12,6 +12,7 @@ interface mozIDOMWindowProxy; interface nsIArray; interface nsIInterceptedChannel; interface nsIPrincipal; +interface nsIPushSubscription; interface nsIRunnable; interface nsIURI; %{C++ @@ -320,7 +321,8 @@ interface nsIServiceWorkerManager : nsISupports in ACString aScope, [optional] in Array aDataBytes); void sendPushSubscriptionChangeEvent(in ACString aOriginAttributes, - in ACString scope); + in ACString scope, + [optional] in nsIPushSubscription aOldSubscription); void addListener(in nsIServiceWorkerManagerListener aListener); diff --git a/dom/interfaces/push/nsIPushNotifier.idl b/dom/interfaces/push/nsIPushNotifier.idl index ded62832d681..c5e36b7c6709 100644 --- a/dom/interfaces/push/nsIPushNotifier.idl +++ b/dom/interfaces/push/nsIPushNotifier.idl @@ -13,6 +13,7 @@ %} interface nsIPrincipal; +interface nsIPushSubscription; /** * Fires XPCOM observer notifications and service worker events for @@ -43,7 +44,8 @@ interface nsIPushNotifier : nsISupports * `pushsubscriptionchange` event to the service worker registered for the * |scope|. */ - void notifySubscriptionChange(in ACString scope, in nsIPrincipal principal); + void notifySubscriptionChange(in ACString scope, in nsIPrincipal principal, + [optional] in nsIPushSubscription oldSubscription); /** * Fires a `push-subscription-modified` observer notification. Chrome code diff --git a/dom/push/ChromePushSubscription.sys.mjs b/dom/push/ChromePushSubscription.sys.mjs new file mode 100644 index 000000000000..84cf27b1c1f2 --- /dev/null +++ b/dom/push/ChromePushSubscription.sys.mjs @@ -0,0 +1,93 @@ +/* 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/. */ + +/** `ChromePushSubscription` instances are passed to all subscription callbacks. */ +export class ChromePushSubscription { + #props; + + constructor(props) { + this.#props = props; + } + + QueryInterface = ChromeUtils.generateQI(["nsIPushSubscription"]); + + /** The URL for sending messages to this subscription. */ + get endpoint() { + return this.#props.endpoint; + } + + /** The last time a message was sent to this subscription. */ + get lastPush() { + return this.#props.lastPush; + } + + /** The total number of messages sent to this subscription. */ + get pushCount() { + return this.#props.pushCount; + } + + /** The number of remaining background messages that can be sent to this + * subscription, or -1 of the subscription is exempt from the quota. + */ + get quota() { + return this.#props.quota; + } + + /** + * Indicates whether this subscription was created with the system principal. + * System subscriptions are exempt from the background message quota and + * permission checks. + */ + get isSystemSubscription() { + return !!this.#props.systemRecord; + } + + /** The private key used to decrypt incoming push messages, in JWK format */ + get p256dhPrivateKey() { + return this.#props.p256dhPrivateKey; + } + + /** + * Indicates whether this subscription is subject to the background message + * quota. + */ + quotaApplies() { + return this.quota >= 0; + } + + /** + * Indicates whether this subscription exceeded the background message quota, + * or the user revoked the notification permission. The caller must request a + * new subscription to continue receiving push messages. + */ + isExpired() { + return this.quota === 0; + } + + /** + * Returns a key for encrypting messages sent to this subscription. JS + * callers receive the key buffer as a return value, while C++ callers + * receive the key size and buffer as out parameters. + */ + getKey(name) { + switch (name) { + case "p256dh": + return this.#getRawKey(this.#props.p256dhKey); + + case "auth": + return this.#getRawKey(this.#props.authenticationSecret); + + case "appServer": + return this.#getRawKey(this.#props.appServerKey); + } + return []; + } + + #getRawKey(key) { + if (!key) { + return []; + } + return new Uint8Array(key); + } +} diff --git a/dom/push/PushComponents.sys.mjs b/dom/push/PushComponents.sys.mjs index 561e85de6e95..7b1657b386c0 100644 --- a/dom/push/PushComponents.sys.mjs +++ b/dom/push/PushComponents.sys.mjs @@ -8,6 +8,7 @@ */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { ChromePushSubscription } from "./ChromePushSubscription.sys.mjs"; var isParent = Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; @@ -488,96 +489,6 @@ Object.assign(PushServiceContent.prototype, { }, }); -/** `ChromePushSubscription` instances are passed to all subscription callbacks. */ -class ChromePushSubscription { - #props; - - constructor(props) { - this.#props = props; - } - - QueryInterface = ChromeUtils.generateQI(["nsIPushSubscription"]); - - /** The URL for sending messages to this subscription. */ - get endpoint() { - return this.#props.endpoint; - } - - /** The last time a message was sent to this subscription. */ - get lastPush() { - return this.#props.lastPush; - } - - /** The total number of messages sent to this subscription. */ - get pushCount() { - return this.#props.pushCount; - } - - /** The number of remaining background messages that can be sent to this - * subscription, or -1 of the subscription is exempt from the quota. - */ - get quota() { - return this.#props.quota; - } - - /** - * Indicates whether this subscription was created with the system principal. - * System subscriptions are exempt from the background message quota and - * permission checks. - */ - get isSystemSubscription() { - return !!this.#props.systemRecord; - } - - /** The private key used to decrypt incoming push messages, in JWK format */ - get p256dhPrivateKey() { - return this.#props.p256dhPrivateKey; - } - - /** - * Indicates whether this subscription is subject to the background message - * quota. - */ - quotaApplies() { - return this.quota >= 0; - } - - /** - * Indicates whether this subscription exceeded the background message quota, - * or the user revoked the notification permission. The caller must request a - * new subscription to continue receiving push messages. - */ - isExpired() { - return this.quota === 0; - } - - /** - * Returns a key for encrypting messages sent to this subscription. JS - * callers receive the key buffer as a return value, while C++ callers - * receive the key size and buffer as out parameters. - */ - getKey(name) { - switch (name) { - case "p256dh": - return this.#getRawKey(this.#props.p256dhKey); - - case "auth": - return this.#getRawKey(this.#props.authenticationSecret); - - case "appServer": - return this.#getRawKey(this.#props.appServerKey); - } - return []; - } - - #getRawKey(key) { - if (!key) { - return []; - } - return new Uint8Array(key); - } -} - // Export the correct implementation depending on whether we're running in // the parent or content process. export let Service = isParent ? PushServiceParent : PushServiceContent; diff --git a/dom/push/PushManager.cpp b/dom/push/PushManager.cpp index 878519284734..9725e71adc3a 100644 --- a/dom/push/PushManager.cpp +++ b/dom/push/PushManager.cpp @@ -33,34 +33,6 @@ namespace mozilla::dom { -namespace { - -nsresult GetPermissionState(nsIPrincipal* aPrincipal, PermissionState& aState) { - nsCOMPtr permManager = - mozilla::components::PermissionManager::Service(); - - if (!permManager) { - return NS_ERROR_FAILURE; - } - uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION; - nsresult rv = permManager->TestExactPermissionFromPrincipal( - aPrincipal, "desktop-notification"_ns, &permission); - if (NS_WARN_IF(NS_FAILED(rv))) { - return rv; - } - - if (permission == nsIPermissionManager::ALLOW_ACTION || - Preferences::GetBool("dom.push.testing.ignorePermission", false)) { - aState = PermissionState::Granted; - } else if (permission == nsIPermissionManager::DENY_ACTION) { - aState = PermissionState::Denied; - } else { - aState = PermissionState::Prompt; - } - - return NS_OK; -} - nsresult GetSubscriptionParams(nsIPushSubscription* aSubscription, nsAString& aEndpoint, nsTArray& aRawP256dhKey, @@ -91,6 +63,34 @@ nsresult GetSubscriptionParams(nsIPushSubscription* aSubscription, return NS_OK; } +namespace { + +nsresult GetPermissionState(nsIPrincipal* aPrincipal, PermissionState& aState) { + nsCOMPtr permManager = + mozilla::components::PermissionManager::Service(); + + if (!permManager) { + return NS_ERROR_FAILURE; + } + uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION; + nsresult rv = permManager->TestExactPermissionFromPrincipal( + aPrincipal, "desktop-notification"_ns, &permission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (permission == nsIPermissionManager::ALLOW_ACTION || + Preferences::GetBool("dom.push.testing.ignorePermission", false)) { + aState = PermissionState::Granted; + } else if (permission == nsIPermissionManager::DENY_ACTION) { + aState = PermissionState::Denied; + } else { + aState = PermissionState::Prompt; + } + + return NS_OK; +} + class GetSubscriptionResultRunnable final : public WorkerThreadRunnable { public: GetSubscriptionResultRunnable(WorkerPrivate* aWorkerPrivate, diff --git a/dom/push/PushManager.h b/dom/push/PushManager.h index 80a6aee9172a..c81c24239831 100644 --- a/dom/push/PushManager.h +++ b/dom/push/PushManager.h @@ -37,6 +37,7 @@ class nsIGlobalObject; class nsIPrincipal; +class nsIPushSubscription; namespace mozilla { class ErrorResult; @@ -49,6 +50,12 @@ class PushManagerImpl; struct PushSubscriptionOptionsInit; class WorkerPrivate; +nsresult GetSubscriptionParams(nsIPushSubscription* aSubscription, + nsAString& aEndpoint, + nsTArray& aRawP256dhKey, + nsTArray& aAuthSecret, + nsTArray& aAppServerKey); + class PushManager final : public nsISupports, public nsWrapperCache { public: NS_DECL_CYCLE_COLLECTING_ISUPPORTS diff --git a/dom/push/PushNotifier.cpp b/dom/push/PushNotifier.cpp index 2949034c09f5..57e8e97d2d0a 100644 --- a/dom/push/PushNotifier.cpp +++ b/dom/push/PushNotifier.cpp @@ -9,6 +9,7 @@ #include "nsContentUtils.h" #include "nsCOMPtr.h" #include "nsICategoryManager.h" +#include "nsIPushService.h" #include "nsIXULRuntime.h" #include "nsNetUtil.h" #include "nsXPCOM.h" @@ -66,9 +67,11 @@ PushNotifier::NotifyPush(const nsACString& aScope, nsIPrincipal* aPrincipal, NS_IMETHODIMP PushNotifier::NotifySubscriptionChange(const nsACString& aScope, - nsIPrincipal* aPrincipal) { + nsIPrincipal* aPrincipal, + nsIPushSubscription* aOldSubscription) { NS_ENSURE_ARG(aPrincipal); - PushSubscriptionChangeDispatcher dispatcher(aScope, aPrincipal); + PushSubscriptionChangeDispatcher dispatcher(aScope, aPrincipal, + aOldSubscription); return Dispatch(dispatcher); } @@ -311,8 +314,9 @@ bool PushMessageDispatcher::SendToChild(ContentParent* aContentActor) { } PushSubscriptionChangeDispatcher::PushSubscriptionChangeDispatcher( - const nsACString& aScope, nsIPrincipal* aPrincipal) - : PushDispatcher(aScope, aPrincipal) {} + const nsACString& aScope, nsIPrincipal* aPrincipal, + nsIPushSubscription* aOldSubscription) + : PushDispatcher(aScope, aPrincipal), mOldSubscription(aOldSubscription) {} PushSubscriptionChangeDispatcher::~PushSubscriptionChangeDispatcher() = default; @@ -334,7 +338,8 @@ nsresult PushSubscriptionChangeDispatcher::NotifyWorkers() { if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - return swm->SendPushSubscriptionChangeEvent(originSuffix, mScope); + return swm->SendPushSubscriptionChangeEvent(originSuffix, mScope, + mOldSubscription); } bool PushSubscriptionChangeDispatcher::SendToParent( diff --git a/dom/push/PushNotifier.h b/dom/push/PushNotifier.h index 3718c7674e62..6cbf14e0981b 100644 --- a/dom/push/PushNotifier.h +++ b/dom/push/PushNotifier.h @@ -149,13 +149,17 @@ class PushMessageDispatcher final : public PushDispatcher { class PushSubscriptionChangeDispatcher final : public PushDispatcher { public: PushSubscriptionChangeDispatcher(const nsACString& aScope, - nsIPrincipal* aPrincipal); + nsIPrincipal* aPrincipal, + nsIPushSubscription* aOldSubscription); ~PushSubscriptionChangeDispatcher(); nsresult NotifyObservers() override; nsresult NotifyWorkers() override; bool SendToParent(ContentChild* aParentActor) override; bool SendToChild(ContentParent* aContentActor) override; + + private: + nsCOMPtr mOldSubscription; }; class PushSubscriptionModifiedDispatcher : public PushDispatcher { diff --git a/dom/push/PushService.sys.mjs b/dom/push/PushService.sys.mjs index 4059ac2f57a2..2a79a52317da 100644 --- a/dom/push/PushService.sys.mjs +++ b/dom/push/PushService.sys.mjs @@ -4,6 +4,7 @@ import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { ChromePushSubscription } from "./ChromePushSubscription.sys.mjs"; const lazy = {}; @@ -661,7 +662,11 @@ export var PushService = { if (!record) { return; } - lazy.gPushNotifier.notifySubscriptionChange(record.scope, record.principal); + lazy.gPushNotifier.notifySubscriptionChange( + record.scope, + record.principal, + new ChromePushSubscription(record.toSubscription()) + ); }, /** diff --git a/dom/push/moz.build b/dom/push/moz.build index 93549c03d906..3e0f6a58bdb3 100644 --- a/dom/push/moz.build +++ b/dom/push/moz.build @@ -11,6 +11,7 @@ EXTRA_COMPONENTS += [ ] EXTRA_JS_MODULES += [ + "ChromePushSubscription.sys.mjs", "Push.sys.mjs", "PushBroadcastService.sys.mjs", "PushComponents.sys.mjs", diff --git a/dom/push/test/xpcshell/test_handler_service.js b/dom/push/test/xpcshell/test_handler_service.js index bee29c1c7773..164d383dfd9f 100644 --- a/dom/push/test/xpcshell/test_handler_service.js +++ b/dom/push/test/xpcshell/test_handler_service.js @@ -7,6 +7,9 @@ const { MockRegistrar } = ChromeUtils.importESModule( "resource://testing-common/MockRegistrar.sys.mjs" ); +const { ChromePushSubscription } = ChromeUtils.importESModule( + "resource://gre/modules/ChromePushSubscription.sys.mjs" +); let pushService = Cc["@mozilla.org/push/Service;1"].getService( Ci.nsIPushService @@ -54,7 +57,21 @@ add_test(function test_service_instantiation() { equal(handlerService.observed[0].data, scope); // and a subscription change. - pushNotifier.notifySubscriptionChange(scope, principal); + pushNotifier.notifySubscriptionChange( + scope, + principal, + new ChromePushSubscription({ + endpoint: "xpcshell", + lastPush: 0, + pushCount: 0, + p256dhKey: [], + p256dhPrivateKey: [], + authenticationSecret: [], + appServerKey: [], + quota: 0, + systemRecord: true, + }) + ); equal(handlerService.observed.length, 2); equal(handlerService.observed[1].topic, pushService.subscriptionChangeTopic); equal(handlerService.observed[1].subject, principal); diff --git a/dom/serviceworkers/ServiceWorkerManager.cpp b/dom/serviceworkers/ServiceWorkerManager.cpp index 909d6a8ec120..04cb51d98fa2 100644 --- a/dom/serviceworkers/ServiceWorkerManager.cpp +++ b/dom/serviceworkers/ServiceWorkerManager.cpp @@ -21,6 +21,7 @@ #include "nsServiceManagerUtils.h" #include "nsDebug.h" #include "nsIPermissionManager.h" +#include "nsIPushService.h" #include "nsXULAppAPI.h" #include "jsapi.h" @@ -1113,7 +1114,8 @@ nsresult ServiceWorkerManager::SendPushEvent( NS_IMETHODIMP ServiceWorkerManager::SendPushSubscriptionChangeEvent( - const nsACString& aOriginAttributes, const nsACString& aScope) { + const nsACString& aOriginAttributes, const nsACString& aScope, + nsIPushSubscription* aOldSubscription) { OriginAttributes attrs; if (!attrs.PopulateFromSuffix(aOriginAttributes)) { return NS_ERROR_INVALID_ARG; @@ -1123,7 +1125,8 @@ ServiceWorkerManager::SendPushSubscriptionChangeEvent( if (!info) { return NS_ERROR_FAILURE; } - return info->WorkerPrivate()->SendPushSubscriptionChangeEvent(); + return info->WorkerPrivate()->SendPushSubscriptionChangeEvent( + aOldSubscription); } nsresult ServiceWorkerManager::SendNotificationEvent( diff --git a/dom/serviceworkers/ServiceWorkerOp.cpp b/dom/serviceworkers/ServiceWorkerOp.cpp index fc59651d671f..8c8a1b4144c7 100644 --- a/dom/serviceworkers/ServiceWorkerOp.cpp +++ b/dom/serviceworkers/ServiceWorkerOp.cpp @@ -12,6 +12,8 @@ #include "js/Exception.h" // JS::ExceptionStack, JS::StealPendingExceptionStack #include "jsapi.h" +#include "mozilla/dom/PushSubscriptionChangeEvent.h" +#include "mozilla/dom/PushSubscriptionChangeEventBinding.h" #include "nsCOMPtr.h" #include "nsContentUtils.h" #include "nsDebug.h" @@ -830,12 +832,28 @@ class PushSubscriptionChangeEventOp final : public ExtendableEventOp { RefPtr target = aWorkerPrivate->GlobalScope(); - ExtendableEventInit init; + ServiceWorkerPushSubscriptionChangeEventOpArgs& args = + mArgs.get_ServiceWorkerPushSubscriptionChangeEventOpArgs(); + + PushSubscriptionChangeEventInit init; init.mBubbles = false; init.mCancelable = false; - RefPtr event = ExtendableEvent::Constructor( - target, u"pushsubscriptionchange"_ns, init); + if (args.oldSubscription()) { + PushSubscriptionData oldSubscriptionData = + args.oldSubscription().extract(); + RefPtr oldSubscription = new PushSubscription( + target->GetParentObject(), oldSubscriptionData.endpoint(), u""_ns, + Nullable(), + std::move(oldSubscriptionData.rawP256dhKey()), + std::move(oldSubscriptionData.authSecret()), + std::move(oldSubscriptionData.appServerKey())); + init.mOldSubscription = oldSubscription.forget(); + } + + RefPtr event = + PushSubscriptionChangeEvent::Constructor( + target, u"pushsubscriptionchange"_ns, init); event->SetTrusted(true); nsresult rv = DispatchExtendableEventOnWorkerScope( diff --git a/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh b/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh index aeadb033389c..9ce4b0748821 100644 --- a/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh +++ b/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh @@ -42,7 +42,16 @@ struct ServiceWorkerPushEventOpArgs { OptionalPushData data; }; -struct ServiceWorkerPushSubscriptionChangeEventOpArgs {}; +struct PushSubscriptionData { + nsString endpoint; + uint8_t[] rawP256dhKey; + uint8_t[] authSecret; + uint8_t[] appServerKey; +}; + +struct ServiceWorkerPushSubscriptionChangeEventOpArgs { + PushSubscriptionData? oldSubscription; +}; struct ServiceWorkerNotificationEventOpArgs { nsString eventName; diff --git a/dom/serviceworkers/ServiceWorkerPrivate.cpp b/dom/serviceworkers/ServiceWorkerPrivate.cpp index c776ae96b0ff..58a864fa8479 100644 --- a/dom/serviceworkers/ServiceWorkerPrivate.cpp +++ b/dom/serviceworkers/ServiceWorkerPrivate.cpp @@ -38,6 +38,7 @@ #include "mozilla/dom/FetchEventOpChild.h" #include "mozilla/dom/InternalHeaders.h" #include "mozilla/dom/InternalRequest.h" +#include "mozilla/dom/PushManager.h" #include "mozilla/dom/ReferrerInfo.h" #include "mozilla/dom/RemoteType.h" #include "mozilla/dom/RemoteWorkerControllerChild.h" @@ -970,12 +971,22 @@ nsresult ServiceWorkerPrivate::SendPushEventInternal( }); } -nsresult ServiceWorkerPrivate::SendPushSubscriptionChangeEvent() { +nsresult ServiceWorkerPrivate::SendPushSubscriptionChangeEvent( + const RefPtr& aOldSubscription) { AssertIsOnMainThread(); + ServiceWorkerPushSubscriptionChangeEventOpArgs args{}; + if (aOldSubscription) { + PushSubscriptionData oldSubscription{}; + MOZ_TRY(GetSubscriptionParams(aOldSubscription, oldSubscription.endpoint(), + oldSubscription.rawP256dhKey(), + oldSubscription.authSecret(), + oldSubscription.appServerKey())); + args.oldSubscription().emplace(oldSubscription); + } + return ExecServiceWorkerOp( - ServiceWorkerPushSubscriptionChangeEventOpArgs(), - ServiceWorkerLifetimeExtension(FullLifetimeExtension{}), + std::move(args), ServiceWorkerLifetimeExtension(FullLifetimeExtension{}), [](ServiceWorkerOpResult&& aResult) { MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult); }); diff --git a/dom/serviceworkers/ServiceWorkerPrivate.h b/dom/serviceworkers/ServiceWorkerPrivate.h index 8257d4ff20e2..edce9a32950b 100644 --- a/dom/serviceworkers/ServiceWorkerPrivate.h +++ b/dom/serviceworkers/ServiceWorkerPrivate.h @@ -30,6 +30,7 @@ #define NOTIFICATION_CLOSE_EVENT_NAME u"notificationclose" class nsIInterceptedChannel; +class nsIPushSubscription; class nsIWorkerDebugger; namespace mozilla { @@ -107,7 +108,8 @@ class ServiceWorkerPrivate final : public RemoteWorkerObserver { const Maybe>& aData, RefPtr aRegistration); - nsresult SendPushSubscriptionChangeEvent(); + nsresult SendPushSubscriptionChangeEvent( + const RefPtr& aOldSubscription); nsresult SendNotificationEvent(const nsAString& aEventName, const nsAString& aID, const nsAString& aTitle, diff --git a/testing/web-platform/mozilla/meta/push-api/__dir__.ini b/testing/web-platform/mozilla/meta/push-api/__dir__.ini new file mode 100644 index 000000000000..13e70c690f32 --- /dev/null +++ b/testing/web-platform/mozilla/meta/push-api/__dir__.ini @@ -0,0 +1 @@ +prefs: [dom.push.serverURL:wss://web-platform.test:8889/mozilla_push_dummy,dom.push.connection.enabled:true] diff --git a/testing/web-platform/mozilla/meta/push-api/pushsubscriptionchange.https.any.js.ini b/testing/web-platform/mozilla/meta/push-api/pushsubscriptionchange.https.any.js.ini new file mode 100644 index 000000000000..977758a74913 --- /dev/null +++ b/testing/web-platform/mozilla/meta/push-api/pushsubscriptionchange.https.any.js.ini @@ -0,0 +1,4 @@ +[pushsubscriptionchange.https.any.window-module.html] + [Fire pushsubscriptionchange event when permission is revoked] + expected: + if os == "android": FAIL diff --git a/testing/web-platform/mozilla/tests/push-api/push-sw.js b/testing/web-platform/mozilla/tests/push-api/push-sw.js new file mode 100644 index 000000000000..43292adcef7b --- /dev/null +++ b/testing/web-platform/mozilla/tests/push-api/push-sw.js @@ -0,0 +1,15 @@ +async function postAll(data) { + const clients = await self.clients.matchAll({ includeUncontrolled: true }); + for (const client of clients) { + client.postMessage(data); + } +} + +onpushsubscriptionchange = ev => { + postAll({ + type: ev.type, + constructor: ev.constructor.name, + oldSubscription: ev.oldSubscription?.toJSON(), + newSubscription: ev.newSubscription?.toJSON(), + }); +} diff --git a/testing/web-platform/mozilla/tests/push-api/pushsubscriptionchange.https.any.js b/testing/web-platform/mozilla/tests/push-api/pushsubscriptionchange.https.any.js new file mode 100644 index 000000000000..e13d152e5187 --- /dev/null +++ b/testing/web-platform/mozilla/tests/push-api/pushsubscriptionchange.https.any.js @@ -0,0 +1,35 @@ +// META: global=window-module +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/notifications/resources/helpers.js + +let registration; + +promise_setup(async () => { + await trySettingPermission("granted"); + registration = await getActiveServiceWorker("push-sw.js"); +}); + +promise_test(async (t) => { + const promise = new Promise(r => { + navigator.serviceWorker.addEventListener("message", r, { once: true }) + }); + + const subscription = await registration.pushManager.subscribe(); + t.add_cleanup(() => subscription.unsubscribe()); + + // https://w3c.github.io/push-api/#security-and-privacy-considerations + // When a permission is revoked, the user agent MAY fire the "pushsubscriptionchange" + // event for subscriptions created with that permission + // + // But Firefox fires pushsubscriptionchange on permission regrant instead of revocation. + // https://github.com/w3c/push-api/issues/236 + await trySettingPermission("prompt"); + await trySettingPermission("granted"); + + const pushSubscriptionChangeEvent = await promise; + + assert_equals(pushSubscriptionChangeEvent.data.type, "pushsubscriptionchange"); + assert_equals(pushSubscriptionChangeEvent.data.constructor, "PushSubscriptionChangeEvent"); + assert_object_equals(pushSubscriptionChangeEvent.data.oldSubscription, subscription.toJSON()); +}, "Fire pushsubscriptionchange event when permission is revoked");