Source/WebCore/ChangeLog

 12022-03-21 Ben Nham <nham@apple.com>
 2
 3 Only show notification permission prompt on transient activation
 4 https://bugs.webkit.org/show_bug.cgi?id=238188
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 In r291427, we changed Notification.requestPermission and PushManager.subscribe to only show
 9 a permission prompt when processing a user gesture. This ended up being too restrictive and
 10 causes compatibility problems with some large sites.
 11
 12 Instead, match Chrome and Firefox by allowing these prompts after a transient activiation,
 13 i.e. a user gesture within the past second.
 14
 15 Per Maciej's suggestion, we also consume the activation to help combat prompt spam.
 16
 17 Covered by new and existing layout tests.
 18
 19 * Modules/notifications/Notification.cpp:
 20 (WebCore::Notification::requestPermission):
 21 * Modules/push-api/PushManager.cpp:
 22 (WebCore::PushManager::subscribe):
 23
1242022-03-18 Jonathan Bedard <jbedard@apple.com>
225
326 [iOS 15.4] Fix unused variables

Source/WebCore/Modules/notifications/Notification.cpp

3535
3636#include "Notification.h"
3737
 38#include "DOMWindow.h"
3839#include "Event.h"
3940#include "EventNames.h"
4041#include "JSDOMPromiseDeferred.h"

4344#include "NotificationEvent.h"
4445#include "NotificationPermissionCallback.h"
4546#include "ServiceWorkerGlobalScope.h"
46 #include "UserGestureIndicator.h"
4747#include "WindowEventLoop.h"
4848#include "WindowFocusAllowedIndicator.h"
4949#include <wtf/CompletionHandler.h>

@@void Notification::requestPermission(Document& document, RefPtr<NotificationPerm
297297 return resolvePromiseAndCallback(Permission::Denied);
298298 }
299299
300  if (!UserGestureIndicator::processingUserGesture()) {
 300 auto* window = document.frame()->window();
 301 if (!window || !window->consumeTransientActivation()) {
301302 document.addConsoleMessage(MessageSource::Security, MessageLevel::Error, "Notification prompting can only be done from a user gesture."_s);
302303 return resolvePromiseAndCallback(Permission::Denied);
303304 }

Source/WebCore/Modules/push-api/PushManager.cpp

2828
2929#if ENABLE(SERVICE_WORKER)
3030
 31#include "DOMWindow.h"
3132#include "DocumentInlines.h"
3233#include "EventLoop.h"
3334#include "Exception.h"

3738#include "PushCrypto.h"
3839#include "ScriptExecutionContext.h"
3940#include "ServiceWorkerRegistration.h"
40 #include "UserGestureIndicator.h"
4141#include <wtf/IsoMallocInlines.h>
4242#include <wtf/Vector.h>
4343#include <wtf/text/Base64.h>

@@void PushManager::subscribe(ScriptExecutionContext& context, std::optional<PushS
7272{
7373 RELEASE_ASSERT(context.isSecureContext());
7474
75  context.eventLoop().queueTask(TaskSource::Networking, [this, protectedThis = Ref { *this }, context = Ref { context }, options = WTFMove(options), promise = WTFMove(promise), processingUserGesture = UserGestureIndicator::processingUserGesture()]() mutable {
 75 context.eventLoop().queueTask(TaskSource::Networking, [this, protectedThis = Ref { *this }, context = Ref { context }, options = WTFMove(options), promise = WTFMove(promise)]() mutable {
7676 if (!options || !options->userVisibleOnly) {
7777 promise.reject(Exception { NotAllowedError, "Subscribing for push requires userVisibleOnly to be true"_s });
7878 return;

@@void PushManager::subscribe(ScriptExecutionContext& context, std::optional<PushS
131131 RELEASE_ASSERT(client);
132132 RELEASE_ASSERT(context->isDocument());
133133
134  if (!downcast<Document>(context.get()).isSameOriginAsTopDocument()) {
 134 auto& document = downcast<Document>(context.get());
 135 if (!document.isSameOriginAsTopDocument()) {
135136 promise.reject(Exception { NotAllowedError, "Cannot request permission from cross-origin iframe"_s });
136137 return;
137138 }
138139
139  if (!processingUserGesture) {
 140 auto* window = document.frame()->window();
 141 if (!window || !window->consumeTransientActivation()) {
140142 promise.reject(Exception { NotAllowedError, "Push notification prompting can only be done from a user gesture"_s });
141143 return;
142144 }

Tools/ChangeLog

 12022-03-21 Ben Nham <nham@apple.com>
 2
 3 Only show notification permission prompt on transient activation
 4 https://bugs.webkit.org/show_bug.cgi?id=238188
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 Add an internal API denyWebNotificationPermissionOnPrompt to WebKitTestRunner that allows a
 9 notification permission prompt to first be displayed and then rejected. This differs from
 10 the existing denyNotificationPermission call, which denied notification permissions before
 11 prompting.
 12
 13 * WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
 14 * WebKitTestRunner/InjectedBundle/TestRunner.cpp:
 15 (WTR::TestRunner::denyWebNotificationPermission):
 16 (WTR::TestRunner::denyWebNotificationPermissionOnPrompt):
 17 * WebKitTestRunner/InjectedBundle/TestRunner.h:
 18 * WebKitTestRunner/TestController.cpp:
 19 (WTR::originUserVisibleName):
 20 (WTR::TestController::denyNotificationPermissionOnPrompt):
 21 (WTR::TestController::resetStateToConsistentValues):
 22 (WTR::TestController::decidePolicyForNotificationPermissionRequest):
 23 * WebKitTestRunner/TestController.h:
 24 * WebKitTestRunner/TestInvocation.cpp:
 25 (WTR::TestInvocation::didReceiveSynchronousMessageFromInjectedBundle):
 26
1272022-03-18 Per Arne Vollan <pvollan@apple.com>
228
329 Fix test failures when enabling content filtering in the Network process

Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl

@@interface TestRunner {
206206 // Web notifications support
207207 undefined grantWebNotificationPermission(DOMString origin);
208208 undefined denyWebNotificationPermission(DOMString origin);
 209 undefined denyWebNotificationPermissionOnPrompt(DOMString origin);
209210 undefined removeAllWebNotificationPermissions();
210211 undefined simulateWebNotificationClick(object notification);
211212 undefined simulateWebNotificationClickForServiceWorkerNotifications();

Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp

@@void TestRunner::denyWebNotificationPermission(JSStringRef origin)
876876 postSynchronousPageMessageWithReturnValue("DenyNotificationPermission", toWK(origin));
877877}
878878
 879void TestRunner::denyWebNotificationPermissionOnPrompt(JSStringRef origin)
 880{
 881 postSynchronousPageMessageWithReturnValue("DenyNotificationPermissionOnPrompt", toWK(origin));
 882}
 883
879884void TestRunner::removeAllWebNotificationPermissions()
880885{
881886 WKBundleRemoveAllWebNotificationPermissions(InjectedBundle::singleton().bundle(), page());

Tools/WebKitTestRunner/InjectedBundle/TestRunner.h

@@public:
292292 // Web notifications.
293293 static void grantWebNotificationPermission(JSStringRef origin);
294294 static void denyWebNotificationPermission(JSStringRef origin);
 295 static void denyWebNotificationPermissionOnPrompt(JSStringRef origin);
295296 static void removeAllWebNotificationPermissions();
296297 static void simulateWebNotificationClick(JSValueRef notification);
297298 static void simulateWebNotificationClickForServiceWorkerNotifications();

Tools/WebKitTestRunner/TestController.cpp

@@WKRetainPtr<WKPageConfigurationRef> TestController::generatePageConfiguration(co
719719 return pageConfiguration;
720720}
721721
 722static String originUserVisibleName(WKSecurityOriginRef origin)
 723{
 724 if (!origin)
 725 return emptyString();
 726
 727 auto host = toWTFString(adoptWK(WKSecurityOriginCopyHost(origin)));
 728 auto protocol = toWTFString(adoptWK(WKSecurityOriginCopyProtocol(origin)));
 729
 730 if (host.isEmpty() || protocol.isEmpty())
 731 return emptyString();
 732
 733 if (int port = WKSecurityOriginGetPort(origin))
 734 return makeString(protocol, "://", host, ':', port);
 735
 736 return makeString(protocol, "://", host);
 737}
 738
722739bool TestController::grantNotificationPermission(WKStringRef originString)
723740{
724741 m_webNotificationProvider.setPermission(toWTFString(originString), true);

@@bool TestController::denyNotificationPermission(WKStringRef originString)
737754 return true;
738755}
739756
 757bool TestController::denyNotificationPermissionOnPrompt(WKStringRef originString)
 758{
 759 auto origin = adoptWK(WKSecurityOriginCreateFromString(originString));
 760 auto originName = originUserVisibleName(origin.get());
 761 m_notificationOriginsToDenyOnPrompt.add(originName);
 762 return true;
 763}
 764
740765void TestController::createWebViewWithOptions(const TestOptions& options)
741766{
742767 auto applicationBundleIdentifier = options.applicationBundleIdentifier();

@@bool TestController::resetStateToConsistentValues(const TestOptions& options, Re
10461071
10471072 // Reset notification permissions
10481073 m_webNotificationProvider.reset();
 1074 m_notificationOriginsToDenyOnPrompt.clear();
10491075
10501076 // Reset Geolocation permissions.
10511077 m_geolocationPermissionRequests.clear();

@@bool TestController::isGeolocationProviderActive() const
24122438 return m_geolocationProvider->isActive();
24132439}
24142440
2415 static String originUserVisibleName(WKSecurityOriginRef origin)
2416 {
2417  if (!origin)
2418  return emptyString();
2419 
2420  auto host = toWTFString(adoptWK(WKSecurityOriginCopyHost(origin)));
2421  auto protocol = toWTFString(adoptWK(WKSecurityOriginCopyProtocol(origin)));
2422 
2423  if (host.isEmpty() || protocol.isEmpty())
2424  return emptyString();
2425 
2426  if (int port = WKSecurityOriginGetPort(origin))
2427  return makeString(protocol, "://", host, ':', port);
2428 
2429  return makeString(protocol, "://", host);
2430 }
2431 
24322441static String userMediaOriginHash(WKSecurityOriginRef userMediaDocumentOrigin, WKSecurityOriginRef topLevelDocumentOrigin)
24332442{
24342443 String userMediaDocumentOriginString = originUserVisibleName(userMediaDocumentOrigin);

@@void TestController::decidePolicyForNotificationPermissionRequest(WKPageRef page
26512660 TestController::singleton().decidePolicyForNotificationPermissionRequest(page, origin, request);
26522661}
26532662
2654 void TestController::decidePolicyForNotificationPermissionRequest(WKPageRef, WKSecurityOriginRef, WKNotificationPermissionRequestRef request)
 2663void TestController::decidePolicyForNotificationPermissionRequest(WKPageRef, WKSecurityOriginRef origin, WKNotificationPermissionRequestRef request)
26552664{
 2665 auto originName = originUserVisibleName(origin);
 2666 if (m_notificationOriginsToDenyOnPrompt.contains(originName)) {
 2667 WKNotificationPermissionRequestDeny(request);
 2668 return;
 2669 }
 2670
26562671 WKNotificationPermissionRequestAllow(request);
26572672}
26582673

Tools/WebKitTestRunner/TestController.h

@@public:
376376
377377 bool grantNotificationPermission(WKStringRef origin);
378378 bool denyNotificationPermission(WKStringRef origin);
 379 bool denyNotificationPermissionOnPrompt(WKStringRef origin);
379380
380381private:
381382 WKRetainPtr<WKPageConfigurationRef> generatePageConfiguration(const TestOptions&);

@@private:
575576 WKRetainPtr<WKStringRef> m_testPluginDirectory;
576577
577578 WebNotificationProvider m_webNotificationProvider;
 579 HashSet<String> m_notificationOriginsToDenyOnPrompt;
578580
579581 std::unique_ptr<PlatformWebView> m_mainWebView;
580582 Vector<UniqueRef<PlatformWebView>> m_auxiliaryWebViews;

Tools/WebKitTestRunner/TestInvocation.cpp

@@WKRetainPtr<WKTypeRef> TestInvocation::didReceiveSynchronousMessageFromInjectedB
10391039 if (WKStringIsEqualToUTF8CString(messageName, "DenyNotificationPermission"))
10401040 return adoptWK(WKBooleanCreate(TestController::singleton().denyNotificationPermission(stringValue(messageBody))));
10411041
 1042 if (WKStringIsEqualToUTF8CString(messageName, "DenyNotificationPermissionOnPrompt"))
 1043 return adoptWK(WKBooleanCreate(TestController::singleton().denyNotificationPermissionOnPrompt(stringValue(messageBody))));
 1044
10421045 if (WKStringIsEqualToUTF8CString(messageName, "IsDoingMediaCapture"))
10431046 return adoptWK(WKBooleanCreate(TestController::singleton().isDoingMediaCapture()));
10441047

LayoutTests/ChangeLog

 12022-03-21 Ben Nham <nham@apple.com>
 2
 3 Only show notification permission prompt on transient activation
 4 https://bugs.webkit.org/show_bug.cgi?id=238188
 5
 6 Reviewed by NOBODY (OOPS!).
 7
 8 Add test cases to make sure that showing a permission prompt consumes a user gesture.
 9
 10 * http/tests/notifications/notification-request-permission-no-callback.html:
 11 * http/tests/notifications/notification-request-permission.html:
 12 * http/tests/notifications/request-consumes-activation-expected.txt: Added.
 13 * http/tests/notifications/request-consumes-activation.html: Added.
 14 * http/tests/push-api/subscribe-deny-permissions-on-prompt-expected.txt: Added.
 15 * http/tests/push-api/subscribe-deny-permissions-on-prompt.html: Added.
 16
1172022-03-18 Ryan Haddad <ryanhaddad@apple.com>
218
319 [macOS arm64] webrtc/vp9-profile2.html is consistently timing out

LayoutTests/http/tests/notifications/notification-request-permission-no-callback.html

@@if (!window.internals)
1515internals.withUserGesture(() => {
1616 window.Notification.requestPermission();
1717 testPassed("Notification.requestPermission does not crash.");
 18});
1819
 20internals.withUserGesture(() => {
1921 testRunner.grantWebNotificationPermission(testURL);
2022 window.Notification.requestPermission();
2123 testPassed("Notification.requestPermission does not crash.");
 24});
2225
 26internals.withUserGesture(() => {
2327 testRunner.denyWebNotificationPermission(testURL);
2428 window.Notification.requestPermission();
2529 testPassed("Notification.requestPermission does not crash.");

LayoutTests/http/tests/notifications/notification-request-permission.html

@@if (!window.internals)
1515internals.withUserGesture(() => {
1616 window.Notification.requestPermission(function() { });
1717 testPassed("Notification.requestPermission does not crash.");
 18});
1819
 20internals.withUserGesture(() => {
1921 testRunner.grantWebNotificationPermission(testURL);
2022 window.Notification.requestPermission(function() { });
2123 testPassed("Notification.requestPermission does not crash.");
 24});
2225
 26internals.withUserGesture(() => {
2327 testRunner.denyWebNotificationPermission(testURL);
2428 window.Notification.requestPermission(function() { });
2529 testPassed("Notification.requestPermission does not crash.");

LayoutTests/http/tests/notifications/request-consumes-activation-expected.txt

 1CONSOLE MESSAGE: Notification prompting can only be done from a user gesture.
 2This tests that Notification.requestPermission can only be called once per user gesture.
 3
 4On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
 5
 6
 7PASS both permission requests denied
 8PASS Notification.permission is "denied"
 9PASS successfullyParsed is true
 10
 11TEST COMPLETE
 12

LayoutTests/http/tests/notifications/request-consumes-activation.html

 1<!DOCTYPE html>
 2<script src="/js-test-resources/js-test.js"></script>
 3<script>
 4description("This tests that Notification.requestPermission can only be called once per user gesture.");
 5jsTestIsAsync = true;
 6
 7if (!window.internals)
 8 finishJSTest();
 9
 10function run() {
 11 testRunner.denyWebNotificationPermission(self.origin);
 12 internals.withUserGesture(() => {
 13 let prompt1 = Notification.requestPermission();
 14 let prompt2 = Notification.requestPermission();
 15 Promise.all([prompt1, prompt2]).then(([permission1, permission2]) => {
 16 if (permission1 == 'denied' && permission2 == 'denied')
 17 testPassed("both permission requests denied");
 18 else
 19 testFailed("both permission requests should be denied, but was " + permission1 + " and then " + permission2);
 20 shouldBeEqualToString("Notification.permission", "denied");
 21 finishJSTest();
 22 });
 23 });
 24}
 25
 26run();
 27</script>

LayoutTests/http/tests/push-api/subscribe-deny-permissions-on-prompt-expected.txt

 1PASS: service worker permissionState was prompt
 2PASS: document permissionState was prompt
 3PASS: service worker subscribe was error: NotAllowedError
 4PASS: document subscribe without user gesture was error: NotAllowedError
 5PASS: document subscribe with user gesture was error: NotAllowedError
 6PASS: document subscribe with consumed user gesture was failed with user gesture error
 7

LayoutTests/http/tests/push-api/subscribe-deny-permissions-on-prompt.html

 1<html>
 2<head>
 3<script src="resources/push-api-test-pre.js"></script>
 4<script src="resources/subscribe-tests.js"></script>
 5</head>
 6<body>
 7<script>
 8if (window.testRunner)
 9 testRunner.denyWebNotificationPermissionOnPrompt(window.origin);
 10
 11navigator.serviceWorker.register("resources/subscribe-worker.js", { }).then(async (registration) => {
 12 try {
 13 await waitForState(registration.installing, "activated");
 14 await testServiceWorkerPermissionState(registration, 'prompt');
 15 await testDocumentPermissionState(registration, 'prompt');
 16 await testServiceWorkerSubscribe(registration, 'NotAllowedError');
 17 await testDocumentSubscribeWithoutUserGesture(registration, 'NotAllowedError');
 18
 19 let promise = new Promise((resolve, reject) => {
 20 if (!window.internals) {
 21 reject('Test requires internals.');
 22 return;
 23 }
 24
 25 internals.withUserGesture(() => {
 26 let request1 = registration.pushManager.subscribe({
 27 userVisibleOnly: true,
 28 applicationServerKey: VALID_SERVER_KEY
 29 });
 30 let request2 = registration.pushManager.subscribe({
 31 userVisibleOnly: true,
 32 applicationServerKey: VALID_SERVER_KEY
 33 });
 34 Promise.allSettled([request1, request2]).then(resolve);
 35 });
 36 });
 37 let [result1, result2] = await(promise);
 38
 39 if (result1.reason && result1.reason.name == 'NotAllowedError')
 40 log('PASS: document subscribe with user gesture was error: NotAllowedError')
 41 else
 42 log(`FAIL: first subscribe failed with unexpected result ${result1.value || result1.reason}`);
 43
 44 if (result2.reason && result2.reason.name == 'NotAllowedError' && result2.reason.message.includes('user gesture'))
 45 log('PASS: document subscribe with consumed user gesture was failed with user gesture error')
 46 else
 47 log(`FAIL: second subscribe failed with unexpected result ${result2.value || result2.reason}`)
 48 } catch (e) {
 49 log(`FAIL: unexpected exception ${e}`);
 50 } finally {
 51 await registration.unregister();
 52 finishPushAPITest();
 53 }
 54});
 55</script>
 56</body>
 57</html>