Skip to content

Commit 388da99

Browse files
authored
Merge pull request #129 from iMattPro/updates
Preserve each user's push token + fixes
2 parents a366683 + b734245 commit 388da99

5 files changed

Lines changed: 108 additions & 26 deletions

File tree

migrations/handle_subscriptions.php

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,32 @@ public function copy_subscription_tables()
8989
$wpn_notification_push_table = $this->table_prefix . 'wpn_notification_push';
9090
$wpn_push_subscriptions_table = $this->table_prefix . 'wpn_push_subscriptions';
9191

92-
/*
93-
* If webpush notification method exists in phpBB core,
94-
* copy all subscriptions data over the corresponding core tables.
95-
*/
96-
foreach ([
97-
$core_notification_push_table => $wpn_notification_push_table,
98-
$core_push_subscriptions_table => $wpn_push_subscriptions_table
99-
] as $core_table => $ext_table)
92+
// Copy push table data
93+
$sql = 'INSERT INTO ' . $core_notification_push_table . '
94+
(notification_type_id, item_id, item_parent_id, user_id, push_data, notification_time, push_token)
95+
SELECT notification_type_id, item_id, item_parent_id, user_id, push_data, notification_time, push_token
96+
FROM ' . $wpn_notification_push_table;
97+
$this->db->sql_query($sql);
98+
99+
// Turn on identity insert on mssql to be able to insert into
100+
// identity columns (e.g. id)
101+
if (strpos($this->db->get_sql_layer(), 'mssql') !== false)
102+
{
103+
$sql = 'SET IDENTITY_INSERT ' . $core_push_subscriptions_table . ' ON';
104+
$this->db->sql_query($sql);
105+
}
106+
107+
// Copy subscription table data
108+
$sql = 'INSERT INTO ' . $core_push_subscriptions_table . '
109+
(subscription_id, user_id, endpoint, expiration_time, p256dh, auth)
110+
SELECT subscription_id, user_id, endpoint, expiration_time, p256dh, auth
111+
FROM ' . $wpn_push_subscriptions_table;
112+
$this->db->sql_query($sql);
113+
114+
// Disable identity insert on mssql again
115+
if (strpos($this->db->get_sql_layer(), 'mssql') !== false)
100116
{
101-
$sql = 'INSERT INTO ' . $core_table . '
102-
SELECT * FROM ' . $ext_table;
117+
$sql = 'SET IDENTITY_INSERT ' . $core_push_subscriptions_table . ' OFF';
103118
$this->db->sql_query($sql);
104119
}
105120
}

notification/method/webpush.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public function notify()
158158
];
159159
$data = self::clean_data($data);
160160
$insert_buffer->insert($data);
161-
$this->push_token_map[$notification->notification_type_id][$notification->item_id] = $data['push_token'];
161+
$this->push_token_map[$notification->notification_type_id][$notification->item_id][$notification->user_id] = $data['push_token'];
162162
}
163163

164164
$insert_buffer->flush();
@@ -196,8 +196,11 @@ protected function notify_using_webpush(): void
196196

197197
// Load all the users we need
198198
$notify_users = array_diff($user_ids, $banned_users);
199+
200+
// If we have no users (e.g. all recipients are banned) empty queue and exit
199201
if (empty($notify_users))
200202
{
203+
$this->empty_queue();
201204
return;
202205
}
203206

@@ -239,7 +242,7 @@ protected function notify_using_webpush(): void
239242
'type_id' => $notification->notification_type_id,
240243
'user_id' => $notification->user_id,
241244
'version' => $this->config['assets_version'],
242-
'token' => hash('sha256', $user['user_form_salt'] . $this->push_token_map[$notification->notification_type_id][$notification->item_id]),
245+
'token' => hash('sha256', $user['user_form_salt'] . $this->push_token_map[$notification->notification_type_id][$notification->item_id][$notification->user_id]),
243246
];
244247
$json_data = json_encode($data);
245248

@@ -531,6 +534,6 @@ protected function is_subscription_unauthorized(\Minishlink\WebPush\MessageSentR
531534
protected function is_endpoint_permanently_removed(string $endpoint): bool
532535
{
533536
$host = parse_url($endpoint, PHP_URL_HOST);
534-
return $host !== null && substr($host, -strlen('.invalid')) === '.invalid';
537+
return is_string($host) && substr($host, -strlen('.invalid')) === '.invalid';
535538
}
536539
}

styles/all/template/webpush.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -479,19 +479,29 @@ function PhpbbWebpush() {
479479
},
480480
body: getFormData({ endpoint: subscription.endpoint }),
481481
})
482-
.then(() => {
483-
loadingIndicator.fadeOut(phpbb.alertTime);
484-
return subscription.unsubscribe();
485-
})
486-
.then(unsubscribed => {
482+
.then(async(response) => {
483+
let data = null;
484+
try {
485+
data = await response.json();
486+
} catch (e) {
487+
// Ignore JSON parsing failures and fall back below.
488+
}
489+
if (!response.ok || !data || !data.success) {
490+
throw new Error(data && data.message ? data.message : 'Unsubscribe failed.');
491+
}
492+
493+
const unsubscribed = await subscription.unsubscribe();
494+
487495
if (unsubscribed) {
488496
removeStoredSubscription(subscription.endpoint);
489497
setSubscriptionState(false);
490498
}
491499
})
492500
.catch(error => {
501+
phpbb.alert(ajaxErrorTitle, error.message || error);
502+
})
503+
.finally(() => {
493504
loadingIndicator.fadeOut(phpbb.alertTime);
494-
phpbb.alert(ajaxErrorTitle, error);
495505
});
496506
}
497507

tests/notification/notification_method_webpush_test.php

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@
1919
require_once __DIR__ . '/../../../../../../tests/notification/base.php';
2020
require_once __DIR__ . '/../../vendor/autoload.php'; // load the composer dependencies for this extension
2121

22-
/**
23-
* @group slow
24-
*/
2522
class notification_method_webpush_test extends \phpbb_tests_notification_base
2623
{
2724
/** @var string[] VAPID keys for testing purposes */
@@ -648,6 +645,58 @@ public function test_get_type(): void
648645
$this->assertEquals('notification.method.phpbb.wpn.webpush', $this->notification_method_webpush->get_type());
649646
}
650647

648+
public function test_push_token_map_is_per_user(): void
649+
{
650+
// Verifies that when multiple users are notified about the same item,
651+
// each user's push token is stored and used independently.
652+
// Previously the map was keyed [type_id][item_id], so the last user's
653+
// token overwrote all others, making every other user's token invalid.
654+
$subscription_info = [];
655+
$expected_users = [2 => ['user_id' => '2'], 3 => ['user_id' => '3'], 4 => ['user_id' => '4']];
656+
foreach ($expected_users as $user_id => $user_data)
657+
{
658+
$subscription_info[$user_id] = $this->create_subscription_for_user($user_id);
659+
}
660+
661+
$post_data = [
662+
'post_time' => 1349413322,
663+
'poster_id' => 1,
664+
'topic_title' => '',
665+
'post_subject' => '',
666+
'post_username' => '',
667+
'forum_name' => '',
668+
'forum_id' => '1',
669+
'post_id' => '2',
670+
'topic_id' => '1',
671+
];
672+
673+
$this->notifications->add_notifications('notification.type.post', $post_data);
674+
675+
// Fetch the stored rows only for users we created subscriptions for
676+
$webpush_table = $this->container->getParameter('tables.phpbb.wpn.notification_push');
677+
$sql = 'SELECT user_id, push_token FROM ' . $webpush_table . ' WHERE ' . $this->db->sql_in_set('user_id', array_keys($expected_users)) . ' ORDER BY user_id ASC';
678+
$result = $this->db->sql_query($sql);
679+
$rows = $this->db->sql_fetchrowset($result);
680+
$this->db->sql_freeresult($result);
681+
682+
$this->assertCount(count($expected_users), $rows, 'Each expected user must have a notification row');
683+
$tokens = array_column($rows, 'push_token');
684+
$this->assertEquals(count($tokens), count(array_unique($tokens)), 'Each user must have a unique push_token');
685+
686+
// Verify each message payload contains the token hashed with that specific user's salt
687+
foreach ($rows as $row)
688+
{
689+
$user_id = (int) $row['user_id'];
690+
$client_hash = basename($subscription_info[$user_id]['endpoint']);
691+
$messages = $this->get_messages_for_subscription($client_hash);
692+
$this->assertNotEmpty($messages);
693+
$payload = json_decode($messages[0], true);
694+
$user = $this->user_loader->get_user($user_id);
695+
$expected_token = hash('sha256', $user['user_form_salt'] . $row['push_token']);
696+
$this->assertEquals($expected_token, $payload['token'], 'Token in push payload must match hash of that user\'s salt + their own push_token');
697+
}
698+
}
699+
651700
public function test_get_ucp_template_data_uses_millisecond_expiration_time(): void
652701
{
653702
$this->user->data['user_id'] = 2;

ucp/controller/webpush.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ public function is_valid_endpoint(string $endpoint): bool
304304
{
305305
$host = parse_url($endpoint, PHP_URL_HOST);
306306

307-
if (empty($host))
307+
if (!is_string($host) || empty($host))
308308
{
309309
return false;
310310
}
@@ -409,8 +409,13 @@ public function subscribe(symfony_request $symfony_request): JsonResponse
409409
*/
410410
protected function get_subscription_write_data(array $data): array
411411
{
412-
$p256dh = is_string($data['keys']['p256dh'] ?? null) ? $data['keys']['p256dh'] : '';
413-
$auth = is_string($data['keys']['auth'] ?? null) ? $data['keys']['auth'] : '';
412+
$keys = $data['keys'] ?? [];
413+
if (!is_array($keys))
414+
{
415+
$keys = [];
416+
}
417+
$p256dh = is_string($keys['p256dh'] ?? null) ? $keys['p256dh'] : '';
418+
$auth = is_string($keys['auth'] ?? null) ? $keys['auth'] : '';
414419

415420
if ($p256dh === '' || $auth === '')
416421
{
@@ -458,7 +463,7 @@ public function unsubscribe(symfony_request $symfony_request): JsonResponse
458463

459464
$data = json_sanitizer::decode($symfony_request->get('data', ''));
460465

461-
$endpoint = $data['endpoint'];
466+
$endpoint = is_string($data['endpoint'] ?? null) ? $data['endpoint'] : '';
462467

463468
$sql = 'DELETE FROM ' . $this->push_subscriptions_table . '
464469
WHERE user_id = ' . (int) $this->user->id() . "

0 commit comments

Comments
 (0)