Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 114
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
webpush
0.00% covered (danger)
0.00%
0 / 114
0.00% covered (danger)
0.00%
0 / 9
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 notification
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 get_user_notifications
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 get_anonymous_notifications
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 get_notification_data
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 worker
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 check_subscribe_requests
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
56
 subscribe
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 unsubscribe
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\ucp\controller;
15
16use phpbb\config\config;
17use phpbb\controller\helper as controller_helper;
18use phpbb\db\driver\driver_interface;
19use phpbb\exception\http_exception;
20use phpbb\form\form_helper;
21use phpbb\json\sanitizer as json_sanitizer;
22use phpbb\language\language;
23use phpbb\notification\manager;
24use phpbb\path_helper;
25use phpbb\request\request_interface;
26use phpbb\symfony_request;
27use phpbb\user;
28use phpbb\user_loader;
29use Symfony\Component\HttpFoundation\JsonResponse;
30use Symfony\Component\HttpFoundation\Response;
31use Twig\Environment;
32use Twig\Error\LoaderError;
33use Twig\Error\RuntimeError;
34use Twig\Error\SyntaxError;
35
36class webpush
37{
38    /** @var string UCP form token name */
39    public const FORM_TOKEN_UCP = 'ucp_webpush';
40
41    /** @var config */
42    protected $config;
43
44    /** @var controller_helper */
45    protected $controller_helper;
46
47    /** @var driver_interface */
48    protected $db;
49
50    /** @var form_helper */
51    protected $form_helper;
52
53    /** @var language */
54    protected $language;
55
56    /** @var manager */
57    protected $notification_manager;
58
59    /** @var path_helper */
60    protected $path_helper;
61
62    /** @var request_interface */
63    protected $request;
64
65    /** @var user_loader */
66    protected $user_loader;
67
68    /** @var user */
69    protected $user;
70
71    /** @var Environment */
72    protected $template;
73
74    /** @var string */
75    protected $notification_webpush_table;
76
77    /** @var string */
78    protected $push_subscriptions_table;
79
80    /**
81     * Constructor for webpush controller
82     *
83     * @param config $config
84     * @param controller_helper $controller_helper
85     * @param driver_interface $db
86     * @param form_helper $form_helper
87     * @param language $language
88     * @param manager $notification_manager
89     * @param path_helper $path_helper
90     * @param request_interface $request
91     * @param user_loader $user_loader
92     * @param user $user
93     * @param Environment $template
94     * @param string $notification_webpush_table
95     * @param string $push_subscriptions_table
96     */
97    public function __construct(config $config, controller_helper $controller_helper, driver_interface $db, form_helper $form_helper, language $language, manager $notification_manager,
98                                path_helper $path_helper, request_interface $request, user_loader $user_loader, user $user, Environment $template, string $notification_webpush_table, string $push_subscriptions_table)
99    {
100        $this->config = $config;
101        $this->controller_helper = $controller_helper;
102        $this->db = $db;
103        $this->form_helper = $form_helper;
104        $this->language = $language;
105        $this->notification_manager = $notification_manager;
106        $this->path_helper = $path_helper;
107        $this->request = $request;
108        $this->user_loader = $user_loader;
109        $this->user = $user;
110        $this->template = $template;
111        $this->notification_webpush_table = $notification_webpush_table;
112        $this->push_subscriptions_table = $push_subscriptions_table;
113    }
114
115    /**
116     * Handle request to retrieve notification data
117     *
118     * @return JsonResponse
119     */
120    public function notification(): JsonResponse
121    {
122        if (!$this->request->is_ajax() || $this->user->data['is_bot'] || $this->user->data['user_type'] == USER_INACTIVE)
123        {
124            throw new http_exception(Response::HTTP_FORBIDDEN, 'NO_AUTH_OPERATION');
125        }
126
127        if ($this->user->id() !== ANONYMOUS)
128        {
129            $notification_data = $this->get_user_notifications();
130        }
131        else
132        {
133            $notification_data = $this->get_anonymous_notifications();
134        }
135
136        // Decode and return data if everything is fine
137        $data = json_decode($notification_data, true);
138        $data['url'] = isset($data['url']) ? $this->path_helper->update_web_root_path($data['url']) : '';
139
140        return new JsonResponse($data);
141    }
142
143    /**
144     * Get notification data for logged in user
145     *
146     * @return string Notification data
147     */
148    private function get_user_notifications(): string
149    {
150        // Subscribe should only be available for logged-in "normal" users
151        if ($this->user->data['user_type'] == USER_IGNORE)
152        {
153            throw new http_exception(Response::HTTP_FORBIDDEN, 'NO_AUTH_OPERATION');
154        }
155
156        $item_id = $this->request->variable('item_id', 0);
157        $type_id = $this->request->variable('type_id', 0);
158
159        $sql = 'SELECT push_data
160            FROM ' . $this->notification_webpush_table . '
161            WHERE user_id = ' . (int) $this->user->id() . '
162                AND notification_type_id = ' . (int) $type_id . '
163                AND item_id = ' . (int) $item_id;
164        $result = $this->db->sql_query($sql);
165        $notification_data = $this->db->sql_fetchfield('push_data');
166        $this->db->sql_freeresult($result);
167
168        return $this->get_notification_data($notification_data);
169    }
170
171    /**
172     * Get notification data for not logged in user via token
173     *
174     * @return string
175     */
176    private function get_anonymous_notifications(): string
177    {
178        $token = $this->request->variable('token', '');
179
180        if ($token)
181        {
182            $item_id = $this->request->variable('item_id', 0);
183            $type_id = $this->request->variable('type_id', 0);
184            $user_id = $this->request->variable('user_id', 0);
185
186            $sql = 'SELECT push_data, push_token
187                FROM ' . $this->notification_webpush_table . '
188                WHERE user_id = ' . (int) $user_id . '
189                    AND notification_type_id = ' . (int) $type_id . '
190                    AND item_id = ' . (int) $item_id;
191            $result = $this->db->sql_query($sql);
192            $notification_row = $this->db->sql_fetchrow($result);
193            $this->db->sql_freeresult($result);
194
195            $notification_data = $notification_row['push_data'];
196            $push_token = $notification_row['push_token'];
197
198            // Check if passed push token is valid
199            $sql = 'SELECT user_form_salt, user_lang
200                FROM ' . USERS_TABLE . '
201                WHERE user_id = ' . (int) $user_id;
202            $result = $this->db->sql_query($sql);
203            $row = $this->db->sql_fetchrow($result);
204            $this->db->sql_freeresult($result);
205
206            $user_form_token = $row['user_form_salt'];
207            $user_lang = $row['user_lang'];
208
209            $expected_push_token = hash('sha256', $user_form_token . $push_token);
210            if ($expected_push_token === $token)
211            {
212                if ($user_lang !== $this->language->get_used_language())
213                {
214                    $this->language->set_user_language($user_lang, true);
215                }
216                return $this->get_notification_data($notification_data);
217            }
218        }
219
220        throw new http_exception(Response::HTTP_FORBIDDEN, 'NO_AUTH_OPERATION');
221    }
222
223    /**
224     * Get notification data for output from json encoded data stored in database
225     *
226     * @param string $notification_data Encoded data stored in database
227     *
228     * @return string Data for notification output with javascript
229     */
230    private function get_notification_data(string $notification_data): string
231    {
232        $row_data = json_decode($notification_data, true);
233
234        // Old notification data is pre-parsed and just needs to be returned
235        if (isset($row_data['heading']))
236        {
237            return $notification_data;
238        }
239
240        // Get notification from row_data
241        $notification = $this->notification_manager->get_item_type_class($row_data['notification_type_name'], $row_data);
242
243        // Load users for notification
244        $this->user_loader->load_users($notification->users_to_query());
245
246        return json_encode([
247            'heading'    => $this->config['sitename'],
248            'title'        => strip_tags(html_entity_decode($notification->get_title(), ENT_NOQUOTES, 'UTF-8')),
249            'text'        => strip_tags(html_entity_decode($notification->get_reference(), ENT_NOQUOTES, 'UTF-8')),
250            'url'        => htmlspecialchars_decode($notification->get_url()),
251            'avatar'    => $notification->get_avatar(),
252        ]);
253    }
254
255    /**
256     * Handle request to push worker javascript
257     *
258     * @return Response
259     * @throws LoaderError
260     * @throws RuntimeError
261     * @throws SyntaxError
262     */
263    public function worker(): Response
264    {
265        $content = $this->template->render('push_worker.js.twig', [
266            'U_WEBPUSH_GET_NOTIFICATION'    => $this->controller_helper->route('phpbb_ucp_push_get_notification_controller'),
267            'ASSETS_VERSION'                => $this->config['assets_version'],
268        ]);
269
270        $response = new Response($content);
271        $response->headers->set('Content-Type', 'text/javascript; charset=UTF-8');
272
273        if (!empty($this->user->data['is_bot']))
274        {
275            // Let reverse proxies know we detected a bot.
276            $response->headers->set('X-PHPBB-IS-BOT', 'yes');
277        }
278
279        return $response;
280    }
281
282    /**
283     * Check (un)subscribe form for valid link hash
284     *
285     * @throws http_exception If form is invalid or user should not request (un)subscription
286     * @return void
287     */
288    protected function check_subscribe_requests(): void
289    {
290        if (!$this->form_helper->check_form_tokens(self::FORM_TOKEN_UCP))
291        {
292            throw new http_exception(Response::HTTP_BAD_REQUEST, 'FORM_INVALID');
293        }
294
295        // Subscribe should only be available for logged-in "normal" users
296        if (!$this->request->is_ajax() || $this->user->id() === ANONYMOUS || $this->user->data['is_bot']
297            || $this->user->data['user_type'] == USER_IGNORE || $this->user->data['user_type'] == USER_INACTIVE)
298        {
299            throw new http_exception(Response::HTTP_FORBIDDEN, 'NO_AUTH_OPERATION');
300        }
301    }
302
303    /**
304     * Handle subscribe requests
305     *
306     * @param symfony_request $symfony_request
307     * @return JsonResponse
308     */
309    public function subscribe(symfony_request $symfony_request): JsonResponse
310    {
311        $this->check_subscribe_requests();
312
313        $data = json_sanitizer::decode($symfony_request->get('data', ''));
314
315        $sql = 'INSERT INTO ' . $this->push_subscriptions_table . ' ' . $this->db->sql_build_array('INSERT', [
316            'user_id'            => $this->user->id(),
317            'endpoint'            => $data['endpoint'],
318            'expiration_time'    => $data['expiration_time'] ?? 0,
319            'p256dh'            => $data['keys']['p256dh'],
320            'auth'                => $data['keys']['auth'],
321        ]);
322        $this->db->sql_query($sql);
323
324        return new JsonResponse([
325            'success'        => true,
326            'form_tokens'    => $this->form_helper->get_form_tokens(self::FORM_TOKEN_UCP),
327        ]);
328    }
329
330    /**
331     * Handle unsubscribe requests
332     *
333     * @param symfony_request $symfony_request
334     * @return JsonResponse
335     */
336    public function unsubscribe(symfony_request $symfony_request): JsonResponse
337    {
338        $this->check_subscribe_requests();
339
340        $data = json_sanitizer::decode($symfony_request->get('data', ''));
341
342        $endpoint = $data['endpoint'];
343
344        $sql = 'DELETE FROM ' . $this->push_subscriptions_table . '
345            WHERE user_id = ' . (int) $this->user->id() . "
346                AND endpoint = '" . $this->db->sql_escape($endpoint) . "'";
347        $this->db->sql_query($sql);
348
349        return new JsonResponse([
350            'success'        => true,
351            'form_tokens'    => $this->form_helper->get_form_tokens(self::FORM_TOKEN_UCP),
352        ]);
353    }
354}