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

Freegle / iznik-server / 95dbe7cb-cb22-497d-adac-3a9d01ad9cb9

19 May 2024 10:56AM UTC coverage: 94.88% (+0.001%) from 94.879%
95dbe7cb-cb22-497d-adac-3a9d01ad9cb9

push

circleci

edwh
Suppress notifications for spammers better.

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

9 existing lines in 1 file now uncovered.

25427 of 26799 relevant lines covered (94.88%)

31.31 hits per line

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

94.44
/include/user/Notifications.php
1
<?php
2
namespace Freegle\Iznik;
3

4

5
require_once(IZNIK_BASE . '/mailtemplates/notifications/notificationsoff.php');
1✔
6

7
class Notifications
8
{
9
    const TYPE_COMMENT_ON_YOUR_POST = 'CommentOnYourPost';
10
    const TYPE_COMMENT_ON_COMMENT = 'CommentOnCommented';
11
    const TYPE_LOVED_POST = 'LovedPost';
12
    const TYPE_LOVED_COMMENT = 'LovedComment';
13
    const TYPE_TRY_FEED = 'TryFeed';
14
    const TYPE_ABOUT_ME = 'AboutMe';
15
    const TYPE_EXHORT = 'Exhort';
16
    const TYPE_GIFTAID = 'GiftAid';
17
    const TYPE_OPEN_POSTS = 'OpenPosts';
18

19
    private $dbhr, $dbhm, $log;
20

21
    function __construct(LoggedPDO $dbhr, LoggedPDO $dbhm)
22
    {
23
        $this->dbhr = $dbhr;
559✔
24
        $this->dbhm = $dbhm;
559✔
25
        $this->log = new Log($dbhr, $dbhm);
559✔
26
    }
27

28
    public function countUnseen($userid) {
29
        # Don't count old notifications - if we are not going to respond to these then after a while it looks better
30
        # to stop nagging people about them.
31
        $mysqltime = date("Y-m-d", strtotime("Midnight 90 days ago"));
7✔
32
        $counts = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_notifications WHERE touser = ? AND seen = 0 AND timestamp >= ?;", [
7✔
33
            $userid,
7✔
34
            $mysqltime
7✔
35
        ]);
7✔
36
        return($counts[0]['count']);
7✔
37
    }
38

39
    private function snip(&$msg) {
40
        if ($msg) {
4✔
41
            if (strlen($msg) > 57) {
4✔
42
                $msg = wordwrap($msg, 60);
1✔
43
                $p = strpos($msg, "\n");
1✔
44
                $msg = $p !== FALSE ? substr($msg, 0, $p) : $msg;
1✔
45
                $msg .= '...';
1✔
46
            }
47
        }
48
    }
49

50
    public function get($userid, &$ctx) {
51
        $ret = [];
7✔
52
        $idq = $ctx && Utils::pres('id', $ctx) ? (" AND id < " . intval($ctx['id'])) : '';
7✔
53
        $sql = "SELECT users_notifications.* FROM users_notifications WHERE touser = ? $idq ORDER BY users_notifications.id DESC LIMIT 10;";
7✔
54
        $notifs = $this->dbhr->preQuery($sql, [ $userid ]);
7✔
55

56
        // Get all the users in one go for speed.
57
        $userids = array_filter(array_column($notifs, 'fromuser'));
7✔
58
        $users = [];
7✔
59

60
        if (count($userids)) {
7✔
61
            $u = new User($this->dbhr, $this->dbhm);
4✔
62
            $users = $u->getPublicsById($userids, NULL, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE);
4✔
63
        }
64

65
        // Similarly newsfeed.
66
        $newsids = array_filter(array_column($notifs, 'newsfeedid'));
7✔
67
        $news = [];
7✔
68

69
        if (count($newsids)) {
7✔
70
            $threads = $this->dbhr->preQuery("SELECT * FROM newsfeed WHERE id IN (" . implode(',', $newsids) . ");");
4✔
71

72
            foreach ($threads as $thread) {
4✔
73
                $news[$thread['id']] = $thread;
4✔
74
            }
75

76
            $replyids = array_filter(array_column($threads, 'replyto'));
4✔
77

78
            if (count($replyids)) {
4✔
79
                $replies = $this->dbhr->preQuery("SELECT * FROM newsfeed WHERE id IN (" . implode(',', $replyids) . ");");
3✔
80

81
                foreach ($replies as $thread) {
3✔
82
                    $news[$thread['id']] = $thread;
3✔
83
                }
84
            }
85
        }
86

87
        foreach ($notifs as &$notif) {
7✔
88
            $notif['timestamp'] = Utils::ISODate($notif['timestamp']);
7✔
89

90
            if (Utils::pres('fromuser', $notif)) {
7✔
91
                $notif['fromuser'] = $users[$notif['fromuser']];
4✔
92
            }
93

94
            if (Utils::pres('newsfeedid', $notif)) {
7✔
95
                $not = $news[$notif['newsfeedid']];
4✔
96
                unset($not['position']);
4✔
97

98
                if ($not['type'] != Newsfeed::TYPE_NOTICEBOARD) {
4✔
99
                    $this->snip($not['message']);
4✔
100
                }
101

102
                if (Utils::pres('deleted', $not)) {
4✔
103
                    # This item has been deleted - don't show the corresponding notification.
104
                    if (!$notif['seen']) {
×
105
                        # This notification hasn't been seen, and would therefore show in the count. Mark it
106
                        # as seen for next time.
107
                        $this->dbhm->background("UPDATE users_notifications SET seen = 1 WHERE id = {$notif['id']}");
×
108
                    }
109

110
                    $notif = NULL;
×
111
                } else {
112
                    if ($not['replyto']) {
4✔
113
                        $orig = Utils::presdef($not['replyto'], $news, NULL);
3✔
114

115
                        if ($orig) {
3✔
116
                            # Thread we're replying to might have been deleted.
117
                            $this->snip($orig['message']);
3✔
118
                            unset($orig['position']);
3✔
119
                        }
120

121
                        $not['replyto'] = $orig;
3✔
122
                    }
123

124
                    unset($not['position']);
4✔
125
                    $notif['newsfeed'] = $not;
4✔
126

127
                    if (Utils::pres('deleted', $not['replyto'])) {
4✔
128
                        # This notification is for a newsfeed item which is in a deleted thread.  Don't show it.
129

130
                        if (!$notif['seen']) {
1✔
131
                            # This notification hasn't been seen, and would therefore show in the count. Mark it
132
                            # as seen for next time.
133
                            $this->dbhm->background("UPDATE users_notifications SET seen = 1 WHERE id = {$notif['id']}");
1✔
134
                        }
135

136
                        $notif = NULL;
1✔
137
                    }
138
                }
139
            }
140

141
            if ($notif) {
7✔
142
                $ret[] = $notif;
7✔
143

144
                $ctx = [
7✔
145
                    'id' => $notif['id']
7✔
146
                ];
7✔
147
            }
148
        }
149

150
        return($ret);
7✔
151
    }
152

153
    public function add($from, $to, $type, $newsfeedid, $newsfeedthreadid = NULL, $url = NULL, $title = NULL, $text = NULL) {
154
        $id = NULL;
559✔
155

156
        if ($from != $to) {
559✔
157
            $n = new Newsfeed($this->dbhr, $this->dbhm);
559✔
158

159
            # For newsfeed items, ensure we don't notify if we've unfollowed.
160
            if (!$newsfeedthreadid || !$n->unfollowed($to, $newsfeedthreadid)){
559✔
161
                $sql = "INSERT INTO users_notifications (`fromuser`, `touser`, `type`, `newsfeedid`, `url`, `title`, `text`) VALUES (?, ?, ?, ?, ?, ?, ?);";
559✔
162
                $this->dbhm->preExec($sql, [ $from, $to, $type, $newsfeedid, $url, $title, $text ]);
559✔
163
                $id = $this->dbhm->lastInsertId();
559✔
164

165
                $p = new PushNotifications($this->dbhr, $this->dbhm);
559✔
166
                $p->notify($to, Session::modtools());
559✔
167
            }
168
        }
169

170
        return($id);
559✔
171
    }
172

173
    public function seen($userid, $id = NULL) {
174
        $idq = $id ? (" AND id = " . intval($id)) : '';
1✔
175
        $sql = "UPDATE users_notifications SET seen = 1 WHERE touser = ? $idq;";
1✔
176
        $rc = $this->dbhm->preExec($sql, [ $userid ] );
1✔
177

178
        #error_log("Seen notify $userid");
179
        $p = new PushNotifications($this->dbhr, $this->dbhm);
1✔
180
        $p->notify($userid, Session::modtools());
1✔
181

182
        return($rc);
1✔
183
    }
184

185
    public function off($uid) {
186
        $u = User::get($this->dbhr, $this->dbhm, $uid);
2✔
187

188
        # The user might not still exist.
189
        if ($u->getId() == $uid) {
2✔
190
            $settings = json_decode($u->getPrivate('settings'), TRUE);
2✔
191

192
            if (Utils::presdef('notificationmails', $settings, TRUE)) {
2✔
193
                $settings['notificationmails'] = FALSE;
2✔
194
                $u->setPrivate('settings', json_encode($settings));
2✔
195

196
                $this->log->log([
2✔
197
                    'type' => Log::TYPE_USER,
2✔
198
                    'subtype' => Log::SUBTYPE_NOTIFICATIONOFF,
2✔
199
                    'user' => $uid
2✔
200
                ]);
2✔
201

202
                $email = $u->getEmailPreferred();
2✔
203

204
                if ($email) {
2✔
205
                    list ($transport, $mailer) = Mail::getMailer();
1✔
206
                    $html = notifications_off(USER_SITE, USERLOGO);
1✔
207

208
                    $message = \Swift_Message::newInstance()
1✔
209
                        ->setSubject("Email Change Confirmation")
1✔
210
                        ->setFrom([NOREPLY_ADDR => SITE_NAME])
1✔
211
                        ->setReturnPath($u->getBounce())
1✔
212
                        ->setTo([ $email => $u->getName() ])
1✔
213
                        ->setBody("Thanks - we've turned off the mails for notifications.");
1✔
214

215
                    # Add HTML in base-64 as default quoted-printable encoding leads to problems on
216
                    # Outlook.
217
                    $htmlPart = \Swift_MimePart::newInstance();
1✔
218
                    $htmlPart->setCharset('utf-8');
1✔
219
                    $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
220
                    $htmlPart->setContentType('text/html');
1✔
221
                    $htmlPart->setBody($html);
1✔
222
                    $message->attach($htmlPart);
1✔
223

224
                    Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::NOTIFICATIONS_OFF, $u->getId());
1✔
225

226
                    $this->sendIt($mailer, $message);
1✔
227
                }
228
            }
229
        }
230
    }
231

232
    public function sendIt($mailer, $message) {
233
        $mailer->send($message);
×
234
    }
235

236
    public function sendEmails($userid = NULL, $before = '24 hours ago', $since = '7 days ago', $unseen = TRUE, $mailed = TRUE) {
237
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
1✔
238
        $twig = new \Twig_Environment($loader);
1✔
239

240
        $userq = $userid ? " AND `touser` = $userid " : '';
1✔
241

242
        $mysqltime = date("Y-m-d H:i:s", strtotime($before));
1✔
243
        $mysqltime2 = date("Y-m-d H:i:s", strtotime($since));
1✔
244
        $seenq = $unseen ? " AND seen = 0 ": '';
1✔
245
        $mailq = $mailed ? " AND mailed = 0 " : '';
1✔
246
        $sql = "SELECT DISTINCT(touser) FROM `users_notifications`
1✔
247
                        LEFT JOIN spam_users ON spam_users.userid = users_notifications.fromuser AND collection IN (?, ?)
248
                        WHERE timestamp <= '$mysqltime' AND timestamp >= '$mysqltime2' $seenq $mailq AND `type` NOT IN (?, ?, ?, ?, ?) 
1✔
249
                                                                            AND spam_users.id IS NULL $userq;";
1✔
250
        $users = $this->dbhr->preQuery($sql, [
1✔
251
            Spam::TYPE_SPAMMER,
1✔
252
            Spam::TYPE_PENDING_ADD,
1✔
253
            Notifications::TYPE_TRY_FEED,
1✔
254
            Notifications::TYPE_EXHORT,
1✔
255
            Notifications::TYPE_ABOUT_ME,
1✔
256
            Notifications::TYPE_GIFTAID,
1✔
257
            Notifications::TYPE_OPEN_POSTS
1✔
258
        ]);
1✔
259

260
        $total = 0;
1✔
261

262
        foreach ($users as $user) {
1✔
263
            $u = new User($this->dbhr, $this->dbhm, $user['touser']);
1✔
264
            #error_log("Consider {$user['touser']} email " . $u->getEmailPreferred());
265

266
            if ($u->sendOurMails() && $u->getSetting('notificationmails', TRUE)) {
1✔
267
                $ctx = NULL;
1✔
268
                $notifs = $this->get($user['touser'], $ctx);
1✔
269

270
                $subj = $this->getNotifTitle($notifs, $unseen);
1✔
271

272
                # Collect the info we need for the twig template.
273
                $twignotifs = [];
1✔
274

275
                foreach ($notifs as &$notif) {
1✔
276
                    $this->dbhm->preExec("UPDATE users_notifications SET mailed = 1 WHERE id = ?;", [
1✔
277
                        $notif['id']
1✔
278
                    ]);
1✔
279

280
                    if ((!$unseen || !$notif['seen']) && $notif['type'] != Notifications::TYPE_TRY_FEED) {
1✔
281
                        #error_log("Message is {$notif['newsfeed']['message']} len " . strlen($notif['newsfeed']['message']));
282
                        $fromname = ($notif['fromuser'] ? "{$notif['fromuser']['displayname']}" : "Someone");
1✔
283
                        $notif['fromname'] = $fromname;
1✔
284
                        $twignotifs[] = $notif;
1✔
285
                    }
286

287
                    if (Utils::pres('newsfeed', $notif) && Utils::pres('replyto', $notif['newsfeed']) && Utils::pres('message', $notif['newsfeed']['replyto'])) {
1✔
288
                        $this->snip($notif['newsfeed']['replyto']['message']);
1✔
289
                    }
290
                }
291

292
                $url = $u->loginLink(USER_SITE, $user['touser'], '/chitchat', 'notifemail');
1✔
293
                $noemail = 'notificationmailsoff-' . $user['touser'] . "@" . USER_DOMAIN;
1✔
294

295
                try {
296
                    $html = $twig->render('notifications/email.html', [
1✔
297
                        'count' => count($twignotifs),
1✔
298
                        'notifications'=> $twignotifs,
1✔
299
                        'settings' => $u->loginLink(USER_SITE, $u->getId(), '/settings', User::SRC_NOTIFICATIONS_EMAIL),
1✔
300
                        'email' => $u->getEmailPreferred(),
1✔
301
                        'noemail' => $noemail
1✔
302
                    ]);
1✔
303

304
                    $message = \Swift_Message::newInstance()
1✔
305
                        ->setSubject($subj)
1✔
306
                        ->setFrom([NOREPLY_ADDR => 'Freegle'])
1✔
307
                        ->setReturnPath($u->getBounce())
1✔
308
                        ->setTo([ $u->getEmailPreferred() => $u->getName() ])
1✔
309
                        ->setBody("\r\n\r\nPlease click here to read them: $url");
1✔
310

311
                    # Add HTML in base-64 as default quoted-printable encoding leads to problems on
312
                    # Outlook.
313
                    $htmlPart = \Swift_MimePart::newInstance();
1✔
314
                    $htmlPart->setCharset('utf-8');
1✔
315
                    $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
316
                    $htmlPart->setContentType('text/html');
1✔
317
                    $htmlPart->setBody($html);
1✔
318
                    $message->attach($htmlPart);
1✔
319

320
                    Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::NOTIFICATIONS, $u->getId());
1✔
321

322
                    list ($transport, $mailer) = Mail::getMailer();
1✔
323
                    $this->sendIt($mailer, $message);
1✔
324

325
                    $total += count($twignotifs);
1✔
UNCOV
326
                } catch (\Exception $e) {
×
UNCOV
327
                    error_log("Message failed with " . $e->getMessage());
×
328
                }
329
            }
330
        }
331

332
        return($total);
1✔
333
    }
334

335
    public function getNotifTitle(&$notifs, $unseen = TRUE) {
336
        # We try to make the subject more enticing if we can.  End-user content from other users is the
337
        # most tantalising.
338
        $title = '';
4✔
339
        $count = 0;
4✔
340

341
        foreach ($notifs as &$notif) {
4✔
342
            if ((!$unseen || !$notif['seen']) && $notif['type'] != Notifications::TYPE_TRY_FEED) {
4✔
343
                #error_log("Message is {$notif['newsfeed']['message']} len " . strlen($notif['newsfeed']['message']));
344
                $fromname = ($notif['fromuser'] ? "{$notif['fromuser']['displayname']}" : "Someone");
4✔
345
                $notif['fromname'] = $fromname;
4✔
346
                $notif['timestamp'] = date("D, jS F g:ia", strtotime($notif['timestamp']));
4✔
347
                $twignotifs[] = $notif;
4✔
348
                
349
                $shortmsg = NULL;
4✔
350

351
                if (Utils::pres('newsfeed', $notif) && Utils::pres('message', $notif['newsfeed']) && Utils::pres('type', $notif['newsfeed']) !== Newsfeed::TYPE_NOTICEBOARD) {
4✔
352
                    $notifmsg = $notif['newsfeed']['message'];
2✔
353
                    $shortmsg = strlen($notifmsg > 30) ? (substr($notifmsg, 0, 30) . "...") : $notifmsg;
2✔
354
                }
355

356
                # We prioritise end-user content because that's more engaging.
357
                switch ($notif['type']) {
4✔
358
                    case Notifications::TYPE_COMMENT_ON_COMMENT:
UNCOV
359
                        $title = $fromname . " replied: $shortmsg";
×
UNCOV
360
                        $count++;
×
UNCOV
361
                        break;
×
362
                    case Notifications::TYPE_COMMENT_ON_YOUR_POST:
363
                        $title = $fromname . " commented: $shortmsg";
1✔
364
                        $count++;
1✔
365
                        break;
1✔
366
                    case Notifications::TYPE_LOVED_POST:
367
                        if (!$title) {
2✔
368
                            $title = $fromname . " loved your post " . ($shortmsg ? "'$shortmsg'" : '');
2✔
369
                        }
370
                        $count++;
2✔
371
                        break;
2✔
372
                     case Notifications::TYPE_LOVED_COMMENT:
373
                         if (!$title) {
1✔
374
                             $title = $fromname . " loved your comment " . ($shortmsg ? "'$shortmsg'" : '');
1✔
375
                         }
376
                         $count++;
1✔
377
                         break;
1✔
378
                    case Notifications::TYPE_ABOUT_ME:
379
                        if (!$title) {
4✔
380
                            $title = "Why not introduce yourself to other freeglers?  You'll get a better response.";
2✔
381
                        }
382
                        $count++;
4✔
383
                        break;
4✔
384
                    case Notifications::TYPE_EXHORT:
UNCOV
385
                        if (!$title) {
×
UNCOV
386
                            $title = $notif['title'];
×
387
                        }
UNCOV
388
                        $count++;
×
UNCOV
389
                        break;
×
390
                }
391
            }
392
        }
393

394
        $title = ($count ==  1) ? $title : ("$title +" . ($count - 1) . " more...");
4✔
395

396
        return($title);
4✔
397
    }
398

399
    public function haveSent($uid, $type, $since) {
400
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
401
        $notifs = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_notifications WHERE touser = ? AND type = ? AND timestamp >= ?;", [
1✔
402
            $uid,
1✔
403
            $type,
1✔
404
            $mysqltime
1✔
405
        ]);
1✔
406

407
        $count = $notifs[0]['count'];
1✔
408
        return($count > 0);
1✔
409
    }
410

411
    public function deleteOldUserType($uid, $type, $age = "Midnight 3 days ago") {
412
        $mysqltime = date("Y-m-d", strtotime($age));
2✔
413

414
        $this->dbhr->preExec("DELETE FROM users_notifications WHERE touser = ? AND type = ? AND timestamp <= ?;", [
2✔
415
            $uid,
2✔
416
            $type,
2✔
417
            $mysqltime
2✔
418
        ]);
2✔
419

420
        $existing = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_notifications WHERE touser = ? AND type = ?;", [
2✔
421
            $uid,
2✔
422
            $type,
2✔
423
        ]);
2✔
424

425
        return $existing[0]['count'];
2✔
426
    }
427
}
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