Skip to content

Commit

Permalink
Declarative Web Push: Add "defaultAction" member to Notification and …
Browse files Browse the repository at this point in the history
…NotificationOptions

https://bugs.webkit.org/show_bug.cgi?id=262004
rdar://115938949

Reviewed by Dan Glastonbury.

When a declarative web push message comes in, it is required to have a default action URL.
This is so the user activating the notification can bypass service workers, as service workers are not required.

If the message is mutable, and a `pushnotification` event handler decides to show a different notification,
then that notification also needs a default action URL.

We put that as an option on NotificationOptions, add a Notification accessor for it, and enforce that
new notifications shown during `pushnotification` event handlers have one.

Then we test it.

* Source/WebCore/Modules/notifications/Notification.cpp:
(WebCore::Notification::create):
(WebCore::Notification::Notification):
(WebCore::Notification::data const):
* Source/WebCore/Modules/notifications/Notification.h:
* Source/WebCore/Modules/notifications/Notification.idl:
* Source/WebCore/Modules/notifications/NotificationOptions.idl:
* Source/WebCore/workers/service/ServiceWorkerRegistration.cpp:
(WebCore::ServiceWorkerRegistration::showNotification):
* Tools/TestWebKitAPI/Tests/WebKitCocoa/WebPushDaemon.mm:

Canonical link: https://commits.webkit.org/268402@main
  • Loading branch information
beidson committed Sep 25, 2023
1 parent e911779 commit 72258db
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 7 deletions.
29 changes: 26 additions & 3 deletions Source/WebCore/Modules/notifications/Notification.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,11 @@ ExceptionOr<Ref<Notification>> Notification::createForServiceWorker(ScriptExecut

Ref<Notification> Notification::create(ScriptExecutionContext& context, NotificationData&& data)
{
#if ENABLE(DECLARATIVE_WEB_PUSH)
Options options { data.direction, WTFMove(data.language), WTFMove(data.body), WTFMove(data.tag), WTFMove(data.iconURL), JSC::jsNull(), nullptr, nullptr, data.silent, { }, WTFMove(data.defaultActionURL) };
#else
Options options { data.direction, WTFMove(data.language), WTFMove(data.body), WTFMove(data.tag), WTFMove(data.iconURL), JSC::jsNull(), nullptr, nullptr, data.silent };

#endif
auto notification = adoptRef(*new Notification(context, data.notificationID, WTFMove(data.title), WTFMove(options), SerializedScriptValue::createFromWireBytes(WTFMove(data.data))));
notification->suspendIfNeeded();
notification->m_serviceWorkerRegistrationURL = WTFMove(data.serviceWorkerRegistrationURL);
Expand All @@ -129,8 +132,14 @@ Ref<Notification> Notification::create(ScriptExecutionContext& context, Notifica
Ref<Notification> Notification::create(ScriptExecutionContext& context, const URL& registrationURL, const NotificationPayload& payload)
{
Options options;
if (payload.options)
if (payload.options) {
#if ENABLE(DECLARATIVE_WEB_PUSH)
options = { payload.options->dir, payload.options->lang, payload.options->body, payload.options->tag, payload.options->icon, JSC::jsNull(), nullptr, nullptr, payload.options->silent, { }, { } };
options.defaultActionURL = payload.defaultActionURL;
#else
options = { payload.options->dir, payload.options->lang, payload.options->body, payload.options->tag, payload.options->icon, JSC::jsNull(), nullptr, nullptr, payload.options->silent };
#endif
}

RefPtr<SerializedScriptValue> dataScriptValue;
if (payload.options && !payload.options->dataJSONString.isEmpty() && context.globalObject()) {
Expand Down Expand Up @@ -170,8 +179,18 @@ Notification::Notification(ScriptExecutionContext& context, WTF::UUID identifier
else
RELEASE_ASSERT_NOT_REACHED();

#if ENABLE(DECLARATIVE_WEB_PUSH)
if (options.defaultActionURL.isValid())
m_defaultActionURL = WTFMove(options.defaultActionURL).isolatedCopy();
else if (!options.defaultAction.isEmpty()) {
auto defaultActionURL = context.completeURL(WTFMove(options.defaultAction).isolatedCopy());
if (defaultActionURL.isValid())
m_defaultActionURL = WTFMove(defaultActionURL);
}
#endif

if (!options.icon.isEmpty()) {
auto iconURL = context.completeURL(options.icon);
auto iconURL = context.completeURL(WTFMove(options.icon).isolatedCopy());
if (iconURL.isValid())
m_icon = iconURL;
}
Expand Down Expand Up @@ -432,7 +451,11 @@ NotificationData Notification::data() const
RELEASE_ASSERT(sessionID);

return {
#if ENABLE(DECLARATIVE_WEB_PUSH)
m_defaultActionURL,
#else
{ },
#endif
m_title,
m_body,
m_icon.string(),
Expand Down
10 changes: 10 additions & 0 deletions Source/WebCore/Modules/notifications/Notification.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ class Notification final : public RefCounted<Notification>, public ActiveDOMObje
RefPtr<SerializedScriptValue> serializedData;
RefPtr<JSON::Value> jsonData;
std::optional<bool> silent;
#if ENABLE(DECLARATIVE_WEB_PUSH)
String defaultAction;
URL defaultActionURL;
#endif
};
// For JS constructor only.
static ExceptionOr<Ref<Notification>> create(ScriptExecutionContext&, String&& title, Options&&);
Expand All @@ -85,6 +89,9 @@ class Notification final : public RefCounted<Notification>, public ActiveDOMObje
void show(CompletionHandler<void()>&& = [] { });
void close();

#if ENABLE(DECLARATIVE_WEB_PUSH)
const URL& defaultAction() const { return m_defaultActionURL; }
#endif
const String& title() const { return m_title; }
Direction dir() const { return m_direction; }
const String& body() const { return m_body; }
Expand Down Expand Up @@ -145,6 +152,9 @@ class Notification final : public RefCounted<Notification>, public ActiveDOMObje

WTF::UUID m_identifier;

#if ENABLE(DECLARATIVE_WEB_PUSH)
URL m_defaultActionURL;
#endif
String m_title;
Direction m_direction;
String m_lang;
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/Modules/notifications/Notification.idl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
attribute EventHandler onclick;
attribute EventHandler onerror;

[Conditional=DECLARATIVE_WEB_PUSH, EnabledBySetting=DeclarativeWebPush] readonly attribute USVString defaultAction;
readonly attribute DOMString title;
readonly attribute NotificationDirection dir;
readonly attribute DOMString lang;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
[
Conditional=NOTIFICATIONS
] dictionary NotificationOptions {
[Conditional=DECLARATIVE_WEB_PUSH, EnabledBySetting=DeclarativeWebPush] USVString defaultAction = "";
NotificationDirection dir = "auto";
DOMString lang = "";
DOMString body = "";
Expand Down
5 changes: 5 additions & 0 deletions Source/WebCore/workers/service/ServiceWorkerRegistration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ void ServiceWorkerRegistration::showNotification(ScriptExecutionContext& context
#if ENABLE(DECLARATIVE_WEB_PUSH)
if (RefPtr pushNotificationEvent = serviceWorkerGlobalScope->pushNotificationEvent()) {
auto notification = notificationResult.releaseReturnValue();
if (!notification->defaultAction().isValid()) {
promise->reject(Exception { TypeError, "Call to showNotification() while handling a `pushnotification` event did not include NotificationOptions that specify a valid defaultAction url"_s });
return;
}

pushNotificationEvent->setUpdatedNotificationData(notification->data());
return;
}
Expand Down
87 changes: 83 additions & 4 deletions Tools/TestWebKitAPI/Tests/WebKitCocoa/WebPushDaemon.mm
Original file line number Diff line number Diff line change
Expand Up @@ -522,18 +522,34 @@ async function disableShowNotifications()
var optionsFromTag = event.proposedNotification.tag.split(" ");
var newTitle;
var newBadge;
var newActionURL;
if (optionsFromTag[0] == "titleandbadge") {
newTitle = optionsFromTag[1];
newBadge = optionsFromTag[2];
} else if (optionsFromTag[0] == "title")
newTitle = optionsFromTag[1];
else if (optionsFromTag[0] == "badge")
newBadge = optionsFromTag[1]
newBadge = optionsFromTag[1];
else if (optionsFromTag[0] == "datatotitle")
newTitle = event.proposedNotification.data;
else if (optionsFromTag[0] == "defaultactionurl")
newActionURL = optionsFromTag[1];
else if (optionsFromTag[0] == "emptydefaultactionurl") {
self.registration.showNotification("Missing default action").then((value) => {
globalPort.postMessage("showNotification succeeded");
}, (exception) => {
globalPort.postMessage("showNotification failed: " + exception);
});
}
if (newTitle)
self.registration.showNotification(newTitle);
if (newTitle || newActionURL) {
if (!newTitle)
newTitle = event.proposedNotification.title;
if (!newActionURL)
newActionURL = event.proposedNotification.defaultAction;
self.registration.showNotification(newTitle, { "defaultAction": newActionURL });
}
if (newBadge)
navigator.setAppBadge(newBadge);
Expand Down Expand Up @@ -901,6 +917,18 @@ void processPushMessage(NSDictionary *pushMessage)
TestWebKitAPI::Util::run(&done);
}

void captureAllMessages()
{
[m_testMessageHandler setWildcardMessageHandler:^(NSString *message){
m_mostRecentMessage = message;
}];
}

const String& mostRecentMessage() const
{
return m_mostRecentMessage;
}

private:
String m_pushPartition;
Markable<WTF::UUID> m_dataStoreIdentifier;
Expand All @@ -912,6 +940,7 @@ void processPushMessage(NSDictionary *pushMessage)
std::unique_ptr<TestWebKitAPI::HTTPServer> m_server;
TestNotificationProvider& m_notificationProvider;
RetainPtr<WKWebView> m_webView;
String m_mostRecentMessage;
};

class WebPushDTest : public ::testing::Test {
Expand Down Expand Up @@ -1815,6 +1844,29 @@ void runTest(NSString *expectedMessage, NSDictionary *apsUserInfo)
"app_badge": "12"
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json45 = R"JSONRESOURCE(
{
"default_action_url": "https://example.com/",
"title": "Test a default action URL override",
"mutable": true,
"options": {
"tag": "defaultactionurl https://webkit.org/"
},
"app_badge": "12"
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json46 = R"JSONRESOURCE(
{
"default_action_url": "https://example.com/",
"title": "Test a missing default action URL override",
"mutable": true,
"options": {
"tag": "emptydefaultactionurl"
},
"app_badge": "12"
}
)JSONRESOURCE"_s;

static constexpr ASCIILiteral errors[] = {
"does not contain valid JSON"_s,
"top level JSON value is not an object"_s,
Expand Down Expand Up @@ -1883,7 +1935,8 @@ void runTest(NSString *expectedMessage, NSDictionary *apsUserInfo)
{ json42, { " "_s } },
{ json43, { " "_s } },
{ json44, { " "_s } },

{ json45, { " "_s } },
{ json46, { " "_s } },
{ { }, { } }
};

Expand Down Expand Up @@ -2028,15 +2081,31 @@ void runTest(ASCIILiteral jsonMessage)

auto messages = webViews().first()->fetchPushMessages();
ASSERT_EQ([messages count], 1u);

webViews().first()->captureAllMessages();
webViews().first()->processPushMessage([messages firstObject]);
}

void waitForMessageAndVerify(NSString *message)
{
while (webViews().first()->mostRecentMessage().isEmpty())
TestWebKitAPI::Util::runFor(0.05_s);

EXPECT_TRUE([(NSString *)webViews().first()->mostRecentMessage() isEqualToString:message]);
}

void checkLastNotificationTitle(NSString *title)
{
NSString *recentTitle = webViews().first()->mostRecentNotification().userInfo[@"WebNotificationTitleKey"];
EXPECT_TRUE([recentTitle isEqualToString:title]);
}

void checkLastNotificationDefaultActionURL(NSString *actionURL)
{
NSString *notificationActionURL = webViews().first()->mostRecentNotification().userInfo[@"WebNotificationDefaultActionURLKey"];
EXPECT_TRUE([notificationActionURL isEqualToString:actionURL]);
}

void checkLastActionURL(NSString *url)
{
NSURL *recentActionURL = webViews().first()->mostRecentActionURL();
Expand Down Expand Up @@ -2075,6 +2144,16 @@ void checkLastAppBadge(std::optional<uint64_t> badge)
runTest(json44);
checkLastNotificationTitle(@"[object Object]");
checkLastAppBadge(12);

runTest(json45);
checkLastNotificationTitle(@"Test a default action URL override");
checkLastNotificationDefaultActionURL(@"https://webkit.org/");
checkLastAppBadge(12);

runTest(json46);
checkLastNotificationTitle(@"Test a missing default action URL override");
checkLastNotificationDefaultActionURL(@"https://example.com/");
waitForMessageAndVerify(@"showNotification failed: TypeError: Call to showNotification() while handling a `pushnotification` event did not include NotificationOptions that specify a valid defaultAction url");
}

#endif // ENABLE(DECLARATIVE_WEB_PUSH)
Expand Down

0 comments on commit 72258db

Please sign in to comment.