diff --git a/migrations/add_webpush_token.php b/migrations/add_webpush_token.php new file mode 100644 index 0000000..82f8377 --- /dev/null +++ b/migrations/add_webpush_token.php @@ -0,0 +1,48 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + */ + +namespace phpbb\webpushnotifications\migrations; + +use phpbb\db\migration\migration; + +class add_webpush_token extends migration +{ + public static function depends_on() + { + return ['\phpbb\webpushnotifications\migrations\add_webpush']; + } + + public function effectively_installed(): bool + { + return $this->db_tools->sql_column_exists($this->table_prefix . 'wpn_notification_push', 'push_token'); + } + + public function update_schema(): array + { + return [ + 'add_columns' => [ + $this->table_prefix . 'wpn_notification_push' => [ + 'push_token' => ['VCHAR', ''], + ], + ], + ]; + } + + public function revert_schema(): array + { + return [ + 'drop_columns' => [ + $this->table_prefix . 'wpn_notification_push' => [ + 'push_token', + ], + ], + ]; + } +} diff --git a/notification/method/webpush.php b/notification/method/webpush.php index b3ec2e3..15335ba 100644 --- a/notification/method/webpush.php +++ b/notification/method/webpush.php @@ -54,6 +54,9 @@ class webpush extends messenger_base implements extended_method_interface /** @var int Fallback size for padding if endpoint is mozilla, see https://github.com/web-push-libs/web-push-php/issues/108#issuecomment-2133477054 */ public const MOZILLA_FALLBACK_PADDING = 2820; + /** @var array Map for storing push token between db insertion and sending of notifications */ + private $push_token_map = []; + /** * Notification Method Web Push constructor * @@ -150,9 +153,11 @@ public function notify() 'avatar' => $this->prepare_avatar($notification->get_avatar()), ]), 'notification_time' => time(), + 'push_token' => hash('sha256', random_bytes(32)) ]; $data = self::clean_data($data); $insert_buffer->insert($data); + $this->push_token_map[$notification->notification_type_id][$notification->item_id] = $data['push_token']; } $insert_buffer->flush(); @@ -226,7 +231,9 @@ protected function notify_using_webpush(): void $data = [ 'item_id' => $notification->item_id, 'type_id' => $notification->notification_type_id, + 'user_id' => $notification->user_id, 'version' => $this->config['assets_version'], + 'token' => hash('sha256', $user['user_form_salt'] . $this->push_token_map[$notification->notification_type_id][$notification->item_id]), ]; $json_data = json_encode($data); @@ -342,6 +349,7 @@ public static function clean_data(array $data): array 'item_parent_id' => null, 'user_id' => null, 'push_data' => null, + 'push_token' => null, 'notification_time' => null, ]; diff --git a/styles/all/template/push_worker.js.twig b/styles/all/template/push_worker.js.twig index f2807cb..cf0ca56 100644 --- a/styles/all/template/push_worker.js.twig +++ b/styles/all/template/push_worker.js.twig @@ -23,12 +23,16 @@ self.addEventListener('push', event => { let itemId = 0; let typeId = 0; - let notificationVersion = 5; + let userId = 0; + let notificationVersion = 0; + let pushToken = ''; try { const notificationData = event.data.json(); itemId = notificationData.item_id; typeId = notificationData.type_id; + userId = notificationData.user_id; notificationVersion = parseInt(notificationData.version, 10); + pushToken = notificationData.token; } catch { self.registration.showNotification(event.data.text()); return; @@ -45,6 +49,8 @@ self.addEventListener('push', event => { const formData = new FormData(); formData.append('item_id', itemId.toString(10)); formData.append('type_id', typeId.toString(10)); + formData.append('user_id', userId.toString(10)); + formData.append('token', pushToken); fetch(getNotificationUrl, { method: 'POST', diff --git a/ucp/controller/webpush.php b/ucp/controller/webpush.php index 9d8eb63..c1cdeeb 100644 --- a/ucp/controller/webpush.php +++ b/ucp/controller/webpush.php @@ -97,10 +97,37 @@ public function __construct(config $config, controller_helper $controller_helper * @return JsonResponse */ public function notification(): JsonResponse + { + if (!$this->request->is_ajax() || $this->user->data['is_bot'] || $this->user->data['user_type'] == USER_INACTIVE) + { + throw new http_exception(Response::HTTP_FORBIDDEN, 'Forbidden'); + } + + if ($this->user->id() !== ANONYMOUS) + { + $notification_data = $this->get_user_notifications(); + } + else + { + $notification_data = $this->get_anonymous_notifications(); + } + + // Decode and return data if everything is fine + $data = json_decode($notification_data, true); + $data['url'] = isset($data['url']) ? $this->path_helper->update_web_root_path($data['url']) : ''; + + return new JsonResponse($data); + } + + /** + * Get notification data for logged in user + * + * @return string Notification data + */ + private function get_user_notifications(): string { // Subscribe should only be available for logged-in "normal" users - if (!$this->request->is_ajax() || $this->user->id() == ANONYMOUS || $this->user->data['is_bot'] - || $this->user->data['user_type'] == USER_IGNORE || $this->user->data['user_type'] == USER_INACTIVE) + if ($this->user->data['user_type'] == USER_IGNORE) { throw new http_exception(Response::HTTP_FORBIDDEN, 'Forbidden'); } @@ -116,10 +143,53 @@ public function notification(): JsonResponse $result = $this->db->sql_query($sql); $notification_data = $this->db->sql_fetchfield('push_data'); $this->db->sql_freeresult($result); - $data = json_decode($notification_data, true); - $data['url'] = isset($data['url']) ? $this->path_helper->update_web_root_path($data['url']) : ''; - return new JsonResponse($data); + return $notification_data; + } + + /** + * Get notification data for not logged in user via token + * + * @return string + */ + private function get_anonymous_notifications(): string + { + $token = $this->request->variable('token', ''); + + if ($token) + { + $item_id = $this->request->variable('item_id', 0); + $type_id = $this->request->variable('type_id', 0); + $user_id = $this->request->variable('user_id', 0); + + $sql = 'SELECT push_data, push_token + FROM ' . $this->notification_webpush_table . ' + WHERE user_id = ' . (int) $user_id . ' + AND notification_type_id = ' . (int) $type_id . ' + AND item_id = ' . (int) $item_id; + $result = $this->db->sql_query($sql); + $notification_row = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + $notification_data = $notification_row['push_data']; + $push_token = $notification_row['push_token']; + + // Check if passed push token is valid + $sql = 'SELECT user_form_salt + FROM ' . USERS_TABLE . ' + WHERE user_id = ' . (int) $user_id; + $result = $this->db->sql_query($sql); + $user_form_token = $this->db->sql_fetchfield('user_form_salt'); + $this->db->sql_freeresult($result); + + $expected_push_token = hash('sha256', $user_form_token . $push_token); + if ($expected_push_token === $token) + { + return $notification_data; + } + } + + throw new http_exception(Response::HTTP_FORBIDDEN, 'Forbidden'); } /**