• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-server / 01a0045f-f8d0-490a-ab94-b493b0d7d3b8

pending completion
01a0045f-f8d0-490a-ab94-b493b0d7d3b8

push

circleci

Edward Hibbert
Various changes for server move.

4 of 4 new or added lines in 1 file covered. (100.0%)

19563 of 20557 relevant lines covered (95.16%)

32.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.49
/include/user/PushNotifications.php
1
<?php
2
namespace Freegle\Iznik;
3

4
use Minishlink\WebPush\WebPush;
5
use Minishlink\WebPush\Subscription;
6
use Pheanstalk\Pheanstalk;
7
use Kreait\Firebase\Factory;
8
use Kreait\Firebase\ServiceAccount;
9
use Kreait\Firebase\Messaging\CloudMessage;
10
use Kreait\Firebase\Messaging\AndroidConfig;
11
use Kreait\Firebase\Messaging\ApnsConfig;
12
use Kreait\Firebase\Exception\Messaging\InvalidMessage;
13

14
class PushNotifications
15
{
16
    const PUSH_GOOGLE = 'Google';
17
    const PUSH_FIREFOX = 'Firefox';
18
    const PUSH_TEST = 'Test';
19
    const PUSH_FCM_ANDROID = 'FCMAndroid';
20
    const PUSH_FCM_IOS = 'FCMIOS';
21
    const APPTYPE_MODTOOLS = 'ModTools';
22
    const APPTYPE_USER = 'User';
23

24
    private $dbhr, $dbhm, $log, $pheanstalk = NULL, $firebase = NULL, $messaging = NULL;
25

26
    function __construct(LoggedPDO $dbhr, LoggedPDO $dbhm)
27
    {
28
        $this->dbhr = $dbhr;
536✔
29
        $this->dbhm = $dbhm;
536✔
30
        $this->log = new Log($dbhr, $dbhm);
536✔
31

32
        if (file_exists('/etc/firebase.json')) {
536✔
33
            $factory = (new Factory)
×
34
                ->withServiceAccount('/etc/firebase.json');
×
35
            $this->messaging = $factory->createMessaging();
×
36
        }
37
    }
38

39
    public function get($userid)
40
    {
41
        # Cache the notification - saves a DB call in GET of session, which is very common.
42
        $ret = Utils::presdef('notification', $_SESSION, []);
21✔
43

44
        if (!$ret) {
21✔
45
            $sql = "SELECT * FROM users_push_notifications WHERE userid = ?;";
21✔
46
            $notifs = $this->dbhr->preQuery($sql, [$userid]);
21✔
47
            foreach ($notifs as &$notif) {
21✔
48
                $notif['added'] = Utils::ISODate($notif['added']);
3✔
49
                $ret[] = $notif;
3✔
50
            }
51

52
            $_SESSION['notification'] = $ret;
21✔
53
        }
54

55
        return ($ret);
21✔
56
    }
57

58
    public function add($userid, $type, $val)
59
    {
60
        $rc = NULL;
3✔
61

62
        if ($userid) {
3✔
63
            $apptype = Session::modtools() ? PushNotifications::APPTYPE_MODTOOLS : PushNotifications::APPTYPE_USER;
3✔
64
            $sql = "INSERT INTO users_push_notifications (`userid`, `type`, `subscription`, `apptype`) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE userid = ?, type = ?, apptype = ?;";
3✔
65
            $rc = $this->dbhm->preExec($sql, [$userid, $type, $val, $apptype, $userid, $type, $apptype]);
3✔
66
            Session::clearSessionCache();
3✔
67
        }
68

69
        return ($rc);
3✔
70
    }
71

72
    public function remove($userid)
73
    {
74
        $sql = "DELETE FROM users_push_notifications WHERE userid = ?;";
2✔
75
        $rc = $this->dbhm->preExec($sql, [$userid]);
2✔
76
        return ($rc);
2✔
77
    }
78

79
    public function uthook($rc = NULL)
80
    {
81
        # Mocked in UT to force an exception.
82
        return ($rc);
99✔
83
    }
84

85
    private function queueSend($userid, $type, $params, $endpoint, $payload)
86
    {
87
        #error_log("queueSend $userid $endpoint params " . var_export($params, TRUE));
88
        try {
89
            $this->uthook();
2✔
90

91
            if (!$this->pheanstalk) {
2✔
92
                $this->pheanstalk = Pheanstalk::create(PHEANSTALK_SERVER);
2✔
93
            }
94

95
            $str = json_encode(array(
2✔
96
                'type' => 'webpush',
97
                'notiftype' => $type,
98
                'queued' => microtime(TRUE),
2✔
99
                'userid' => $userid,
100
                'params' => $params,
101
                'endpoint' => $endpoint,
102
                'payload' => $payload,
103
                'ttr' => Utils::PHEANSTALK_TTR
104
            ));
105

106
            $id = $this->pheanstalk->put($str);
2✔
107
        } catch (\Exception $e) {
2✔
108
            error_log("Beanstalk exception " . $e->getMessage());
2✔
109
            $this->pheanstalk = NULL;
2✔
110
        }
111
    }
112

113
    public function executeSend($userid, $notiftype, $params, $endpoint, $payload)
114
    {
115
        #error_log("Execute send type $notiftype params " . var_export($params, TRUE) . " payload " . var_export($payload, TRUE) . " endpoint $endpoint");
116
        try {
117
            error_log("notiftype " . $notiftype . " userid " . $userid);
2✔
118

119
            switch ($notiftype) {
120
                case PushNotifications::PUSH_FCM_ANDROID:
121
                case PushNotifications::PUSH_FCM_IOS:
122
                    {
123
                        # Everything is in one array as passed to this function; split it out into what we need
124
                        # for FCM.
125
                        #error_log("FCM notif " . var_export($payload, TRUE));
126
                        $data = $payload;
1✔
127

128
                        # We can only have key => string, so the chatids needs to be converted from an array to
129
                        # a string.
130
                        $data['chatids'] = implode(',', $data['chatids']);
1✔
131

132
                        # And anything that isn't a string needs to pretend to be one.  How dull.
133
                        foreach ($data as $key => $val) {
1✔
134
                            if (gettype($val) !== 'string') {
1✔
135
                                $data[$key] = "$val";
1✔
136
                            }
137
                        }
138

139
                        $data['notId'] = (string)floor(microtime(TRUE));
1✔
140

141
                        #error_log("Data is " . var_export($data, TRUE));
142

143
                        if ($notiftype == PushNotifications::PUSH_FCM_ANDROID) {
1✔
144
                            # Need to omit notification for reasons to do with Cordova plugin.
145
                            if ($payload['count']) {
1✔
146
                                $data['content-available'] = "1";
1✔
147
                            }
148

149
                            $message = CloudMessage::fromArray([
1✔
150
                                'token' => $endpoint,
151
                                'data' => $data
152
                            ]);
153

154
                            $message = $message->withAndroidConfig([
1✔
155
                                'ttl' => '3600s',
156
                                'priority' => 'normal'
157
                            ]);
158
                        } else {
159
                            $ios = [
1✔
160
                                'token' => $endpoint,
161
                                'data' => $data
162
                            ];
163

164
                            if (!empty($payload['title'])) {   // Don't set notification if clearing
1✔
165
                                $iostitle = $payload['title'];
1✔
166
                                $iosbody = $payload['message'];
1✔
167

168
                                if (empty($iosbody)) {  // older iOS only shows body and doesn't show if body empty
1✔
169
                                    $iosbody = $iostitle;
1✔
170
                                    $iostitle = ' ';
1✔
171
                                }
172

173
                                $ios['notification'] = [
1✔
174
                                    'title' => $iostitle,
175
                                    'body' => $iosbody
176
                                ];
177
                            }
178

179
                            #error_log("ios is " . var_export($ios, TRUE));
180
                            $message = CloudMessage::fromArray($ios);
1✔
181
                            $params = [
1✔
182
                                'headers' => [
183
                                    'apns-priority' => '10',
184
                                ],
185
                                'payload' => [
186
                                    'aps' => [
187
                                        'badge' => $payload['count'],
1✔
188
                                        'sound' => "default"
189
                                        //'content-available' => 1
190
                                    ]
191
                                ],
192
                            ];
193

194
                            #error_log("Send params " . var_export($params, TRUE));
195
                            #error_log("Send payload " . var_export($payload, TRUE));
196
                            $message = $message->withApnsConfig($params);
1✔
197
                        }
198

199
                        try {
200
                            if ($this->messaging) {
1✔
201
                                $this->messaging->validate($message);
1✔
202
                            }
203
                        } catch (InvalidMessage $e) {
×
204
                            # We might not want to remove the subscription.  Check the nature of the error
205
                            # and (for now) record unknown ones to check.
206
                            $error = $e->errors()['error'];
×
207
                            file_put_contents('/tmp/fcmerrors', date(DATE_RFC2822) . ': ' . $userid . ' - ' . $endpoint . ' - ' . var_export($error, TRUE) . "\r\n", FILE_APPEND);
×
208
                            error_log("FCM InvalidMessage " . var_export($error, TRUE));
×
209
                            $errorCode = 'CODE NOT FOUND';
×
210
                            if (array_key_exists('errorCode', $error['details'][0])) {
×
211
                                $errorCode = $error['details'][0]['errorCode'];
×
212
                            }
213
                            error_log("FCM errorCode " . $errorCode);
×
214

215
                            if ($errorCode == 'UNREGISTERED') {
×
216
                                # We do want to remove the subscription in this case.
217
                                throw new \Exception($errorCode);
×
218
                            }
219

220
                            foreach ($error['details'] as $detail) {
×
221
                                if (array_key_exists('fieldViolations', $detail)) {
×
222
                                    if ($detail['fieldViolations'][0]['description'] == 'Invalid registration token') {
×
223
                                        # We do want to remove the subscription in this case.
224
                                        throw new \Exception($detail['fieldViolations'][0]['description']);
×
225
                                    }
226
                                    if ($detail['fieldViolations'][0]['description'] == 'The registration token is not a valid FCM registration token') {
×
227
                                        # We do want to remove the subscription in this case.
228
                                        throw new \Exception($detail['fieldViolations'][0]['description']);
×
229
                                    }
230
                                }
231
                            }
232

233
                            $rc = TRUE; // Problem is ignored and subscription/token NOT removed: eyeball logs to check
×
234
                            break;
×
235
                        }
236

237
                        if ($this->messaging) {
1✔
238
                            $ret = $this->messaging->send($message);
×
239
                            error_log("FCM send " . var_export($ret, TRUE));
×
240
                        }
241

242
                        $rc = TRUE;
1✔
243
                        break;
1✔
244
                    }
245
                case PushNotifications::PUSH_GOOGLE:
246
                case PushNotifications::PUSH_FIREFOX:
247
                    $params = $params ? $params : [];
2✔
248
                    $webPush = new WebPush($params);
2✔
249
                    $subscription = Subscription::create([
2✔
250
                                                           "endpoint" => $endpoint,
251
                                                       ]);
252
                    #error_log("Send params " . var_export($params, TRUE) . " " . ($payload['count'] > 0) . "," . (!is_null($payload['title'])));
253
                    if (($payload && ($payload['count'] > 0) && (!is_null($payload['title'])))) {
2✔
254
                        $rc = $webPush->queueNotification($subscription, $payload['title']);
1✔
255
                    } else
256
                        $rc = TRUE;
2✔
257
                    break;
2✔
258
            }
259

260
            #error_log("Returned " . var_export($rc, TRUE) . " for $userid type $notiftype $endpoint payload " . var_export($payload, TRUE));
261
            $rc = $this->uthook($rc);
2✔
262
        } catch (\Exception $e) {
2✔
263
            $rc = ['exception' => $e->getMessage()];
2✔
264
            #error_log("push exc " . var_export($e, TRUE));
265
            #error_log("push exc " . $e->getMessage());
266
            error_log("Push exception {$rc['exception']}");
2✔
267
        }
268

269
        if ($rc !== TRUE) {
2✔
270
            error_log("Push Notification to $userid failed with " . var_export($rc, TRUE));
2✔
271
            $this->dbhm->preExec("DELETE FROM users_push_notifications WHERE userid = ? AND subscription = ?;", [$userid, $endpoint]);
2✔
272
        } else {
273
            # Don't log - lots of these.
274
            $this->dbhm->preExec("UPDATE users_push_notifications SET lastsent = NOW() WHERE userid = ? AND subscription = ?;", [$userid, $endpoint], FALSE);
2✔
275
        }
276

277
        return $rc;
2✔
278
    }
279

280
    public function notify($userid, $modtools)
281
    {
282
        $count = 0;
521✔
283
        $u = User::get($this->dbhr, $this->dbhm, $userid);
521✔
284
        $proceedpush = $u->notifsOn(User::NOTIFS_PUSH);
521✔
285
        $proceedapp = $u->notifsOn(User::NOTIFS_APP);
521✔
286
        #error_log("Notify $userid, push on $proceedpush app on $proceedapp MT $modtools");
287

288
        $notifs = $this->dbhr->preQuery("SELECT * FROM users_push_notifications WHERE userid = ? AND apptype = ?;", [
521✔
289
            $userid,
290
            $modtools ? PushNotifications::APPTYPE_MODTOOLS : PushNotifications::APPTYPE_USER
521✔
291
        ]);
292

293
        foreach ($notifs as $notif) {
521✔
294
            #error_log("Consider notif {$notif['id']} proceed $proceedpush type {$notif['type']}");
295
            if ($proceedpush && in_array($notif['type'],
2✔
296
                    [PushNotifications::PUSH_FIREFOX, PushNotifications::PUSH_GOOGLE]) ||
297
                ($proceedapp && in_array($notif['type'],
×
298
                        [PushNotifications::PUSH_FCM_ANDROID, PushNotifications::PUSH_FCM_IOS]))) {
299
                #error_log("Send user $userid {$notif['subscription']} type {$notif['type']} for modtools $modtools");
300
                $payload = NULL;
2✔
301
                $params = [];
2✔
302

303
                list ($total, $chatcount, $notifscount, $title, $message, $chatids, $route) = $u->getNotificationPayload($modtools);
2✔
304

305
                $message = ($total === 0) ? "" : $message;
2✔
306
                if (is_null($message)) $message = "";
2✔
307

308
                # badge and/or count are used by the app, possibly when it isn't running, to set the home screen badge.
309
                $payload = [
2✔
310
                    'badge' => $total,
311
                    'count' => $total,
312
                    'chatcount' => $chatcount,
313
                    'notifcount' => $notifscount,
314
                    'title' => $title,
315
                    'message' => $message,
316
                    'chatids' => $chatids,
317
                    'content-available' => $total > 0,
318
                    'image' => $modtools ? "www/images/modtools_logo.png" : "www/images/user_logo.png",
2✔
319
                    'modtools' => $modtools,
320
                    'sound' => 'default',
321
                    'route' => $route
322
                ];
323

324
                switch ($notif['type']) {
2✔
325
                    case PushNotifications::PUSH_GOOGLE:
326
                        {
327
                            $params = [
1✔
328
                                'GCM' => GOOGLE_PUSH_KEY
329
                            ];
330
                            break;
1✔
331
                        }
332
                }
333

334
                $this->queueSend($userid, $notif['type'], $params, $notif['subscription'], $payload);
2✔
335
                #error_log("Queued send {$notif['type']} for $userid");
336
                $count++;
2✔
337
            }
338
        }
339

340
        return ($count);
521✔
341
    }
342

343
    public function notifyGroupMods($groupid)
344
    {
345
        $count = 0;
99✔
346
        $mods = $this->dbhr->preQuery("SELECT DISTINCT userid FROM memberships WHERE groupid = ? AND role IN ('Owner', 'Moderator');",
99✔
347
            [$groupid]);
348

349
        foreach ($mods as $mod) {
99✔
350
            $u = User::get($this->dbhr, $this->dbhm, $mod['userid']);
29✔
351
            $settings = $u->getGroupSettings($groupid);
29✔
352

353
            if (!array_key_exists('pushnotify', $settings) || $settings['pushnotify']) {
29✔
354
                #error_log("Notify {$mod['userid']} for $groupid notify " . Utils::presdef('pushnotify', $settings, TRUE) . " settings " . var_export($settings, TRUE));
355
                $count += $this->notify($mod['userid'], TRUE);
29✔
356
            }
357
        }
358

359
        return ($count);
99✔
360
    }
361

362
    public function pokeGroupMods($groupid, $data)
363
    {
364
        $count = 0;
29✔
365
        $mods = $this->dbhr->preQuery("SELECT userid FROM memberships WHERE groupid = ? AND role IN ('Owner', 'Moderator');",
29✔
366
            [$groupid]);
367

368
        foreach ($mods as $mod) {
29✔
369
            $this->poke($mod['userid'], $data, TRUE);
19✔
370
            $count++;
19✔
371
        }
372

373
        return ($count);
29✔
374
    }
375

376
    public function fsockopen($host, $port, &$errno, &$errstr)
377
    {
378
        $fp = @fsockopen($host, $port, $errno, $errstr);
×
379
        return ($fp);
×
380
    }
381

382
    public function fputs($fp, $str)
383
    {
384
        return (fputs($fp, $str));
×
385
    }
386

387
    public function poke($userid, $data, $modtools)
388
    {
389
        # We background this as it hits another server, so it may be slow (especially if that server is sick).
390
        try {
391
            $this->uthook();
97✔
392

393
            if (!$this->pheanstalk) {
97✔
394
                $this->pheanstalk = Pheanstalk::create(PHEANSTALK_SERVER);
97✔
395
            }
396

397
            $str = json_encode(array(
97✔
398
                'type' => 'poke',
399
                'queued' => microtime(TRUE),
97✔
400
                'groupid' => $userid,
401
                'data' => $data,
402
                'modtools' => $modtools,
403
                'ttr' => Utils::PHEANSTALK_TTR
404
            ));
405

406
            $this->pheanstalk->put($str);
97✔
407
            $ret = TRUE;
97✔
408
        } catch (\Exception $e) {
1✔
409
            error_log("poke Beanstalk exception " . $e->getMessage());
1✔
410
            $this->pheanstalk = NULL;
1✔
411
            $ret = FALSE;
1✔
412
        }
413

414
        return ($ret);
97✔
415
    }
416

417
    public function executePoke($userid, $data, $modtools)
418
    {
419
        # This kicks a user who is online at the moment with an outstanding long poll.
420
        Utils::filterResult($data);
1✔
421

422
        # We want to POST to notify.  We can speed this up using a persistent socket.
423
        $service_uri = "/publish?id=$userid";
1✔
424

425
        $topdata = array(
1✔
426
            'text' => $data,
427
            'channel' => $userid,
428
            'modtools' => $modtools,
429
            'id' => 1
430
        );
431

432
        $vars = json_encode($topdata);
1✔
433

434
        $header = "Host: " . CHAT_HOST . "\r\n";
1✔
435
        $header .= "User-Agent: Iznik Notify\r\n";
1✔
436
        $header .= "Content-Type: application/json\r\n";
1✔
437
        $header .= "Content-Length: " . strlen($vars) . "\r\n";
1✔
438
        $header .= "Connection: close\r\n\r\n";
1✔
439

440
        # Currently we don't do anything with these.  We used to send them to an nchan instance,
441
        # but that was no longer reliable and the clients don't use them.
442
        if (CHAT_HOST && FALSE) {
1✔
443
            try {
444
                #error_log("Connect to " . CHAT_HOST . " port " . CHAT_PORT);
445
                $fp = $this->fsockopen('ssl://' . CHAT_HOST, CHAT_PORT, $errno, $errstr, 2);
×
446

447
                if ($fp) {
×
448
                    if (!$this->fputs($fp, "POST $service_uri  HTTP/1.1\r\n")) {
×
449
                        # This can happen if the socket is broken.  Just close it ready for next time.
450
                        fclose($fp);
×
451
                        error_log("Failed to post");
×
452
                    } else {
453
                        fputs($fp, $header . $vars);
×
454
                        $server_response = fread($fp, 512);
×
455
                        fclose($fp);
×
456
                        #error_log("Rsp on $service_uri $server_response");
457
                    }
458
                }
459
            } catch (\Exception $e) {
×
460
                error_log("Failed to notify");
×
461
            }
462
        }
463
    }
464
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc