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

Freegle / iznik-server / #2452

26 Nov 2025 05:59PM UTC coverage: 90.579% (-0.004%) from 90.583%
#2452

push

php-coveralls

edwh
Use BuildKit cache mounts for apt-get to speed up Docker builds

26335 of 29074 relevant lines covered (90.58%)

31.22 hits per line

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

95.82
/include/user/User.php
1
<?php
2
namespace Freegle\Iznik;
3

4
require_once(IZNIK_BASE . '/mailtemplates/invite.php');
5
require_once(IZNIK_BASE . '/lib/wordle/functions.php');
6
require_once(IZNIK_BASE . '/lib/GreatCircle.php');
7

8
use Jenssegers\ImageHash\ImageHash;
9

10
class User extends Entity
11
{
12
    # We have a cache of users, because we create users a _lot_, and this can speed things up significantly by avoiding
13
    # hitting the DB.
14
    static $cache = [];
15
    static $cacheDeleted = [];
16
    const CACHE_SIZE = 100;
17

18
    const OPEN_AGE = 90;
19

20
    const KUDOS_NEWBIE = 'Newbie';
21
    const KUDOS_OCCASIONAL = 'Occasional';
22
    const KUDOS_FREQUENT = 'Frequent';
23
    const KUDOS_AVID = 'Avid';
24

25
    const RATING_UP = 'Up';
26
    const RATING_DOWN = 'Down';
27
    const RATING_MINE = 'Mine';
28
    const RATING_UNKNOWN = 'Unknown';
29

30
    const RATINGS_REASON_NOSHOW = 'NoShow';
31
    const RATINGS_REASON_PUNCTUALITY = 'Punctuality';
32
    const RATINGS_REASON_GHOSTED = 'Ghosted';
33
    const RATINGS_REASON_RUDE = 'Rude';
34
    const RATINGS_REASON_OTHER = 'Other';
35

36
    const TRUST_EXCLUDED = 'Excluded';
37
    const TRUST_DECLINED = 'Declined';
38
    const TRUST_BASIC = 'Basic';
39
    const TRUST_MODERATE = 'Moderate';
40
    const TRUST_ADVANCED = 'Advanced';
41

42
    /** @var  $dbhm LoggedPDO */
43
    var $publicatts = array('id', 'firstname', 'lastname', 'fullname', 'systemrole', 'settings', 'yahooid', 'newslettersallowed', 'relevantallowed', 'bouncing', 'added', 'invitesleft', 'onholidaytill', 'tnuserid', 'ljuserid', 'deleted', 'forgotten');
44

45
    # Roles on specific groups
46
    const ROLE_NONMEMBER = 'Non-member';
47
    const ROLE_MEMBER = 'Member';
48
    const ROLE_MODERATOR = 'Moderator';
49
    const ROLE_OWNER = 'Owner';
50

51
    # Permissions
52
    const PERM_BUSINESS_CARDS = 'BusinessCardsAdmin';
53
    const PERM_NEWSLETTER = 'Newsletter';
54
    const PERM_NATIONAL_VOLUNTEERS = 'NationalVolunteers';
55
    const PERM_TEAMS = 'Teams';
56
    const PERM_GIFTAID = 'GiftAid';
57
    const PERM_SPAM_ADMIN = 'SpamAdmin';
58

59
    const HAPPY = 'Happy';
60
    const FINE = 'Fine';
61
    const UNHAPPY = 'Unhappy';
62

63
    # Role on site
64
    const SYSTEMROLE_SUPPORT = 'Support';
65
    const SYSTEMROLE_ADMIN = 'Admin';
66
    const SYSTEMROLE_USER = 'User';
67
    const SYSTEMROLE_MODERATOR = 'Moderator';
68

69
    const LOGIN_YAHOO = 'Yahoo';
70
    const LOGIN_FACEBOOK = 'Facebook';
71
    const LOGIN_GOOGLE = 'Google';
72
    const LOGIN_NATIVE = 'Native';
73
    const LOGIN_LINK = 'Link';
74

75
    const NOTIFS_EMAIL = 'email';
76
    const NOTIFS_EMAIL_MINE = 'emailmine';
77
    const NOTIFS_PUSH = 'push';
78
    const NOTIFS_APP = 'app';
79

80
    const INVITE_PENDING = 'Pending';
81
    const INVITE_ACCEPTED = 'Accepted';
82
    const INVITE_DECLINED = 'Declined';
83

84
    # Traffic sources
85
    const SRC_DIGEST = 'digest';
86
    const SRC_RELEVANT = 'relevant';
87
    const SRC_CHASEUP = 'chaseup';
88
    const SRC_CHASEUP_IDLE = 'beenawhile';
89
    const SRC_CHATNOTIF = 'chatnotif';
90
    const SRC_REPOST_WARNING = 'repostwarn';
91
    const SRC_FORGOT_PASSWORD = 'forgotpass';
92
    const SRC_PUSHNOTIF = 'pushnotif'; // From JS
93
    const SRC_EVENT_DIGEST = 'eventdigest';
94
    const SRC_VOLUNTEERING_DIGEST = 'voldigest';
95
    const SRC_VOLUNTEERING_RENEWAL = 'volrenew';
96
    const SRC_NEWSLETTER = 'newsletter';
97
    const SRC_NOTIFICATIONS_EMAIL = 'notifemail';
98
    const SRC_NEWSFEED_DIGEST = 'newsfeeddigest';
99
    const SRC_NOTICEBOARD = 'noticeboard';
100
    const SRC_ADMIN = 'admin';
101
    const SRC_DUMMY = 'dummy';
102
    const SRC_FBL = 'fbl';
103
    const SRC_CALENDAR = 'calendar';
104

105
    # Chat mod status
106
    const CHAT_MODSTATUS_MODERATED = 'Moderated';
107
    const CHAT_MODSTATUS_UNMODERATED = 'Unmoderated';
108
    const CHAT_MODSTATUS_FULLY = 'Fully';
109

110
    # Newsfeed mod status
111
    const NEWSFEED_UNMODERATED = 'Unmoderated';
112
    const NEWSFEED_MODERATED = 'Moderated';
113
    const NEWSFEED_SUPPRESSED = 'Suppressed';
114

115
    # Simple email settings.
116
    const SIMPLE_MAIL_NONE = 'None';
117
    const SIMPLE_MAIL_BASIC = 'Basic';
118
    const SIMPLE_MAIL_FULL = 'Full';
119

120
    /** @var  $log Log */
121
    private $log;
122
    var $user;
123
    private $memberships = NULL;
124
    private $ouremailid = NULL;
125
    private $emails = NULL;
126
    private $emailsord = NULL;
127
    private $profile = NULL;
128
    private $spammer = NULL;
129

130
    function __construct(LoggedPDO $dbhr, LoggedPDO $dbhm, $id = NULL)
131
    {
132
        #error_log("Construct user " .  debug_backtrace()[1]['function'] . "," . debug_backtrace()[2]['function']);
133
        # We don't use Entity::fetch because we can reduce the number of DB ops in getPublic later by
134
        # doing a more complex query here.  This adds code complexity, but as you can imagine the performance of
135
        # this class is critical.
136
        $this->log = new Log($dbhr, $dbhm);
636✔
137
        $this->notif = new PushNotifications($dbhr, $dbhm);
636✔
138
        $this->dbhr = $dbhr;
636✔
139
        $this->dbhm = $dbhm;
636✔
140
        $this->name = 'user';
636✔
141
        $this->user = NULL;
636✔
142
        $this->id = NULL;
636✔
143
        $this->table = 'users';
636✔
144
        $this->spammer = [];
636✔
145

146
        if ($id) {
636✔
147
            # Fetch the user.  There are so many users that there is no point trying to use the query cache.
148
            $sql = "SELECT * FROM users WHERE id = ?;";
597✔
149

150
            $users = $dbhr->preQuery($sql, [
597✔
151
                $id
597✔
152
            ]);
597✔
153

154
            foreach ($users as $user) {
597✔
155
                $this->user = $user;
597✔
156
                $this->id = $id;
597✔
157
            }
158
        }
159
    }
160

161
    public static function get(LoggedPDO $dbhr, LoggedPDO $dbhm, $id = NULL, $usecache = TRUE, $testonly = FALSE)
162
    {
163
        $u = NULL;
628✔
164

165
        if ($id) {
628✔
166
            # We cache the constructed user.
167
            if ($usecache && array_key_exists($id, User::$cache) && User::$cache[$id]->getId() == $id) {
596✔
168
                # We found it.
169
                # @var User
170
                $u = User::$cache[$id];
561✔
171
                #error_log("Found $id in cache with " . $u->getId());
172

173
                if (!User::$cacheDeleted[$id]) {
561✔
174
                    # And it's not zapped - so we can use it.
175
                    #error_log("Not zapped");
176
                    return ($u);
554✔
177
                } else if (!$testonly) {
433✔
178
                    # It's zapped - so refetch.  It's important that we do this using the original DB handles, because
179
                    # whatever caused us to zap the cache might have done a modification operation which in turn
180
                    # zapped the SQL read cache.
181
                    #error_log("Zapped, refetch " . $id);
182
                    $u->fetch($u->dbhr, $u->dbhm, $id, 'users', 'user', $u->publicatts);
432✔
183
                    #error_log("Fetched $id as " . $u->getId() . " mod " . $u->isModerator());
184

185
                    User::$cache[$id] = $u;
432✔
186
                    User::$cacheDeleted[$id] = FALSE;
432✔
187
                    return ($u);
432✔
188
                }
189
            }
190
        }
191

192
        # Not cached.
193
        if (!$testonly) {
628✔
194
            #error_log("$id not in cache");
195
            $u = new User($dbhr, $dbhm, $id);
628✔
196

197
            if ($id && count(User::$cache) < User::CACHE_SIZE) {
628✔
198
                # Store for next time
199
                #error_log("store $id in cache");
200
                User::$cache[$id] = $u;
596✔
201
                User::$cacheDeleted[$id] = FALSE;
596✔
202
            }
203
        }
204

205
        return ($u);
628✔
206
    }
207

208
    public static function clearCache($id = NULL)
209
    {
210
        # Remove this user from our cache.
211
        #error_log("Clear $id from cache");
212
        if ($id) {
765✔
213
            User::$cacheDeleted[$id] = TRUE;
550✔
214
        } else {
215
            User::$cache = [];
765✔
216
            User::$cacheDeleted = [];
765✔
217
        }
218
    }
219

220
    public function hashPassword($pw, $salt = PASSWORD_SALT)
221
    {
222
        return sha1($pw . $salt);
504✔
223
    }
224

225
    public function login($suppliedpw, $force = FALSE)
226
    {
227
        # TODO lockout
228
        if ($this->id) {
286✔
229
            $logins = $this->getLogins(TRUE);
286✔
230

231
            foreach ($logins as $login) {
286✔
232
                if ($login['type'] == User::LOGIN_NATIVE) {
286✔
233
                    $pw = $this->hashPassword($suppliedpw, Utils::presdef('salt', $login, PASSWORD_SALT));
286✔
234

235
                    if ($force || ($login['type'] == User::LOGIN_NATIVE && $login['uid'] == $this->id && strtolower(
286✔
236
                                $pw
286✔
237
                            ) == strtolower($login['credentials']))) {
286✔
238
                        $s = new Session($this->dbhr, $this->dbhm);
286✔
239
                        $s->create($this->id);
286✔
240

241
                        User::clearCache($this->id);
286✔
242

243
                        $l = new Log($this->dbhr, $this->dbhm);
286✔
244
                        $l->log([
286✔
245
                                    'type' => Log::TYPE_USER,
286✔
246
                                    'subtype' => Log::SUBTYPE_LOGIN,
286✔
247
                                    'byuser' => $this->id,
286✔
248
                                    'text' => 'Using email/password'
286✔
249
                                ]);
286✔
250

251
                        return (TRUE);
286✔
252
                    }
253
                }
254
            }
255
        }
256

257
        return (FALSE);
4✔
258
    }
259

260
    public function linkLogin($key)
261
    {
262
        $ret = TRUE;
2✔
263

264
        if (Utils::presdef('id', $_SESSION, NULL) != $this->id) {
2✔
265
            # We're not already logged in as this user.
266
            $ret = FALSE;
2✔
267

268
            $sql = "SELECT users_logins.* FROM users_logins INNER JOIN users ON users.id = users_logins.userid WHERE userid = ? AND type = ? AND credentials = ?;";
2✔
269

270
            $logins = $this->dbhr->preQuery($sql, [$this->id, User::LOGIN_LINK, $key]);
2✔
271
            foreach ($logins as $login) {
2✔
272
                # We found a match - log them in.
273
                $s = new Session($this->dbhr, $this->dbhm);
2✔
274
                $s->create($this->id);
2✔
275

276
                $l = new Log($this->dbhr, $this->dbhm);
2✔
277
                $l->log([
2✔
278
                    'type' => Log::TYPE_USER,
2✔
279
                    'subtype' => Log::SUBTYPE_LOGIN,
2✔
280
                    'byuser' => $this->id,
2✔
281
                    'text' => 'Using link'
2✔
282
                ]);
2✔
283

284
                $this->dbhm->preExec("UPDATE users_logins SET lastaccess = NOW() WHERE userid = ? AND type = ?;", [
2✔
285
                    $this->id,
2✔
286
                    User::LOGIN_LINK
2✔
287
                ]);
2✔
288

289
                $ret = TRUE;
2✔
290
            }
291
        }
292

293
        return ($ret);
2✔
294
    }
295

296
    public function getBounce()
297
    {
298
        return ("bounce-{$this->id}-" . time() . "@" . USER_DOMAIN);
34✔
299
    }
300

301
    public function getName($default = TRUE, $atts = NULL)
302
    {
303
        $atts = $atts ? $atts : $this->user;
597✔
304

305
        $name = NULL;
597✔
306

307
        # We may or may not have the knowledge about how the name is split out, depending
308
        # on the sign-in mechanism.
309
        if (Utils::pres('fullname', $atts)) {
597✔
310
            $name = $atts['fullname'];
508✔
311
        } else if (Utils::pres('firstname', $atts) || Utils::pres('lastname', $atts)) {
116✔
312
            $first = Utils::pres('firstname', $atts);
109✔
313
            $last = Utils::pres('lastname', $atts);
109✔
314

315
            $name = $first && $last ? "$first $last" : ($first ? $first : $last);
109✔
316
        }
317

318
        # Make sure we don't return an email if somehow one has snuck in.
319
        $name = ($name && strpos($name, '@') !== FALSE) ? substr($name, 0, strpos($name, '@')) : $name;
597✔
320

321
        # If we are logged in as this user and it's showing deleted then we've resurrected it; give it a new name.
322
        $resurrect = isset($_SESSION) && Utils::presdef('id', $_SESSION, NULL) == $this->id && strpos($name, 'Deleted User') === 0;
597✔
323

324
        if ($default &&
597✔
325
            $this->id &&
597✔
326
            (strlen(trim($name)) === 0 ||
597✔
327
                $name == 'A freegler' ||
597✔
328
                $resurrect ||
597✔
329
                (strlen($name) == 32 && preg_match('/[A-Za-z].*[0-9]|[0-9].*[A-Za-z]/', $name)) ||
597✔
330
                strpos($name, 'FBUser') !== FALSE)
597✔
331
        ) {
332
            # We have:
333
            # - no name, or
334
            # - a name derived from a Yahoo ID which is a hex string, which looks silly
335
            # - A freegler, which was an old way of anonymising.
336
            # - A very old FBUser name
337
            $u = new User($this->dbhr, $this->dbhm, $atts['id']);
18✔
338
            $email = $u->inventEmail();
18✔
339
            $name = substr($email, 0, strpos($email, '-'));
18✔
340
            $u->setPrivate('fullname', $name);
18✔
341
            $u->setPrivate('inventedname', 1);
18✔
342
        }
343

344
        # Stop silly long names.
345
        $name = strlen($name) > 32 ? (substr($name, 0, 32) . '...') : $name;
597✔
346

347
        # Numeric names confuse the client.
348
        $name = is_numeric($name) ? "$name." : $name;
597✔
349

350
        # TN group numbers in names confuse everyone.
351
        $name = User::removeTNGroup($name);
597✔
352

353
        return ($name);
597✔
354
    }
355

356
    public static function removeTNGroup($name) {
357
        # For users, we hide the "-gxxx" part of names, which will almost always be for TN members.
358
        return preg_replace('/^([\s\S]+?)-g[0-9]+$/', '$1', $name);
598✔
359
    }
360

361
    /**
362
     * @param LoggedPDO $dbhm
363
     */
364
    public function setDbhm($dbhm)
365
    {
366
        $this->dbhm = $dbhm;
2✔
367
    }
368

369
    public function create($firstname, $lastname, $fullname, $reason = '', $yahooid = NULL)
370
    {
371
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
597✔
372

373
        try {
374
            $src = Utils::presdef('src', $_SESSION, NULL);
597✔
375
            $rc = $this->dbhm->preExec("INSERT INTO users (firstname, lastname, fullname, yahooid, source) VALUES (?, ?, ?, ?, ?)",
597✔
376
                [$firstname, $lastname, $fullname, $yahooid, $src]);
597✔
377
            $id = $this->dbhm->lastInsertId();
596✔
378
        } catch (\Exception $e) {
1✔
379
            $id = NULL;
1✔
380
            $rc = 0;
1✔
381
        }
382

383
        if ($rc && $id) {
597✔
384
            $this->fetch($this->dbhm, $this->dbhm, $id, 'users', 'user', $this->publicatts);
596✔
385
            $this->log->log([
596✔
386
                'type' => Log::TYPE_USER,
596✔
387
                'subtype' => Log::SUBTYPE_CREATED,
596✔
388
                'user' => $id,
596✔
389
                'byuser' => $me ? $me->getId() : NULL,
596✔
390
                'text' => $this->getName() . " #$id " . $reason
596✔
391
            ]);
596✔
392

393
            # Encourage them to introduce themselves.
394
            $n = new Notifications($this->dbhr, $this->dbhm);
596✔
395
            $n->add(NULL, $id, Notifications::TYPE_ABOUT_ME, NULL, NULL, NULL);
596✔
396

397
            return ($id);
596✔
398
        } else {
399
            return (NULL);
1✔
400
        }
401
    }
402

403
    public function inventPassword()
404
    {
405
        $spamwords = [];
5✔
406
        $ws = $this->dbhr->preQuery("SELECT word FROM spam_keywords");
5✔
407
        foreach ($ws as $w) {
5✔
408
            $spamwords[] = strtolower($w['word']);
5✔
409
        }
410

411
        $lengths = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/distinct_word_lengths.json'), TRUE);
5✔
412
        $bigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/word_start_bigrams.json'), TRUE);
5✔
413
        $trigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/trigrams.json'), TRUE);
5✔
414

415
        $pw = '';
5✔
416

417
        do {
418
            $length = \Wordle\array_weighted_rand($lengths);
5✔
419
            $start = \Wordle\array_weighted_rand($bigrams);
5✔
420
            $word = \Wordle\fill_word($start, $length, $trigrams);
5✔
421

422
            if (!in_array(strtolower($word), $spamwords)) {
5✔
423
                $pw .= $word;
5✔
424
            }
425
        } while (strlen($pw) < 6);
5✔
426

427
        $pw = strtolower($pw);
5✔
428
        return ($pw);
5✔
429
    }
430

431
    public function getEmails($recent = FALSE, $nobouncing = FALSE)
432
    {
433
        #error_log("Get emails " .  debug_backtrace()[1]['function']);
434
        # Don't return canon - don't need it on the client.
435
        $ordq = $recent ? 'id' : 'preferred';
385✔
436

437
        if (!$this->emails || $ordq != $this->emailsord) {
385✔
438
            $bounceq = $nobouncing ? " AND bounced IS NULL " : '';
385✔
439
            $sql = "SELECT id, userid, email, preferred, added, validated FROM users_emails WHERE userid = ? $bounceq ORDER BY $ordq DESC, email ASC;";
385✔
440
            #error_log("$sql, {$this->id}");
441
            $this->emails = $this->dbhr->preQuery($sql, [$this->id]);
385✔
442
            $this->emailsord = $ordq;
385✔
443

444
            foreach ($this->emails as &$email) {
385✔
445
                $email['ourdomain'] = Mail::ourDomain($email['email']);
340✔
446
            }
447
        }
448

449
        return ($this->emails);
385✔
450
    }
451

452
    public function getEmailsById($uids) {
453
        $ret = [];
173✔
454

455
        if ($uids && count($uids)) {
173✔
456
            $sql = "SELECT id, userid, email, preferred, added, validated FROM users_emails WHERE userid IN (" . implode(',', $uids) . ") ORDER BY preferred DESC, email ASC;";
171✔
457
            $emails = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
171✔
458

459
            foreach ($emails as $email) {
171✔
460
                $email['ourdomain'] = Mail::ourDomain($email['email']);
154✔
461
                $ret[$email['userid']][] = $email;
154✔
462
            }
463
        }
464

465
        return ($ret);
173✔
466
    }
467

468
    public function getEmailPreferred($allowOurDomain = FALSE)
469
    {
470
        # This gets the email address which we think the user actually uses.  So we pay attention to:
471
        # - the preferred flag, which gets set by end user action
472
        # - the date added, as most recently added emails are most likely to be right
473
        # - exclude our own invented mails
474
        # - exclude any yahoo groups mails which have snuck in.
475
        $emails = $this->getEmails();
372✔
476
        $ret = NULL;
372✔
477

478
        foreach ($emails as $email) {
372✔
479
            if (($allowOurDomain || !Mail::ourDomain($email['email'])) &&
320✔
480
                strpos($email['email'], '@yahoogroups.') === FALSE &&
320✔
481
                strpos($email['email'], GROUP_DOMAIN) === FALSE) {
320✔
482
                $ret = $email['email'];
301✔
483
                break;
301✔
484
            }
485
        }
486

487
        return ($ret);
372✔
488
    }
489

490
    public function getOurEmail($emails = NULL)
491
    {
492
        $emails = $emails ? $emails : $this->dbhr->preQuery("SELECT id, userid, email, preferred, added, validated FROM users_emails WHERE userid = ? ORDER BY preferred DESC, added DESC;",
2✔
493
            [$this->id]);
2✔
494
        $ret = NULL;
2✔
495

496
        foreach ($emails as $email) {
2✔
497
            if (Mail::ourDomain($email['email'])) {
2✔
498
                $ret = $email['email'];
1✔
499
                break;
1✔
500
            }
501
        }
502

503
        return ($ret);
2✔
504
    }
505

506
    public function getAnEmailId()
507
    {
508
        $emails = $this->dbhr->preQuery("SELECT id FROM users_emails WHERE userid = ? ORDER BY preferred DESC;",
9✔
509
            [$this->id]);
9✔
510
        return (count($emails) == 0 ? NULL : $emails[0]['id']);
9✔
511
    }
512

513
    public function isApprovedMember($groupid)
514
    {
515
        $membs = $this->dbhr->preQuery("SELECT id FROM memberships WHERE userid = ? AND groupid = ? AND collection = 'Approved';", [$this->id, $groupid]);
324✔
516
        return (count($membs) > 0 ? $membs[0]['id'] : NULL);
324✔
517
    }
518

519
    public function getEmailAge($email)
520
    {
521
        $emails = $this->dbhr->preQuery("SELECT TIMESTAMPDIFF(HOUR, added, NOW()) AS ago FROM users_emails WHERE email LIKE ?;", [
1✔
522
            $email
1✔
523
        ]);
1✔
524

525
        return (count($emails) > 0 ? $emails[0]['ago'] : NULL);
1✔
526
    }
527

528
    public function getIdForEmail($email)
529
    {
530
        # Email is a unique key but conceivably we could be called with an email for another user.
531
        $ids = $this->dbhr->preQuery("SELECT id, userid FROM users_emails WHERE (email = ? OR canon = ?);", [
23✔
532
            $email,
23✔
533
            User::canonMail($email),
23✔
534
        ]);
23✔
535

536
        foreach ($ids as $id) {
23✔
537
            return ($id);
23✔
538
        }
539

540
        return (NULL);
1✔
541
    }
542

543
    public function getEmailById($id)
544
    {
545
        $emails = $this->dbhr->preQuery("SELECT email FROM users_emails WHERE id = ?;", [
1✔
546
            $id
1✔
547
        ]);
1✔
548

549
        $ret = NULL;
1✔
550

551
        foreach ($emails as $email) {
1✔
552
            $ret = $email['email'];
1✔
553
        }
554

555
        return ($ret);
1✔
556
    }
557

558
    public function findByTNId($id) {
559
        $ret = NULL;
4✔
560

561
        $users = $this->dbhr->preQuery("SELECT id FROM users WHERE tnuserid = ?;", [
4✔
562
            $id
4✔
563
        ]);
4✔
564

565
        foreach ($users as $user) {
4✔
566
            $ret = $user['id'];
×
567
        }
568

569
        return $ret;
4✔
570
    }
571

572
    public function findByEmail($email) {
573
        return $this->findByEmailIncludingUnvalidated($email)[0];
249✔
574
    }
575

576
    public function findByEmailIncludingUnvalidated($email)
577
    {
578
        if (preg_match('/.*\-(.*)\@' . USER_DOMAIN . '/', $email, $matches)) {
273✔
579
            # Our own email addresses have the UID in there.  This will match even if the email address has
580
            # somehow been removed from the list.
581
            $uid = $matches[1];
33✔
582
            $users = $this->dbhr->preQuery("SELECT id FROM users WHERE id = ?;", [
33✔
583
                $uid
33✔
584
            ]);
33✔
585

586
            foreach ($users as $user) {
33✔
587
                return [ $user['id'], FALSE ];
4✔
588
            }
589
        }
590

591
        # Take care not to pick up empty or null else that will cause is to overmerge.
592
        #
593
        # Use canon to match - that handles variant TN addresses or % addressing.
594
        $users = $this->dbhr->preQuery("SELECT id, userid FROM users_emails WHERE (email = ? OR canon = ?) AND canon IS NOT NULL AND LENGTH(canon) > 0;",
273✔
595
            [
273✔
596
                $email,
273✔
597
                User::canonMail($email),
273✔
598
            ]);
273✔
599

600
        foreach ($users as $user) {
273✔
601
            if ($user['userid']) {
248✔
602
                return [ $user['userid'], FALSE ];
247✔
603
            } else {
604
                // This email is not yet validated.
605
                return [ NULL, TRUE ];
1✔
606
            }
607
        }
608

609
        return [ NULL, FALSE ];
139✔
610
    }
611

612
    public function findByEmailHash($hash)
613
    {
614
        # Take care not to pick up empty or null else that will cause is to overmerge.
615
        $users = $this->dbhr->preQuery("SELECT userid FROM users_emails WHERE md5hash LIKE ? AND md5hash IS NOT NULL AND LENGTH(md5hash) > 0;",
1✔
616
            [
1✔
617
                User::canonMail($hash),
1✔
618
            ]);
1✔
619

620
        foreach ($users as $user) {
1✔
621
            return ($user['userid']);
1✔
622
        }
623

624
        return (NULL);
1✔
625
    }
626

627
    public function findByYahooId($id)
628
    {
629
        # Take care not to pick up empty or null else that will cause is to overmerge.
630
        $users = $this->dbhr->preQuery("SELECT id FROM users WHERE yahooid = ? AND yahooid IS NOT NULL AND LENGTH(yahooid) > 0;",
8✔
631
            [$id]);
8✔
632

633
        foreach ($users as $user) {
8✔
634
            return ($user['id']);
6✔
635
        }
636

637
        return (NULL);
3✔
638
    }
639

640
    public static function canonMail($email)
641
    {
642
        # Googlemail is Gmail really in US and UK.
643
        $email = str_replace('@googlemail.', '@gmail.', $email);
527✔
644
        $email = str_replace('@googlemail.co.uk', '@gmail.co.uk', $email);
527✔
645

646
        # Canonicalise TN addresses.
647
        if (preg_match('/(.*)\-(.*)(@user.trashnothing.com)/', $email, $matches)) {
527✔
648
            $email = $matches[1] . $matches[3];
4✔
649
        }
650

651
        # Remove plus addressing, which is sometimes used by spammers as a trick, except for Facebook where it
652
        # appears to be genuinely used for routing to distinct users.
653
        #
654
        # O2 puts a + at the start of an email address.  That would lead to us canonicalising all emails the same.
655
        if (substr($email, 0, 1) != '+' && preg_match('/(.*)\+(.*)(@.*)/', $email, $matches) && strpos($email, '@proxymail.facebook.com') === FALSE) {
527✔
656
            $email = $matches[1] . $matches[3];
1✔
657
        }
658

659
        # Remove dots in LHS, which are ignored by gmail and can therefore be used to give the appearance of separate
660
        # emails.
661
        $p = strpos($email, '@');
527✔
662

663
        if ($p !== FALSE) {
527✔
664
            $lhs = substr($email, 0, $p);
527✔
665
            $rhs = substr($email, $p);
527✔
666

667
            if (stripos($rhs, '@gmail') !== FALSE || stripos($rhs, '@googlemail') !== FALSE) {
527✔
668
                $lhs = str_replace('.', '', $lhs);
4✔
669
            }
670

671
            # Remove dots from the RHS - saves a little space and is the format we have historically used.
672
            # Very unlikely to introduce ambiguity.
673
            $email = $lhs . str_replace('.', '', $rhs);
527✔
674
        }
675

676
        return ($email);
527✔
677
    }
678

679
    public function addEmail($email, $primary = 1, $changeprimary = TRUE)
680
    {
681
        $email = trim($email);
528✔
682

683
        # Invalidate cache.
684
        $this->emails = NULL;
528✔
685

686
        if (stripos($email, '-owner@yahoogroups.co') !== FALSE ||
528✔
687
            stripos($email, '-volunteers@' . GROUP_DOMAIN) !== FALSE ||
527✔
688
            stripos($email, '-auto@' . GROUP_DOMAIN) !== FALSE)
528✔
689
        {
690
            # We don't allow people to add Yahoo owner addresses as the address of an individual user, or
691
            # the volunteer addresses.
692
            $rc = NULL;
1✔
693
        } else if (stripos($email, 'replyto-') !== FALSE || stripos($email, 'notify-') !== FALSE) {
527✔
694
            # This can happen because of dodgy email clients replying to the wrong place.  We don't want to end up
695
            # with this getting added to the user.
696
            $rc = NULL;
1✔
697
        } else {
698
            # If the email already exists in the table, then that's fine.  But we don't want to use INSERT IGNORE as
699
            # that scales badly for clusters.
700
            $canon = User::canonMail($email);
526✔
701

702
            # Don't cache - lots of emails so don't want to flood the query cache.
703
            $sql = "SELECT SQL_NO_CACHE id, preferred FROM users_emails WHERE userid = ? AND email = ?;";
526✔
704
            $emails = $this->dbhm->preQuery($sql, [
526✔
705
                $this->id,
526✔
706
                $email
526✔
707
            ]);
526✔
708

709
            if (count($emails) == 0) {
526✔
710
                $sql = "INSERT IGNORE INTO users_emails (userid, email, preferred, canon, backwards) VALUES (?, ?, ?, ?, ?)";
526✔
711
                $rc = $this->dbhm->preExec($sql,
526✔
712
                    [$this->id, $email, $primary, $canon, strrev($canon)]);
526✔
713
                $rc = $this->dbhm->lastInsertId();
526✔
714

715
                if ($rc && $primary) {
526✔
716
                    # Make sure no other email is flagged as primary
717
                    $this->dbhm->preExec("UPDATE users_emails SET preferred = 0 WHERE userid = ? AND id != ?;", [
526✔
718
                        $this->id,
526✔
719
                        $rc
526✔
720
                    ]);
526✔
721
                }
722
            } else {
723
                $rc = $emails[0]['id'];
31✔
724

725
                if ($changeprimary && $primary != $emails[0]['preferred']) {
31✔
726
                    # Change in status.
727
                    $this->dbhm->preExec("UPDATE users_emails SET preferred = ? WHERE id = ?;", [
4✔
728
                        $primary,
4✔
729
                        $rc
4✔
730
                    ]);
4✔
731
                }
732

733
                if ($primary) {
31✔
734
                    # Make sure no other email is flagged as primary
735
                    $this->dbhm->preExec("UPDATE users_emails SET preferred = 0 WHERE userid = ? AND id != ?;", [
29✔
736
                        $this->id,
29✔
737
                        $rc
29✔
738
                    ]);
29✔
739

740
                    # If we've set an email we might no longer be bouncing.
741
                    $this->unbounce($rc, FALSE);
29✔
742
                }
743
            }
744
        }
745

746
        $this->assignUserToToDonation($email, $this->id);
528✔
747

748
        return ($rc);
528✔
749
    }
750

751
    public function unbounce($emailid, $log)
752
    {
753
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
31✔
754
        $myid = $me ? $me->getId() : NULL;
31✔
755

756
        if ($log) {
31✔
757
            $l = new Log($this->dbhr, $this->dbhm);
2✔
758

759
            $l->log([
2✔
760
                'type' => Log::TYPE_USER,
2✔
761
                'subtype' => Log::SUBTYPE_UNBOUNCE,
2✔
762
                'user' => $this->id,
2✔
763
                'byuser' => $myid
2✔
764
            ]);
2✔
765
        }
766

767
        if ($emailid) {
31✔
768
            $this->dbhm->preExec("UPDATE bounces_emails SET reset = 1 WHERE emailid = ?;", [$emailid]);
31✔
769
        }
770

771
        $this->dbhm->preExec("UPDATE users SET bouncing = 0 WHERE id = ?;", [$this->id]);
31✔
772
    }
773

774
    public function removeEmail($email)
775
    {
776
        # Invalidate cache.
777
        $this->emails = NULL;
15✔
778

779
        $rc = $this->dbhm->preExec("DELETE FROM users_emails WHERE userid = ? AND email = ?;",
15✔
780
            [$this->id, $email]);
15✔
781
        return ($rc);
15✔
782
    }
783

784
    private function updateSystemRole($role)
785
    {
786
        #error_log("Update systemrole $role on {$this->id}");
787
        User::clearCache($this->id);
473✔
788

789
        if ($role == User::ROLE_MODERATOR || $role == User::ROLE_OWNER) {
473✔
790
            $sql = "UPDATE users SET systemrole = ? WHERE id = ? AND systemrole = ?;";
156✔
791
            $this->dbhm->preExec($sql, [User::SYSTEMROLE_MODERATOR, $this->id, User::SYSTEMROLE_USER]);
156✔
792
            $this->user['systemrole'] = $this->user['systemrole'] == User::SYSTEMROLE_USER ?
156✔
793
                User::SYSTEMROLE_MODERATOR : $this->user['systemrole'];
156✔
794
        } else if ($this->user['systemrole'] == User::SYSTEMROLE_MODERATOR) {
456✔
795
            # Check that we are still a mod on a group, otherwise we need to demote ourselves.
796
            $sql = "SELECT id FROM memberships WHERE userid = ? AND role IN (?,?);";
29✔
797
            $roles = $this->dbhr->preQuery($sql, [
29✔
798
                $this->id,
29✔
799
                User::ROLE_MODERATOR,
29✔
800
                User::ROLE_OWNER
29✔
801
            ]);
29✔
802

803
            if (count($roles) == 0) {
29✔
804
                $sql = "UPDATE users SET systemrole = ? WHERE id = ?;";
24✔
805
                $this->dbhm->preExec($sql, [User::SYSTEMROLE_USER, $this->id]);
24✔
806
                $this->user['systemrole'] = User::SYSTEMROLE_USER;
24✔
807
            }
808
        }
809
    }
810

811
    public function postToCollection($groupid)
812
    {
813
        # Which collection should we post to?  If this is a group on Yahoo then ourPostingStatus will be NULL.  We
814
        # will post to Pending, and send the message to Yahoo; if the user is unmoderated on there it will come back
815
        # to us and move to Approved.  If there is a value for ourPostingStatus, then this is a native group and
816
        # we will use that.
817
        #
818
        # We shouldn't come through here with prohibited, but if we do, best send to pending.
819
        $ps = $this->getMembershipAtt($groupid, 'ourPostingStatus');
18✔
820
        $coll = (!$ps || $ps == Group::POSTING_MODERATED || $ps == Group::POSTING_PROHIBITED) ? MessageCollection::PENDING : MessageCollection::APPROVED;
18✔
821
        return ($coll);
18✔
822
    }
823

824
    public function isBanned($groupid) {
825
        $sql = "SELECT * FROM users_banned WHERE userid = ? AND groupid = ?;";
473✔
826
        $banneds = $this->dbhr->preQuery($sql, [
473✔
827
            $this->id,
473✔
828
            $groupid
473✔
829
        ]);
473✔
830

831
        foreach ($banneds as $banned) {
473✔
832
            return TRUE;
5✔
833
        }
834

835
        return FALSE;
473✔
836
    }
837

838
    public function addMembership($groupid, $role = User::ROLE_MEMBER, $emailid = NULL, $collection = MembershipCollection::APPROVED, $byemail = NULL, $addedhere = TRUE, $manual = NULL, $g = NULL)
839
    {
840
        $this->memberships = NULL;
473✔
841
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
473✔
842
        $g = $g ? $g : Group::get($this->dbhr, $this->dbhm, $groupid);
473✔
843

844
        Session::clearSessionCache();
473✔
845

846
        # Check if we're banned
847
        if ($this->isBanned($groupid)) {
473✔
848
            return FALSE;
3✔
849
        }
850

851
        $existing = $this->dbhm->preQuery("SELECT COUNT(*) AS count FROM memberships WHERE userid = ? AND groupid = ? AND collection = ?;", [
473✔
852
            $this->id,
473✔
853
            $groupid,
473✔
854
            $collection
473✔
855
        ]);
473✔
856

857
        # We don't want to use REPLACE INTO because the membershipid is a foreign key in some tables, and if the
858
        # membership already exists, then this would cause us to delete and re-add it, which would result in the
859
        # row in the child table being deleted.
860
        $simplemail = $this->getSetting('simplemail', NULL);
473✔
861

862
        $emailfrequency = 24;
473✔
863
        $eventsallowed = 1;
473✔
864
        $volunteeringallowed = 1;
473✔
865

866
        switch ($simplemail) {
867
            case User::SIMPLE_MAIL_NONE: {
868
                $emailfrequency = 0;
1✔
869
                $eventsallowed = 0;
1✔
870
                $volunteeringallowed = 0;
1✔
871
                break;
1✔
872
            }
873

874
            case User::SIMPLE_MAIL_BASIC: {
875
                $emailfrequency = 24;
×
876
                $eventsallowed = 0;
×
877
                $volunteeringallowed = 0;
×
878
                break;
×
879
            }
880

881
            case User::SIMPLE_MAIL_FULL: {
882
                $emailfrequency = -1;
×
883
                $eventsallowed = 1;
×
884
                $volunteeringallowed = 1;
×
885
                break;
×
886
            }
887
        }
888

889
        $rc = $this->dbhm->preExec("INSERT INTO memberships (userid, groupid, role, collection, emailfrequency, eventsallowed, volunteeringallowed) VALUES (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), role = ?, collection = ?, emailfrequency = ?, eventsallowed = ?, volunteeringallowed = ?;", [
473✔
890
            $this->id,
473✔
891
            $groupid,
473✔
892
            $role,
473✔
893
            $collection,
473✔
894
            $emailfrequency,
473✔
895
            $eventsallowed,
473✔
896
            $volunteeringallowed,
473✔
897
            $role,
473✔
898
            $collection,
473✔
899
            $emailfrequency,
473✔
900
            $eventsallowed,
473✔
901
            $volunteeringallowed
473✔
902
        ]);
473✔
903

904
        $membershipid = $this->dbhm->lastInsertId();
473✔
905

906
        # We added it if it wasn't there before and the INSERT worked.
907
        $added = $this->dbhm->rowsAffected() && $existing[0]['count'] == 0;
473✔
908

909
        # Record the operation for abuse detection.  Setting processingrequired will cause background code to do
910
        # work.
911
        $this->dbhm->preExec("INSERT INTO memberships_history (userid, groupid, collection, processingrequired) VALUES (?,?,?,?);", [
473✔
912
            $this->id,
473✔
913
            $groupid,
473✔
914
            $collection,
473✔
915
            $added
473✔
916
        ]);
473✔
917

918
        $historyid = $this->dbhm->lastInsertId();
473✔
919

920
        # We might need to update the systemrole.
921
        #
922
        # Not the end of the world if this fails.
923
        $this->updateSystemRole($role);
473✔
924

925
        // @codeCoverageIgnoreStart
926
        if ($byemail) {
927
            list ($transport, $mailer) = Mail::getMailer();
928
            $message = \Swift_Message::newInstance()
929
                ->setSubject("Welcome to " . $g->getPrivate('nameshort'))
930
                ->setFrom($g->getAutoEmail())
931
                ->setReplyTo($g->getModsEmail())
932
                ->setTo($byemail)
933
                ->setDate(time())
934
                ->setBody("Pleased to meet you.");
935

936
            Mail::addHeaders($this->dbhr, $this->dbhm, $message,Mail::WELCOME, $this->id);
937

938
            $this->sendIt($mailer, $message);
939
        }
940
        // @codeCoverageIgnoreEnd
941

942
        if ($added) {
473✔
943
            $l = new Log($this->dbhr, $this->dbhm);
473✔
944
            $text = NULL;
473✔
945

946
            if ($manual !== NULL) {
473✔
947
                $text = $manual ? 'Manual' : 'Auto';
4✔
948
            }
949

950
            $l->log([
473✔
951
                'type' => Log::TYPE_GROUP,
473✔
952
                'subtype' => Log::SUBTYPE_JOINED,
473✔
953
                'user' => $this->id,
473✔
954
                'byuser' => $me ? $me->getId() : NULL,
473✔
955
                'groupid' => $groupid,
473✔
956
                'text' => $text
473✔
957
            ]);
473✔
958
        }
959

960
        return ($rc);
473✔
961
    }
962

963
    public function cacheMemberships($id = NULL)
964
    {
965
        $id = $id ? $id : $this->id;
355✔
966

967
        # We get all the memberships in a single call, because some members are on many groups and this can
968
        # save hundreds of calls to the DB.
969
        if (!$this->memberships) {
355✔
970
            $this->memberships = [];
355✔
971

972
            $membs = $this->dbhr->preQuery("SELECT memberships.*, groups.type FROM memberships INNER JOIN `groups` ON groups.id = memberships.groupid WHERE userid = ?;", [ $id ]);
355✔
973
            foreach ($membs as $memb) {
355✔
974
                $this->memberships[$memb['groupid']] = $memb;
328✔
975
            }
976
        }
977

978
        return ($this->memberships);
355✔
979
    }
980

981
    public function clearMembershipCache()
982
    {
983
        $this->memberships = NULL;
298✔
984
    }
985

986
    public function getMembershipAtt($groupid, $att)
987
    {
988
        $this->cacheMemberships();
183✔
989
        $val = NULL;
183✔
990
        if (Utils::pres($groupid, $this->memberships)) {
183✔
991
            $val = Utils::presdef($att, $this->memberships[$groupid], NULL);
178✔
992
        }
993

994
        return ($val);
183✔
995
    }
996

997
    public function setMembershipAtt($groupid, $att, $val)
998
    {
999
        $this->clearMembershipCache();
242✔
1000
        Session::clearSessionCache();
242✔
1001
        $sql = "UPDATE memberships SET $att = ? WHERE groupid = ? AND userid = ?;";
242✔
1002
        $rc = $this->dbhm->preExec($sql, [
242✔
1003
            $val,
242✔
1004
            $groupid,
242✔
1005
            $this->id
242✔
1006
        ]);
242✔
1007

1008
        return ($rc);
242✔
1009
    }
1010

1011
    public function setMembershipAttId($id, $att, $val)
1012
    {
1013
        $this->clearMembershipCache();
×
1014
        Session::clearSessionCache();
×
1015
        $sql = "UPDATE memberships SET $att = ? WHERE id = ?;";
×
1016
        $rc = $this->dbhm->preExec($sql, [
×
1017
            $val,
×
1018
            $id
×
1019
        ]);
×
1020

1021
        return ($rc);
×
1022
    }
1023

1024
    public function removeMembership($groupid, $ban = FALSE, $spam = FALSE, $byemail = NULL)
1025
    {
1026
        $this->clearMembershipCache();
40✔
1027
        $g = Group::get($this->dbhr, $this->dbhm, $groupid);
40✔
1028
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
40✔
1029
        $meid = $me ? $me->getId() : NULL;
40✔
1030

1031
        // @codeCoverageIgnoreStart
1032
        //
1033
        // Let them know.  We always want to let TN know if a member is removed/banned so that they can't see
1034
        // the messages.
1035
        if ($byemail || $this->isTN()) {
1036
            list ($transport, $mailer) = Mail::getMailer();
1037
            $message = \Swift_Message::newInstance()
1038
                ->setSubject("Farewell from " . $g->getPrivate('nameshort'))
1039
                ->setFrom($g->getAutoEmail())
1040
                ->setReplyTo($g->getModsEmail())
1041
                ->setTo($this->getEmailPreferred())
1042
                ->setDate(time())
1043
                ->setBody("Parting is such sweet sorrow.");
1044

1045
            Mail::addHeaders($this->dbhr, $this->dbhm, $message,Mail::REMOVED, $this->id);
1046

1047
            $this->sendIt($mailer, $message);
1048
        }
1049
        // @codeCoverageIgnoreEnd
1050

1051
        if ($ban) {
40✔
1052
            $sql = "INSERT IGNORE INTO users_banned (userid, groupid, byuser) VALUES (?,?,?);";
13✔
1053
            $this->dbhm->preExec($sql, [
13✔
1054
                $this->id,
13✔
1055
                $groupid,
13✔
1056
                $meid
13✔
1057
            ]);
13✔
1058

1059
            # Mark any messages on this group as withdrawn.  Not strictly TRUE, but it will stop people replying
1060
            # while keeping the messages around for stats purposes and in case we want to look at them.
1061
            $msgs = $this->dbhr->preQuery("SELECT messages_groups.msgid FROM messages_groups 
13✔
1062
    INNER JOIN messages ON messages_groups.msgid = messages.id 
1063
    LEFT JOIN messages_outcomes ON messages_outcomes.msgid = messages_groups.msgid 
1064
    WHERE messages.fromuser = ? AND messages_groups.groupid = ? AND messages.type IN (?, ?);", [
13✔
1065
                $this->id,
13✔
1066
                $groupid,
13✔
1067
                Message::TYPE_OFFER,
13✔
1068
                Message::TYPE_WANTED
13✔
1069
            ]);
13✔
1070

1071
            foreach ($msgs as $msg) {
13✔
1072
                $m = new Message($this->dbhr, $this->dbhm, $msg['msgid']);
1✔
1073
                if (!$m->hasOutcome()) {
1✔
1074
                    $m->mark(Message::OUTCOME_WITHDRAWN, "Marked as withdrawn by ban", NULL, NULL);
1✔
1075
                }
1076
            }
1077
        }
1078

1079
        # Now remove the membership.
1080
        $rc = $this->dbhm->preExec("DELETE FROM memberships WHERE userid = ? AND groupid = ?;",
40✔
1081
            [
40✔
1082
                $this->id,
40✔
1083
                $groupid
40✔
1084
            ]);
40✔
1085

1086
        if ($this->dbhm->rowsAffected() || $ban) {
40✔
1087
            $l = new Log($this->dbhr, $this->dbhm);
40✔
1088
            $l->log([
40✔
1089
                'type' => Log::TYPE_GROUP,
40✔
1090
                'subtype' => Log::SUBTYPE_LEFT,
40✔
1091
                'user' => $this->id,
40✔
1092
                'byuser' => $meid,
40✔
1093
                'groupid' => $groupid,
40✔
1094
                'text' => $spam ? "Autoremoved spammer" : ($ban ? "via ban" : NULL)
40✔
1095
            ]);
40✔
1096
        }
1097

1098
        return ($rc);
40✔
1099
    }
1100

1101
    public function getMembershipGroupIds($modonly = FALSE, $grouptype = NULL, $id = NULL) {
1102
        $id = $id ? $id : $this->id;
5✔
1103

1104
        $ret = [];
5✔
1105
        $modq = $modonly ? " AND role IN ('Owner', 'Moderator') " : "";
5✔
1106
        $typeq = $grouptype ? (" AND `type` = " . $this->dbhr->quote($grouptype)) : '';
5✔
1107
        $publishq = Session::modtools() ? "" : "AND groups.publish = 1";
5✔
1108
        $sql = "SELECT groupid FROM memberships INNER JOIN `groups` ON groups.id = memberships.groupid $publishq WHERE userid = ? $modq $typeq ORDER BY memberships.added DESC;";
5✔
1109
        $groups = $this->dbhr->preQuery($sql, [$id]);
5✔
1110
        #error_log("getMemberships $sql {$id} " . var_export($groups, TRUE));
1111
        $groupids = array_filter(array_column($groups, 'groupid'));
5✔
1112
        return $groupids;
5✔
1113
    }
1114

1115
    public function getMemberships($modonly = FALSE, $grouptype = NULL, $getwork = FALSE, $pernickety = FALSE, $id = NULL)
1116
    {
1117
        $id = $id ? $id : $this->id;
162✔
1118

1119
        $ret = [];
162✔
1120
        $modq = $modonly ? " AND role IN ('Owner', 'Moderator') " : "";
162✔
1121
        $typeq = $grouptype ? (" AND `type` = " . $this->dbhr->quote($grouptype)) : '';
162✔
1122
        $publishq = Session::modtools() ? "" : "AND groups.publish = 1";
162✔
1123
        $sql = "SELECT type, memberships.heldby, memberships.settings, collection, emailfrequency, eventsallowed, volunteeringallowed, groupid, role, configid, ourPostingStatus, CASE WHEN namefull IS NOT NULL THEN namefull ELSE nameshort END AS namedisplay FROM memberships INNER JOIN `groups` ON groups.id = memberships.groupid $publishq WHERE userid = ? $modq $typeq ORDER BY LOWER(namedisplay) ASC;";
162✔
1124
        $groups = $this->dbhr->preQuery($sql, [$id]);
162✔
1125
        #error_log("getMemberships $sql {$id} " . var_export($groups, TRUE));
1126

1127
        $c = new ModConfig($this->dbhr, $this->dbhm);
162✔
1128

1129
        # Get all the groups efficiently.
1130
        $groupids = array_filter(array_column($groups, 'groupid'));
162✔
1131
        $gc = new GroupCollection($this->dbhr, $this->dbhm, $groupids);
162✔
1132
        $groupobjs = $gc->get();
162✔
1133
        $getworkids = [];
162✔
1134
        $groupsettings = [];
162✔
1135

1136
        for ($i = 0; $i < count($groupids); $i++) {
162✔
1137
            $group = $groups[$i];
126✔
1138
            $g = $groupobjs[$i];
126✔
1139
            $one = $g->getPublic();
126✔
1140

1141
            $one['role'] = $group['role'];
126✔
1142
            $one['collection'] = $group['collection'];
126✔
1143
            $amod = ($one['role'] == User::ROLE_MODERATOR || $one['role'] == User::ROLE_OWNER);
126✔
1144
            $one['configid'] = Utils::presdef('configid', $group, NULL);
126✔
1145

1146
            if ($amod && !Utils::pres('configid', $one)) {
126✔
1147
                # Get a config using defaults.
1148
                $one['configid'] = $c->getForGroup($id, $group['groupid']);
31✔
1149
            }
1150

1151
            $one['mysettings'] = $this->getGroupSettings($group['groupid'], Utils::presdef('configid', $one, NULL), $id);
126✔
1152

1153
            # If we don't have our own email on this group we won't be sending mails.  This is what affects what
1154
            # gets shown on the Settings page for the user, and we only want to check this here
1155
            # for performance reasons.
1156
            $one['mysettings']['emailfrequency'] = ($group['type'] ==  Group::GROUP_FREEGLE &&
126✔
1157
                ($pernickety || $this->sendOurMails($g, FALSE, FALSE))) ?
126✔
1158
                (array_key_exists('emailfrequency', $one['mysettings']) ? $one['mysettings']['emailfrequency'] :  24)
86✔
1159
                : 0;
41✔
1160

1161
            $groupsettings[$group['groupid']] = $one['mysettings'];
126✔
1162

1163
            if ($getwork) {
126✔
1164
                # We need to find out how much work there is whether or not we are an active mod because we need
1165
                # to be able to see that it is there.  The UI shows it less obviously.
1166
                if ($amod) {
21✔
1167
                    $getworkids[] = $group['groupid'];
19✔
1168
                }
1169
            }
1170

1171
            $ret[] = $one;
126✔
1172
        }
1173

1174
        if ($getwork) {
162✔
1175
            # Get all the work.  This is across all groups for performance.
1176
            $g = new Group($this->dbhr, $this->dbhm);
27✔
1177
            $work = $g->getWorkCounts($groupsettings, $groupids);
27✔
1178

1179
            foreach ($getworkids as $groupid) {
27✔
1180
                foreach ($ret as &$group) {
19✔
1181
                    if ($group['id'] == $groupid) {
19✔
1182
                        $group['work'] = $work[$groupid];
19✔
1183
                    }
1184
                }
1185
            }
1186

1187
            # We might have been returned extra group info for wider chat review.  Add any extra groups from the work
1188
            # counts, in a basic way, so that the work counts appear.
1189
            #
1190
            # Find groupids in $work which are not in $ret.
1191
            $existingids = [];
27✔
1192
            foreach ($ret as $r) {
27✔
1193
                $existingids[] = $r['id'];
21✔
1194
            }
1195

1196

1197
            $extraworkids = [];
27✔
1198

1199
            foreach ($work as $gid => $w) {
27✔
1200
                if (!in_array($gid, $existingids)) {
21✔
1201
                    $extraworkids[] = $gid;
1✔
1202
                }
1203
            }
1204

1205
            # We have some subtle and baffling reference thing going on which is trashing the array.
1206
            # Do a json_encode/decode to remove them.
1207
            $ret = json_decode(json_encode($ret), TRUE);
27✔
1208
            foreach ($extraworkids as $groupid) {
27✔
1209
                $g = Group::get($this->dbhr, $this->dbhm, $groupid);
1✔
1210
                $group = $g->getPublic($groupid, TRUE);
1✔
1211
                $group['work'] = $work[$groupid];
1✔
1212
                $group['role'] = User::ROLE_NONMEMBER;
1✔
1213
                $ret[] = $group;
1✔
1214
            }
1215
        }
1216

1217
        return ($ret);
162✔
1218
    }
1219

1220
    public function getConfigs($all)
1221
    {
1222
        $ret = [];
23✔
1223
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
23✔
1224

1225
        if ($all) {
23✔
1226
            # We can see configs which
1227
            # - we created
1228
            # - are used by mods on groups on which we are a mod
1229
            # - defaults
1230
            $modships = $me ? $this->getModeratorships() : [];
22✔
1231
            $modships = count($modships) > 0 ? $modships : [0];
22✔
1232

1233
            $sql = "SELECT DISTINCT id FROM ((SELECT configid AS id FROM memberships WHERE groupid IN (" . implode(',', $modships) . ") AND role IN ('Owner', 'Moderator') AND configid IS NOT NULL) UNION (SELECT id FROM mod_configs WHERE createdby = {$this->id} OR `default` = 1)) t;";
22✔
1234
            $ids = $this->dbhr->preQuery($sql);
22✔
1235
        } else {
1236
            # We only want to see the configs that we are actively using.  This reduces the size of what we return
1237
            # for people on many groups.
1238
            $sql = "SELECT DISTINCT configid AS id FROM memberships WHERE userid = ? AND configid IS NOT NULL;";
3✔
1239
            $ids = $this->dbhr->preQuery($sql, [ $me->getId() ]);
3✔
1240
        }
1241

1242
        $configids = array_filter(array_column($ids, 'id'));
23✔
1243

1244
        if ($configids) {
23✔
1245
            # Get all the info we need for the modconfig object in a single SELECT for performance.  This is particularly
1246
            # valuable for people on many groups and therefore with access to many modconfigs.
1247
            $sql = "SELECT DISTINCT mod_configs.*, 
4✔
1248
        CASE WHEN users.fullname IS NOT NULL THEN users.fullname ELSE CONCAT(users.firstname, ' ', users.lastname) END AS createdname 
1249
        FROM mod_configs LEFT JOIN users ON users.id = mod_configs.createdby
1250
        WHERE mod_configs.id IN (" . implode(',', $configids) . ");";
4✔
1251
            $configs = $this->dbhr->preQuery($sql);
4✔
1252

1253
            # Also get all the bulk ops and standard messages, again for performance.
1254
            $stdmsgs = $this->dbhr->preQuery("SELECT DISTINCT * FROM mod_stdmsgs WHERE configid IN (" . implode(',', $configids) . ");");
4✔
1255
            $bulkops = $this->dbhr->preQuery("SELECT * FROM mod_bulkops WHERE configid IN (" . implode(',', $configids) . ");");
4✔
1256

1257
            foreach ($configs as $config) {
4✔
1258
                $c = new ModConfig($this->dbhr, $this->dbhm, $config['id'], $config, $stdmsgs, $bulkops);
4✔
1259
                $thisone = $c->getPublic(FALSE);
4✔
1260

1261
                if (Utils::pres('createdby', $config)) {
4✔
1262
                    $ctx = NULL;
3✔
1263
                    $thisone['createdby'] = [
3✔
1264
                        'id' => $config['createdby'],
3✔
1265
                        'displayname' => $config['createdname']
3✔
1266
                    ];
3✔
1267
                }
1268

1269
                $ret[] = $thisone;
4✔
1270
            }
1271
        }
1272

1273
        # Return in alphabetical order.
1274
        $rc = usort($ret, function ($a, $b) {
23✔
1275
            return strcasecmp($a['name'], $b['name']);
1✔
1276
        });
23✔
1277

1278
        return ($ret);
23✔
1279
    }
1280

1281
    public function getModeratorships($id = NULL, $activeonly = FALSE)
1282
    {
1283
        $this->cacheMemberships($id);
148✔
1284
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
148✔
1285

1286
        $ret = [];
148✔
1287
        foreach ($this->memberships AS $membership) {
148✔
1288
            if ($membership['role'] == 'Owner' || $membership['role'] == 'Moderator') {
123✔
1289
                if (!$activeonly || $me->activeModForGroup($membership['groupid'])) {
91✔
1290
                    $ret[] = $membership['groupid'];
91✔
1291
                }
1292
            }
1293
        }
1294

1295
        return ($ret);
148✔
1296
    }
1297

1298
    public function isModOrOwner($groupid)
1299
    {
1300
        # Very frequently used.  Cache in session.
1301
        #error_log("modOrOwner " . var_export($_SESSION['modorowner'], TRUE));
1302
        if ((session_status() !== PHP_SESSION_NONE || getenv('UT')) &&
175✔
1303
            array_key_exists('modorowner', $_SESSION) &&
175✔
1304
            array_key_exists($this->id, $_SESSION['modorowner']) &&
175✔
1305
            array_key_exists($groupid, $_SESSION['modorowner'][$this->id])) {
175✔
1306
            return ($_SESSION['modorowner'][$this->id][$groupid]);
31✔
1307
        } else {
1308
            $sql = "SELECT groupid FROM memberships WHERE userid = ? AND role IN ('Moderator', 'Owner') AND groupid = ?;";
175✔
1309
            #error_log("$sql {$this->id}, $groupid");
1310
            $groups = $this->dbhr->preQuery($sql, [
175✔
1311
                $this->id,
175✔
1312
                $groupid
175✔
1313
            ]);
175✔
1314

1315
            foreach ($groups as $group) {
175✔
1316
                $_SESSION['modorowner'][$this->id][$groupid] = TRUE;
41✔
1317
                return TRUE;
41✔
1318
            }
1319

1320
            $_SESSION['modorowner'][$this->id][$groupid] = FALSE;
160✔
1321
            return (FALSE);
160✔
1322
        }
1323
    }
1324

1325
    public function getLogins($credentials = TRUE, $id = NULL, $excludelink = FALSE)
1326
    {
1327
        $excludelinkq = $excludelink ? (" AND type != '" . User::LOGIN_LINK . "'") : '';
296✔
1328

1329
        $logins = $this->dbhr->preQuery("SELECT * FROM users_logins WHERE userid = ? $excludelinkq ORDER BY lastaccess DESC;",
296✔
1330
            [$id ? $id : $this->id]);
296✔
1331

1332
        foreach ($logins as &$login) {
296✔
1333
            if (!$credentials) {
294✔
1334
                unset($login['credentials']);
24✔
1335
            }
1336
            $login['added'] = Utils::ISODate($login['added']);
294✔
1337
            $login['lastaccess'] = Utils::ISODate($login['lastaccess']);
294✔
1338
            $login['uid'] = '' . $login['uid'];
294✔
1339
        }
1340

1341
        return ($logins);
296✔
1342
    }
1343

1344
    public function findByLogin($type, $uid)
1345
    {
1346
        $logins = $this->dbhr->preQuery("SELECT * FROM users_logins WHERE uid = ? AND type = ?;",
5✔
1347
            [$uid, $type]);
5✔
1348

1349
        foreach ($logins as $login) {
5✔
1350
            return ($login['userid']);
4✔
1351
        }
1352

1353
        return (NULL);
5✔
1354
    }
1355

1356
    public function addLogin($type, $uid, $creds = NULL, $salt = PASSWORD_SALT)
1357
    {
1358
        if ($type == User::LOGIN_NATIVE) {
506✔
1359
            # Native login - encrypt the password a bit.  The password salt is global in FD, but per-login for users
1360
            # migrated from Norfolk.
1361
            $creds = $this->hashPassword($creds, $salt);
504✔
1362
            $uid = $this->id;
504✔
1363
        }
1364

1365
        # If the login with this type already exists in the table, that's fine.
1366
        $sql = "INSERT INTO users_logins (userid, uid, type, credentials, salt) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE credentials = ?, salt = ?;";
506✔
1367
        $rc = $this->dbhm->preExec($sql,
506✔
1368
            [$this->id, $uid, $type, $creds, $salt, $creds, $salt]);
506✔
1369

1370
        # If we add a login, we might be about to log in.
1371
        # TODO This is a bit hacky.
1372
        global $sessionPrepared;
506✔
1373
        $sessionPrepared = FALSE;
506✔
1374

1375
        return ($rc);
506✔
1376
    }
1377

1378
    public function removeLogin($type, $uid)
1379
    {
1380
        $rc = $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ? AND type = ? AND uid = ?;",
4✔
1381
            [$this->id, $type, $uid]);
4✔
1382
        return ($rc);
4✔
1383
    }
1384

1385
    public function getRoleForGroup($groupid, $overrides = TRUE)
1386
    {
1387
        # We can have a number of roles on a group
1388
        # - none, we can only see what is member
1389
        # - member, we are a group member and can see some extra info
1390
        # - moderator, we can see most info on a group
1391
        # - owner, we can see everything
1392
        #
1393
        # If our system role is support then we get moderator status; if it's admin we get owner status.
1394
        $role = User::ROLE_NONMEMBER;
75✔
1395

1396
        if ($overrides) {
75✔
1397
            switch ($this->getPrivate('systemrole')) {
35✔
1398
                case User::SYSTEMROLE_SUPPORT:
1399
                    $role = User::ROLE_MODERATOR;
3✔
1400
                    break;
3✔
1401
                case User::SYSTEMROLE_ADMIN:
1402
                    $role = User::ROLE_OWNER;
1✔
1403
                    break;
1✔
1404
            }
1405
        }
1406

1407
        # Now find if we have any membership of the group which might also give us a role.
1408
        $membs = $this->dbhr->preQuery("SELECT role FROM memberships WHERE userid = ? AND groupid = ?;", [
75✔
1409
            $this->id,
75✔
1410
            $groupid
75✔
1411
        ]);
75✔
1412

1413
        foreach ($membs as $memb) {
75✔
1414
            switch ($memb['role']) {
72✔
1415
                case 'Moderator':
72✔
1416
                    # Don't downgrade from owner if we have that by virtue of an override.
1417
                    $role = $role == User::ROLE_OWNER ? $role : User::ROLE_MODERATOR;
36✔
1418
                    break;
36✔
1419
                case 'Owner':
63✔
1420
                    $role = User::ROLE_OWNER;
8✔
1421
                    break;
8✔
1422
                case 'Member':
61✔
1423
                    # Don't downgrade if we already have a role by virtue of an override.
1424
                    $role = $role == User::ROLE_NONMEMBER ? User::ROLE_MEMBER : $role;
61✔
1425
                    break;
61✔
1426
            }
1427
        }
1428

1429
        return ($role);
75✔
1430
    }
1431

1432
    public function moderatorForUser($userid, $allowmod = FALSE)
1433
    {
1434
        # There are times when we want to check whether we can administer a user, but when we are not immediately
1435
        # within the context of a known group.  We can administer a user when:
1436
        # - they're only a user themselves
1437
        # - we are a mod on one of the groups on which they are a member.
1438
        # - it's us
1439
        if ($userid != $this->getId()) {
15✔
1440
            $u = User::get($this->dbhr, $this->dbhm, $userid);
12✔
1441

1442
            $usermemberships = [];
12✔
1443
            $modq = $allowmod ? ", 'Moderator', 'Owner'" : '';
12✔
1444
            $groups = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ? AND role IN ('Member' $modq);", [$userid]);
12✔
1445
            foreach ($groups as $group) {
12✔
1446
                $usermemberships[] = $group['groupid'];
8✔
1447
            }
1448

1449
            $mymodships = $this->getModeratorships();
12✔
1450

1451
            # Is there any group which we mod and which they are a member of?
1452
            $canmod = count(array_intersect($usermemberships, $mymodships)) > 0;
12✔
1453
        } else {
1454
            $canmod = TRUE;
5✔
1455
        }
1456

1457
        return ($canmod);
15✔
1458
    }
1459

1460
    public function getSetting($setting, $default)
1461
    {
1462
        $ret = $default;
477✔
1463
        $s = $this->getPrivate('settings');
477✔
1464

1465
        if ($s) {
477✔
1466
            $settings = json_decode($s, TRUE);
31✔
1467
            $ret = array_key_exists($setting, $settings) ? $settings[$setting] : $default;
31✔
1468
        }
1469

1470
        return ($ret);
477✔
1471
    }
1472

1473
    public function setSetting($setting, $val)
1474
    {
1475
        $s = $this->getPrivate('settings');
30✔
1476

1477
        if ($s) {
30✔
1478
            $settings = json_decode($s, TRUE);
1✔
1479
        } else {
1480
            $settings = [];
30✔
1481
        }
1482

1483
        $settings[$setting] = $val;
30✔
1484
        $this->setPrivate('settings', json_encode($settings));
30✔
1485
    }
1486

1487
    public function setGroupSettings($groupid, $settings)
1488
    {
1489
        $this->clearMembershipCache();
6✔
1490
        $sql = "UPDATE memberships SET settings = ? WHERE userid = ? AND groupid = ?;";
6✔
1491
        return ($this->dbhm->preExec($sql, [
6✔
1492
            json_encode($settings),
6✔
1493
            $this->id,
6✔
1494
            $groupid
6✔
1495
        ]));
6✔
1496
    }
1497

1498
    public function activeModForGroup($groupid, $mysettings = NULL)
1499
    {
1500
        $mysettings = $mysettings ? $mysettings : $this->getGroupSettings($groupid);
36✔
1501

1502
        # If we have the active flag use that; otherwise assume that the legacy showmessages flag tells us.  Default
1503
        # to active.
1504
        # TODO Retire showmessages entirely and remove from user configs.
1505
        $active = array_key_exists('active', $mysettings) ? $mysettings['active'] : (!array_key_exists('showmessages', $mysettings) || $mysettings['showmessages']);
36✔
1506
        return ($active);
36✔
1507
    }
1508

1509
    public function widerReview() {
1510
        # Check if we are participating in wider chat review, i.e. we are a mod on a group with that setting.
1511
        $modships = $this->getModeratorships();
20✔
1512
        $widerreview = FALSE;
20✔
1513

1514
        foreach ($modships as $mod) {
20✔
1515
            if ($this->activeModForGroup($mod)) {
18✔
1516
                $g = Group::get($this->dbhr, $this->dbhm, $mod);
18✔
1517
                if ($g->getSetting('widerchatreview', FALSE)) {
18✔
1518
                    $widerreview = TRUE;
1✔
1519
                    break;
1✔
1520
                }
1521
            }
1522
        }
1523

1524
        return $widerreview;
20✔
1525
    }
1526

1527
    public function getGroupSettings($groupid, $configid = NULL, $id = NULL)
1528
    {
1529
        $id = $id ? $id : $this->id;
156✔
1530

1531
        # We have some parameters which may give us some info which saves queries
1532
        $this->cacheMemberships($id);
156✔
1533

1534
        # Defaults match memberships ones in Group.php.
1535
        $defaults = [
156✔
1536
            'active' => 1,
156✔
1537
            'showchat' => 1,
156✔
1538
            'pushnotify' => 1,
156✔
1539
            'eventsallowed' => 1,
156✔
1540
            'volunteeringallowed' => 1
156✔
1541
        ];
156✔
1542

1543
        $settings = $defaults;
156✔
1544

1545
        if (Utils::pres($groupid, $this->memberships)) {
156✔
1546
            $set = $this->memberships[$groupid];
156✔
1547

1548
            if ($set['settings']) {
156✔
1549
                $settings = json_decode($set['settings'], TRUE);
5✔
1550

1551
                if (!$configid && ($set['role'] == User::ROLE_OWNER || $set['role'] == User::ROLE_MODERATOR)) {
5✔
1552
                    $c = new ModConfig($this->dbhr, $this->dbhm);
5✔
1553

1554
                    # We might have an explicit configid - if so, use it to save on DB calls.
1555
                    $settings['configid'] = $set['configid'] ? $set['configid'] : $c->getForGroup($this->id, $groupid);
5✔
1556
                }
1557
            }
1558

1559
            # Base active setting on legacy showmessages setting if not present.
1560
            $settings['active'] = array_key_exists('active', $settings) ? $settings['active'] : (!array_key_exists('showmessages', $settings) || $settings['showmessages']);
156✔
1561
            $settings['active'] = $settings['active'] ? 1 : 0;
156✔
1562

1563
            foreach ($defaults as $key => $val) {
156✔
1564
                if (!array_key_exists($key, $settings)) {
156✔
1565
                    $settings[$key] = $val;
5✔
1566
                }
1567
            }
1568

1569
            $settings['emailfrequency'] = $set['emailfrequency'];
156✔
1570
            $settings['eventsallowed'] = $set['eventsallowed'];
156✔
1571
            $settings['volunteeringallowed'] = $set['volunteeringallowed'];
156✔
1572
        }
1573

1574
        return ($settings);
156✔
1575
    }
1576

1577
    public function setRole($role, $groupid)
1578
    {
1579
        $rc = TRUE;
46✔
1580
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
46✔
1581

1582
        Session::clearSessionCache();
46✔
1583

1584
        $currentRole = $this->getRoleForGroup($groupid, FALSE);
46✔
1585

1586
        if ($currentRole != $role) {
46✔
1587
            $l = new Log($this->dbhr, $this->dbhm);
46✔
1588
            $l->log([
46✔
1589
                        'type' => Log::TYPE_USER,
46✔
1590
                        'byuser' => $me ? $me->getId() : NULL,
46✔
1591
                        'subtype' => Log::SUBTYPE_ROLE_CHANGE,
46✔
1592
                        'groupid' => $groupid,
46✔
1593
                        'user' => $this->id,
46✔
1594
                        'text' => $role
46✔
1595
                    ]);
46✔
1596

1597
            $this->clearMembershipCache();
46✔
1598
            $sql = "UPDATE memberships SET role = ? WHERE userid = ? AND groupid = ?;";
46✔
1599
            $rc = $this->dbhm->preExec($sql, [
46✔
1600
                $role,
46✔
1601
                $this->id,
46✔
1602
                $groupid
46✔
1603
            ]);
46✔
1604

1605
            # We might need to update the systemrole.
1606
            #
1607
            # Not the end of the world if this fails.
1608
            $this->updateSystemRole($role);
46✔
1609

1610
            if ($currentRole == User::ROLE_MEMBER) {
46✔
1611
                # We have promoted this member.  We want to ensure that they have no unread old chats.
1612
                $r = new ChatRoom($this->dbhr, $this->dbhm);
44✔
1613
                $r->upToDateAll($this->getId(),[
44✔
1614
                    ChatRoom::TYPE_USER2MOD
44✔
1615
                ]);
44✔
1616
            } else if (($currentRole == User::ROLE_MODERATOR || $currentRole == User::ROLE_OWNER) && $role == User::ROLE_MEMBER) {
16✔
1617
                # This member has been demoted.  Mail the other mods.
1618
                $g = Group::get($this->dbhr, $this->dbhm, $groupid);
15✔
1619
                $g->notifyAboutSignificantEvent($this->getName() . " is no longer a moderator on " . $g->getPrivate('nameshort'),
15✔
1620
                    "If this is unexpected, please contact them and/or check that there are enough volunteers on this group.  If you need help, please contact ". MENTORS_ADDR . "."
15✔
1621
                );
15✔
1622
            }
1623

1624
            $this->memberships = NULL;
46✔
1625
        }
1626

1627
        return ($rc);
46✔
1628
    }
1629

1630
    public function getActiveCounts() {
1631
        $users = [
1✔
1632
            $this->id => [
1✔
1633
                'id' => $this->id
1✔
1634
            ]];
1✔
1635

1636
        $this->getActiveCountss($users);
1✔
1637
        return($users[$this->id]['activecounts']);
1✔
1638
    }
1639

1640
    public function getActiveCountss(&$users) {
1641
        $start = date('Y-m-d', strtotime(User::OPEN_AGE . " days ago"));
18✔
1642
        $uids = array_filter(array_column($users, 'id'));
18✔
1643

1644
        if (count($uids)) {
18✔
1645
            $counts = $this->dbhr->preQuery("SELECT messages.fromuser AS userid, COUNT(*) AS count, messages.type, messages_outcomes.outcome FROM messages LEFT JOIN messages_outcomes ON messages_outcomes.msgid = messages.id INNER JOIN messages_groups ON messages_groups.msgid = messages.id WHERE fromuser IN (" . implode(',', $uids) . ") AND messages_groups.arrival > ? AND collection = ? AND messages_groups.deleted = 0 AND messages_outcomes.id IS NULL GROUP BY messages.fromuser, messages.type, messages_outcomes.outcome;", [
17✔
1646
                $start,
17✔
1647
                MessageCollection::APPROVED
17✔
1648
            ]);
17✔
1649

1650
            foreach ($users as $user) {
17✔
1651
                $offers = 0;
17✔
1652
                $wanteds = 0;
17✔
1653

1654
                foreach ($counts as $count) {
17✔
1655
                    if ($count['userid'] == $user['id']) {
1✔
1656
                        if ($count['type'] == Message::TYPE_OFFER) {
1✔
1657
                            $offers += $count['count'];
1✔
1658
                        } else if ($count['type'] == Message::TYPE_WANTED) {
1✔
1659
                            $wanteds += $count['count'];
1✔
1660
                        }
1661
                    }
1662
                }
1663

1664
                $users[$user['id']]['activecounts'] = [
17✔
1665
                    'offers' => $offers,
17✔
1666
                    'wanteds' => $wanteds
17✔
1667
                ];
17✔
1668
            }
1669
        }
1670
    }
1671

1672
    public function getInfos(&$users, $grace = ChatRoom::REPLY_GRACE) {
1673
        $uids = array_filter(array_column($users, 'id'));
124✔
1674

1675
        $start = date('Y-m-d', strtotime(User::OPEN_AGE . " days ago"));
124✔
1676
        $days90 = date("Y-m-d", strtotime("90 days ago"));
124✔
1677
        $userq = "userid IN (" . implode(',', $uids) . ")";
124✔
1678

1679
        foreach ($uids as $uid) {
124✔
1680
            $users[$uid]['info']['replies'] = 0;
124✔
1681
            $users[$uid]['info']['taken'] = 0;
124✔
1682
            $users[$uid]['info']['reneged'] = 0;
124✔
1683
            $users[$uid]['info']['collected'] = 0;
124✔
1684
            $users[$uid]['info']['openage'] = User::OPEN_AGE;
124✔
1685
        }
1686

1687
        // We can combine some queries into a single one.  This is better for performance because it saves on
1688
        // the round trip (seriously, I've measured it, and it's worth doing).
1689
        //
1690
        // No need to check on the chat room type as we can only get messages of type Interested in a User2User chat.
1691
        $tq = Session::modtools() ? ", t6.*, t7.*" : '';
124✔
1692
        $sql = "SELECT t0.id AS theuserid, t0.lastaccess AS lastaccess, t1.*, t3.*, t4.*, t5.* $tq FROM
124✔
1693
(SELECT id, lastaccess FROM users WHERE id in (" . implode(',', $uids) . ")) t0 LEFT JOIN                                                                
124✔
1694
(SELECT COUNT(DISTINCT refmsgid) AS replycount, userid FROM chat_messages WHERE $userq AND date > ? AND refmsgid IS NOT NULL AND type = ?) t1 ON t1.userid = t0.id LEFT JOIN";
124✔
1695

1696
        if (Session::modtools()) {
124✔
1697
            $sql .= "(SELECT COUNT(DISTINCT refmsgid) AS replycountoffer, userid FROM chat_messages INNER JOIN messages ON messages.id = chat_messages.refmsgid WHERE $userq AND chat_messages.date > '$start' AND refmsgid IS NOT NULL AND chat_messages.type = '" . ChatMessage::TYPE_INTERESTED . "' AND messages.type = '" . Message::TYPE_OFFER . "') t6 ON t6.userid = t0.id LEFT JOIN ";
122✔
1698
            $sql .= "(SELECT COUNT(DISTINCT refmsgid) AS replycountwanted, userid FROM chat_messages INNER JOIN messages ON messages.id = chat_messages.refmsgid WHERE $userq AND chat_messages.date > '$start' AND refmsgid IS NOT NULL AND chat_messages.type = '" . ChatMessage::TYPE_INTERESTED . "' AND messages.type = '" . Message::TYPE_WANTED . "') t7 ON t7.userid = t0.id LEFT JOIN ";
122✔
1699
        }
1700

1701
        $sql .= "(SELECT COUNT(DISTINCT(messages_reneged.msgid)) AS reneged, userid FROM messages_reneged WHERE $userq AND timestamp > ?) t3 ON t3.userid = t0.id LEFT JOIN
124✔
1702
(SELECT COUNT(DISTINCT messages_by.msgid) AS collected, messages_by.userid FROM messages_by INNER JOIN messages ON messages.id = messages_by.msgid INNER JOIN chat_messages ON chat_messages.refmsgid = messages.id AND messages.type = ? AND chat_messages.type = ? INNER JOIN messages_groups ON messages_groups.msgid = messages.id WHERE chat_messages.$userq AND messages_by.$userq AND messages_by.userid != messages.fromuser AND messages_groups.arrival >= '$days90') t4 ON t4.userid = t0.id LEFT JOIN
124✔
1703
(SELECT timestamp AS abouttime, text AS abouttext, userid FROM users_aboutme WHERE $userq ORDER BY timestamp DESC LIMIT 1) t5 ON t5.userid = t0.id
124✔
1704
;";
124✔
1705
        $counts = $this->dbhr->preQuery($sql, [
124✔
1706
            $start,
124✔
1707
            ChatMessage::TYPE_INTERESTED,
124✔
1708
            $start,
124✔
1709
            Message::TYPE_OFFER,
124✔
1710
            ChatMessage::TYPE_INTERESTED
124✔
1711
        ]);
124✔
1712

1713
        foreach ($users as $uid => $user) {
124✔
1714
            foreach ($counts as $count) {
124✔
1715
                if ($count['theuserid'] == $users[$uid]['id']) {
124✔
1716
                    $users[$uid]['info']['replies'] = $count['replycount'] ? $count['replycount'] : 0;
124✔
1717

1718
                    if (Session::modtools()) {
124✔
1719
                        $users[$uid]['info']['repliesoffer'] = $count['replycountoffer'] ? $count['replycountoffer'] : 0;
122✔
1720
                        $users[$uid]['info']['replieswanted'] = $count['replycountwanted'] ? $count['replycountwanted'] : 0;
122✔
1721
                    }
1722

1723
                    $users[$uid]['info']['reneged'] = $count['reneged'] ? $count['reneged'] : 0;
124✔
1724
                    $users[$uid]['info']['collected'] = $count['collected'] ? $count['collected'] : 0;
124✔
1725
                    $users[$uid]['info']['lastaccess'] = $count['lastaccess'] ? Utils::ISODate($count['lastaccess']) : NULL;
124✔
1726
                    $users[$uid]['info']['count'] = $count;
124✔
1727

1728
                    if (Utils::pres('abouttime', $count)) {
124✔
1729
                        $users[$uid]['info']['aboutme'] = [
2✔
1730
                            'timestamp' => Utils::ISODate($count['abouttime']),
2✔
1731
                            'text' => $count['abouttext']
2✔
1732
                        ];
2✔
1733
                    }
1734
                }
1735
            }
1736
        }
1737

1738
        $sql = "SELECT messages.fromuser AS userid, COUNT(*) AS count, messages.type, messages_outcomes.outcome FROM messages LEFT JOIN messages_outcomes ON messages_outcomes.msgid = messages.id INNER JOIN messages_groups ON messages_groups.msgid = messages.id WHERE fromuser IN (" . implode(',', $uids) . ") AND messages.arrival > ? AND collection = ? AND messages_groups.deleted = 0 GROUP BY messages.fromuser, messages.type, messages_outcomes.outcome;";
124✔
1739
        $counts = $this->dbhr->preQuery($sql, [
124✔
1740
            $start,
124✔
1741
            MessageCollection::APPROVED
124✔
1742
        ]);
124✔
1743

1744
        foreach ($users as $uid => $user) {
124✔
1745
            $users[$uid]['info']['offers'] = 0;
124✔
1746
            $users[$uid]['info']['wanteds'] = 0;
124✔
1747
            $users[$uid]['info']['openoffers'] = 0;
124✔
1748
            $users[$uid]['info']['openwanteds'] = 0;
124✔
1749
            $users[$uid]['info']['expectedreply'] = 0;
124✔
1750

1751
            foreach ($counts as $count) {
124✔
1752
                if ($count['userid'] == $users[$uid]['id']) {
57✔
1753
                    if ($count['type'] == Message::TYPE_OFFER) {
57✔
1754
                        $users[$uid]['info']['offers'] += $count['count'];
41✔
1755

1756
                        if (!Utils::pres('outcome', $count)) {
41✔
1757
                            $users[$uid]['info']['openoffers'] += $count['count'];
41✔
1758
                        }
1759
                    } else if ($count['type'] == Message::TYPE_WANTED) {
18✔
1760
                        $users[$uid]['info']['wanteds'] += $count['count'];
7✔
1761

1762
                        if (!Utils::pres('outcome', $count)) {
7✔
1763
                            $users[$uid]['info']['openwanteds'] += $count['count'];
3✔
1764
                        }
1765
                    }
1766
                }
1767
            }
1768
        }
1769

1770
        # Distance away.
1771
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
124✔
1772

1773
        if ($me) {
124✔
1774
            list ($mylat, $mylng, $myloc) = $me->getLatLng();
84✔
1775

1776
            if (!is_null($myloc)) {
84✔
1777
                $latlngs = $this->getLatLngs($users, FALSE, TRUE);
14✔
1778

1779
                foreach ($latlngs as $userid => $latlng) {
14✔
1780
                    if ($latlng) {
14✔
1781
                        $users[$userid]['info']['milesaway'] = $this->getDistanceBetween($mylat, $mylng, $latlng['lat'], $latlng['lng']);
13✔
1782
                    }
1783
                }
1784
            }
1785

1786
            $this->getPublicLocations($users);
84✔
1787
        }
1788

1789
        $r = new ChatRoom($this->dbhr, $this->dbhm);
124✔
1790
        $replytimes = $r->replyTimes($uids);
124✔
1791

1792
        foreach ($replytimes as $uid => $replytime) {
124✔
1793
            $users[$uid]['info']['replytime'] = $replytime;
124✔
1794
        }
1795

1796
        $nudges = $r->nudgeCounts($uids);
124✔
1797

1798
        foreach ($nudges as $uid => $nudgecount) {
124✔
1799
            $users[$uid]['info']['nudges'] = $nudgecount;
124✔
1800
        }
1801

1802
        $ratings = $this->getRatings($uids);
124✔
1803

1804
        foreach ($ratings as $uid => $rating) {
124✔
1805
            $users[$uid]['info']['ratings'] = $rating;
124✔
1806
        }
1807

1808
        $replies = $this->getExpectedReplies($uids, ChatRoom::ACTIVELIM, $grace);
124✔
1809

1810
        foreach ($replies as $reply) {
124✔
1811
            if ($reply['expectee']) {
124✔
1812
                $users[$reply['expectee']]['info']['expectedreply'] = $reply['count'];
1✔
1813
            }
1814
        }
1815
    }
1816
    
1817
    public function getInfo($grace = ChatRoom::REPLY_GRACE)
1818
    {
1819
        $users = [
14✔
1820
            $this->id => [
14✔
1821
                'id' => $this->id
14✔
1822
            ]
14✔
1823
        ];
14✔
1824

1825
        $this->getInfos($users, $grace);
14✔
1826

1827
        return ($users[$this->id]['info']);
14✔
1828
    }
1829

1830
    public function getAboutMe() {
1831
        $ret = NULL;
48✔
1832

1833
        $aboutmes = $this->dbhr->preQuery("SELECT * FROM users_aboutme WHERE userid = ? ORDER BY timestamp DESC LIMIT 1;", [
48✔
1834
            $this->id
48✔
1835
        ]);
48✔
1836

1837
        foreach ($aboutmes as $aboutme) {
48✔
1838
            $ret = [
1✔
1839
                'timestamp' => Utils::ISODate($aboutme['timestamp']),
1✔
1840
                'text' => $aboutme['text']
1✔
1841
            ];
1✔
1842
        }
1843

1844
        return($ret);
48✔
1845
    }
1846

1847
    private function md5_hex_to_dec($hex_str)
1848
    {
1849
        $arr = str_split($hex_str, 4);
20✔
1850
        foreach ($arr as $grp) {
20✔
1851
            $dec[] = str_pad(hexdec($grp), 5, '0', STR_PAD_LEFT);
20✔
1852
        }
1853
        return floatval("0." . implode('', $dec));
20✔
1854
    }
1855

1856
    public function getDistance($mylat, $mylng) {
1857
        list ($tlat, $tlng, $tloc) = $this->getLatLng();
1✔
1858
        #error_log("Get distance $mylat, $mylng, $tlat, $tlng = " . $this->getDistanceBetween($mylat, $mylng, $tlat, $tlng));
1859
        return($this->getDistanceBetween($mylat, $mylng, $tlat, $tlng));
1✔
1860
    }
1861

1862
    public function getDistanceBetween($mylat, $mylng, $tlat, $tlng)
1863
    {
1864
        $p1 = new POI($mylat, $mylng);
20✔
1865

1866
        # We need to make sure that we don't reveal the actual location (well, the postcode location) to
1867
        # someone attempting to triangulate.  So first we move the location a bit based on something which
1868
        # can't be known about a user - a hash of their ID and the password salt.
1869
        $tlat += ($this->md5_hex_to_dec(md5(PASSWORD_SALT . $this->id)) - 0.5) / 100;
20✔
1870
        $tlng += ($this->md5_hex_to_dec(md5($this->id . PASSWORD_SALT)) - 0.5) / 100;
20✔
1871

1872
        # Now randomise the distance a bit each time we get it, so that anyone attempting repeated measurements
1873
        # will get conflicting results around the precise location that isn't actually theirs.  But still close
1874
        # enough to be useful for our purposes.
1875
        $tlat += mt_rand(-100, 100) / 20000;
20✔
1876
        $tlng += mt_rand(-100, 100) / 20000;
20✔
1877

1878
        $p2 = new POI($tlat, $tlng);
20✔
1879
        $metres = $p1->getDistanceInMetersTo($p2);
20✔
1880
        $miles = $metres / 1609.344;
20✔
1881
        $miles = $miles > 2 ? round($miles) : round($miles, 1);
20✔
1882
        return ($miles);
20✔
1883
    }
1884

1885
    public function gravatar($email, $s = 80, $d = 'mm', $r = 'g')
1886
    {
1887
        $url = 'https://www.gravatar.com/avatar/';
20✔
1888
        $url .= md5(strtolower(trim($email)));
20✔
1889
        $url .= "?s=$s&d=$d&r=$r";
20✔
1890
        return $url;
20✔
1891
    }
1892

1893
    public function getPublicLocation()
1894
    {
1895
        $users = [
30✔
1896
            $this->id => [
30✔
1897
                'id' => $this->id
30✔
1898
            ]
30✔
1899
        ];
30✔
1900

1901
        $this->getLatLngs($users);
30✔
1902
        $this->getPublicLocations($users);
30✔
1903

1904
        return($users[$this->id]['info']['publiclocation']);
30✔
1905
    }
1906

1907
    public function ensureAvatar(&$atts)
1908
    {
1909
        # This involves querying external sites, so we need to use it with care, otherwise we can hang our
1910
        # system.  It can also cause updates, so if we call it lots of times, it can result in cluster issues.
1911
        $forcedefault = FALSE;
19✔
1912
        $settings = Utils::presdef('settings', $atts, NULL);
19✔
1913

1914
        if ($settings) {
19✔
1915
            if (array_key_exists('useprofile', $settings) && !$settings['useprofile']) {
19✔
1916
                $forcedefault = TRUE;
1✔
1917
            }
1918
        }
1919

1920
        if (!$forcedefault && $atts['profile']['default']) {
19✔
1921
            # See if we can do better than a default.
1922
            $emails = $this->getEmails($atts['id']);
19✔
1923

1924
            try {
1925
                foreach ($emails as $email) {
19✔
1926
                    if (preg_match('/(.*)-g.*@user.trashnothing.com/', $email['email'], $matches)) {
8✔
1927
                        # TrashNothing has an API we can use.
1928
                        $url = "https://trashnothing.com/api/users/{$matches[1]}/profile-image?default=" . urlencode('https://' . IMAGE_DOMAIN . '/defaultprofile.png');
1✔
1929
                        $atts['profile'] = [
1✔
1930
                            'url' => $url,
1✔
1931
                            'turl' => $url,
1✔
1932
                            'default' => FALSE,
1✔
1933
                            'TN' => TRUE
1✔
1934
                        ];
1✔
1935

1936
                        break;
1✔
1937
                    } else if (!Mail::ourDomain($email['email'])) {
8✔
1938
                        # Try for gravatar
1939
                        $gurl = $this->gravatar($email['email'], 200, 404);
8✔
1940
                        $g = @file_get_contents($gurl);
8✔
1941

1942
                        if ($g) {
8✔
1943
                            $atts['profile'] = [
1✔
1944
                                'url' => $gurl,
1✔
1945
                                'turl' => $this->gravatar($email['email'], 100, 404),
1✔
1946
                                'default' => FALSE,
1✔
1947
                                'gravatar' => TRUE
1✔
1948
                            ];
1✔
1949

1950
                            break;
1✔
1951
                        }
1952
                    }
1953
                }
1954

1955
                if ($atts['profile']['default']) {
19✔
1956
                    # Try for Facebook.
1957
                    $logins = $this->getLogins(TRUE);
19✔
1958
                    foreach ($logins as $login) {
19✔
1959
                        if ($login['type'] == User::LOGIN_FACEBOOK) {
10✔
1960
                            if (Utils::presdef('useprofile', $atts['settings'], TRUE)) {
×
1961
                                // As of October 2020 we can no longer just access the profile picture via the UID, we need to make a
1962
                                // call to the Graph API to fetch it.
1963
                                $f = new Facebook($this->dbhr, $this->dbhm);
×
1964
                                $atts['profile'] = $f->getProfilePicture($login['uid']);
×
1965
                            }
1966
                        }
1967
                    }
1968
                }
1969
            } catch (Throwable $e) {}
×
1970

1971
            $hash = NULL;
19✔
1972

1973
            if (!Utils::pres('default', $atts['profile'])) {
19✔
1974
                # We think we have a profile.  Make sure we can fetch it and filter out other people's
1975
                # default images.  But skip this check for trusted external sources (TN, gravatar) since
1976
                # they may not be accessible in test environments and we trust them anyway.
1977
                if (!Utils::pres('TN', $atts['profile']) && !Utils::pres('gravatar', $atts['profile'])) {
1✔
1978
                    $atts['profile']['default'] = TRUE;
×
1979
                    $this->filterDefault($atts['profile'], $hash);
×
1980
                }
1981
            }
1982

1983
            if (Utils::pres('default', $atts['profile'])) {
19✔
1984
                # Nothing - so get gravatar to generate a default for us.
1985
                $atts['profile'] = [
19✔
1986
                    'url' => $this->gravatar($this->getEmailPreferred(), 200, 'identicon'),
19✔
1987
                    'turl' => $this->gravatar($this->getEmailPreferred(), 100, 'identicon'),
19✔
1988
                    'default' => FALSE,
19✔
1989
                    'gravatar' => TRUE
19✔
1990
                ];
19✔
1991
            }
1992

1993
            # Save for next time.
1994
            $this->dbhm->preExec("INSERT INTO users_images (userid, url, `default`, hash) VALUES (?, ?, ?, ?);", [
19✔
1995
                $atts['id'],
19✔
1996
                $atts['profile']['default'] ? NULL : $atts['profile']['url'],
19✔
1997
                $atts['profile']['default'],
19✔
1998
                $hash
19✔
1999
            ]);
19✔
2000
        }
2001
    }
2002

2003
    public function filterDefault(&$profile, &$hash) {
2004
        $hasher = new ImageHash;
×
2005
        $data = Utils::pres('url', $profile) && strlen($profile['url']) ? @file_get_contents($profile['url']) : NULL;
×
2006
        $hash = NULL;
×
2007

2008
        if ($data) {
×
2009
            $img = @imagecreatefromstring($data);
×
2010

2011
            if ($img) {
×
2012
                $hash = $hasher->hash($img);
×
2013
                $profile['default'] = FALSE;
×
2014
            }
2015
        }
2016

2017
        if ($hash == 'e070716060607120' || $hash == 'd0f0323171707030' || $hash == '13130f4e0e0e4e52' ||
×
2018
            $hash == '1f0fcf9f9f9fcfff' || $hash == '23230f0c0e0e0c24' || $hash == 'c0c0e070e0603100' ||
×
2019
            $hash == 'f0f0316870f07130' || $hash == '242e070e060b0d24' || $hash == '69aa49558e4da88e') {
×
2020
            # This is a default profile - replace it with ours.
2021
            $profile['url'] = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
2022
            $profile['turl'] = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
2023
            $profile['default'] = TRUE;
×
2024
            $hash = NULL;
×
2025
        }
2026
    }
2027

2028
    public function getPublic($groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [ MessageCollection::APPROVED ], $historyfull = FALSE)
2029
    {
2030
        $atts = [];
250✔
2031

2032
        if ($this->id) {
250✔
2033
            $users = [ $this->user ];
250✔
2034
            $rets = $this->getPublics($users, $groupids, $history, $comments, $memberof, $applied, $modmailsonly, $emailhistory, $msgcoll, $historyfull);
250✔
2035
            $atts = $rets[$this->id];
250✔
2036
        }
2037

2038
        return($atts);
250✔
2039
    }
2040

2041
    public function getPublicAtts(&$rets, $users, $me) {
2042
        foreach ($users as &$user) {
255✔
2043
            if (!array_key_exists($user['id'], $rets)) {
255✔
2044
                $rets[$user['id']] = [];
255✔
2045
            }
2046

2047
            $atts = $this->publicatts;
255✔
2048

2049
            if (Session::modtools()) {
255✔
2050
                # We have some extra attributes.
2051
                $atts[] = 'deleted';
237✔
2052
                $atts[] = 'lastaccess';
237✔
2053
                $atts[] = 'trustlevel';
237✔
2054
            }
2055

2056
            foreach ($atts as $att) {
255✔
2057
                $rets[$user['id']][$att] = Utils::presdef($att, $user, NULL);
255✔
2058
            }
2059

2060
            $rets[$user['id']]['settings'] = ['dummy' => TRUE];
255✔
2061

2062
            if (Utils::presdef('settings', $user, NULL)) {
255✔
2063
                # This is a bit of a type guddle.
2064
                if (gettype($user['settings']) == 'string') {
37✔
2065
                    $rets[$user['id']]['settings'] = json_decode($user['settings'], TRUE);
36✔
2066
                } else {
2067
                    $rets[$user['id']]['settings'] = $user['settings'];
1✔
2068
                }
2069
            }
2070

2071
            if (Utils::pres('mylocation', $rets[$user['id']]['settings']) && Utils::pres('groupsnear', $rets[$user['id']]['settings']['mylocation'])) {
255✔
2072
                # This is large - no need for it.
2073
                $rets[$user['id']]['settings']['mylocation']['groupsnear'] = NULL;
×
2074
            }
2075

2076
            $rets[$user['id']]['settings']['notificationmails'] = array_key_exists('notificationmails', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['notificationmails'] : TRUE;
255✔
2077
            $rets[$user['id']]['settings']['engagement'] = array_key_exists('engagement', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['engagement'] : TRUE;
255✔
2078
            $rets[$user['id']]['settings']['modnotifs'] = array_key_exists('modnotifs', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['modnotifs'] : 4;
255✔
2079
            $rets[$user['id']]['settings']['backupmodnotifs'] = array_key_exists('backupmodnotifs', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['backupmodnotifs'] : 12;
255✔
2080

2081
            $rets[$user['id']]['displayname'] = $this->getName(TRUE, $user);
255✔
2082

2083
            $rets[$user['id']]['added'] = array_key_exists('added', $user) ? Utils::ISODate($user['added']) : NULL;
255✔
2084

2085
            foreach (['fullname', 'firstname', 'lastname'] as $att) {
255✔
2086
                # Make sure we don't return an email if somehow one has snuck in.
2087
                if (isset($rets[$user['id']][$att]) && $rets[$user['id']][$att] !== NULL) {
255✔
2088
                    $value = $rets[$user['id']][$att];
255✔
2089
                    $rets[$user['id']][$att] = $value && strpos($value, '@') !== FALSE ? substr($value, 0, strpos($value, '@')) : $value;
255✔
2090
                }
2091
            }
2092

2093
            if ($me && $rets[$user['id']]['id'] == $me->getId()) {
255✔
2094
                # Add in private attributes for our own entry.
2095
                $rets[$user['id']]['emails'] = $me->getEmails();
143✔
2096
                $rets[$user['id']]['email'] = $me->getEmailPreferred();
143✔
2097
                $rets[$user['id']]['relevantallowed'] = $me->getPrivate('relevantallowed');
143✔
2098
                $rets[$user['id']]['permissions'] = $me->getPrivate('permissions');
143✔
2099
            }
2100

2101
            if ($me && ($me->isModerator() || $user['id'] == $me->getId())) {
255✔
2102
                # Mods can see email settings, no matter which group.
2103
                $rets[$user['id']]['onholidaytill'] = (Utils::pres('onholidaytill', $rets[$user['id']]) && (time() < strtotime($rets[$user['id']]['onholidaytill']))) ? Utils::ISODate($rets[$user['id']]['onholidaytill']) : NULL;
164✔
2104
            } else {
2105
                # Don't show some attributes unless they're a mod or ourselves.
2106
                $ismod = $rets[$user['id']]['systemrole'] == User::SYSTEMROLE_ADMIN ||
133✔
2107
                    $rets[$user['id']]['systemrole'] == User::SYSTEMROLE_SUPPORT ||
133✔
2108
                    $rets[$user['id']]['systemrole'] == User::SYSTEMROLE_MODERATOR;
133✔
2109
                $showmod = $ismod && Utils::presdef('showmod', $rets[$user['id']]['settings'], FALSE);
133✔
2110
                $rets[$user['id']]['settings']['showmod'] = $showmod;
133✔
2111
                $rets[$user['id']]['yahooid'] = NULL;
133✔
2112
            }
2113

2114
            if (Utils::pres('deleted', $rets[$user['id']])) {
255✔
2115
                $rets[$user['id']]['deleted'] = Utils::ISODate($rets[$user['id']]['deleted']);
×
2116
            }
2117

2118
            if (Utils::pres('lastaccess', $rets[$user['id']])) {
255✔
2119
                $rets[$user['id']]['lastaccess'] = Utils::ISODate($rets[$user['id']]['lastaccess']);
237✔
2120
            }
2121
        }
2122
    }
2123
    
2124
    public function getPublicProfiles(&$rets, $users) {
2125
        $idsleft = [];
256✔
2126

2127
        foreach ($rets as $userid => $ret) {
256✔
2128
            if (Utils::pres($userid, $users)) {
256✔
2129
                if (Utils::pres('profile', $users[$userid])) {
8✔
2130
                    $rets[$userid]['profile'] = $users[$userid]['profile'];
1✔
2131
                } else {
2132
                    $idsleft[] = $userid;
8✔
2133
                }
2134
            } else {
2135
                $idsleft[] = $userid;
251✔
2136
            }
2137
        }
2138

2139
        if (count($idsleft)) {
256✔
2140
            foreach ($idsleft as $id) {
256✔
2141
                $rets[$id]['profile'] = [
256✔
2142
                    'url' => 'https://' . IMAGE_DOMAIN . '/defaultprofile.png',
256✔
2143
                    'turl' => 'https://' . IMAGE_DOMAIN . '/defaultprofile.png',
256✔
2144
                    'default' => TRUE
256✔
2145
                ];
256✔
2146
            }
2147

2148
            # Ordering by id ASC means we'll end up with the most recent value in our output.
2149
            $sql = "SELECT * FROM users_images WHERE userid IN (" . implode(',', $idsleft) . ") ORDER BY userid, id ASC;";
256✔
2150

2151
            $profiles = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
256✔
2152

2153
            foreach ($profiles as $profile) {
256✔
2154
                # Get a profile.  This function is called so frequently that we can't afford to query external sites
2155
                # within it, so if we don't find one, we default to none.
2156
                if (Utils::pres('settings', $rets[$profile['userid']]) &&
12✔
2157
                    gettype($rets[$profile['userid']]['settings']) == 'array' &&
12✔
2158
                    (!array_key_exists('useprofile', $rets[$profile['userid']]['settings']) || $rets[$profile['userid']]['settings']['useprofile'])) {
12✔
2159
                    # We found a profile that we can use.
2160
                    if (!$profile['default']) {
12✔
2161
                        # If it's a gravatar image we can return a thumbnail url that specifies a different size.
2162
                        $turl = Utils::pres('url', $profile) ? $profile['url'] : ('https://' . IMAGE_DOMAIN . "/tuimg_{$profile['id']}.jpg");
12✔
2163
                        $turl = strpos($turl, 'https://www.gravatar.com') === 0 ? str_replace('?s=200', '?s=100', $turl) : $turl;
12✔
2164
                        $rets[$profile['userid']]['profile'] = [
12✔
2165
                            'id' => $profile['id'],
12✔
2166
                            'url' => Utils::pres('url', $profile) ? $profile['url'] : ('https://' . IMAGE_DOMAIN . "/uimg_{$profile['id']}.jpg"),
12✔
2167
                            'turl' => $turl,
12✔
2168
                            'default' => FALSE
12✔
2169
                        ];
12✔
2170
                    }
2171
                }
2172
            }
2173
        }
2174
    }
2175

2176
    public function getPublicHistory($me, &$rets, $users, $historyfull, $systemrole, $msgcoll = [ MessageCollection::APPROVED ]) {
2177
        $idsleft = [];
117✔
2178

2179
        foreach ($rets as $userid => $ret) {
117✔
2180
            if (Utils::pres($userid, $users)) {
117✔
2181
                if (array_key_exists('messagehistory', $users[$userid])) {
6✔
2182
                    $rets[$userid]['messagehistory'] = $users[$userid]['messagehistory'];
1✔
2183
                    $rets[$userid]['modmails'] = $users[$userid]['modmails'];
1✔
2184
                } else {
2185
                    $idsleft[] = $userid;
6✔
2186
                }
2187
            } else {
2188
                $idsleft[] = $userid;
112✔
2189
            }
2190
        }
2191

2192
        if (count($idsleft)) {
117✔
2193
            foreach ($rets as &$atts) {
117✔
2194
                $atts['messagehistory'] = [];
117✔
2195
            }
2196

2197
            # Add in the message history - from any of the emails associated with this user.
2198
            #
2199
            # We want one entry in here for each repost, so we LEFT JOIN with the reposts table.
2200
            $sql = null;
117✔
2201

2202
            if (count($idsleft)) {
117✔
2203
                $collq = " AND messages_groups.collection IN ('" . implode("','", $msgcoll) . "') ";
117✔
2204
                $earliest = $historyfull ? '1970-01-01' : date('Y-m-d', strtotime("midnight 30 days ago"));
117✔
2205
                $delq = $historyfull ? '' : ' AND messages_groups.deleted = 0';
117✔
2206

2207
                $sql = "SELECT GREATEST(COALESCE(messages_postings.date, messages.arrival), COALESCE(messages_postings.date, messages.arrival)) AS postdate, (SELECT outcome FROM messages_outcomes WHERE messages_outcomes.msgid = messages.id ORDER BY timestamp DESC LIMIT 1) AS outcome, messages.fromuser, messages.id, messages.fromaddr, messages.arrival, messages.date, messages_groups.collection, messages_groups.deleted, messages_postings.date AS repostdate, messages_postings.repost, messages_postings.autorepost, messages.subject, messages.type, DATEDIFF(NOW(), messages.date) AS daysago, messages_groups.groupid FROM messages INNER JOIN messages_groups ON messages.id = messages_groups.msgid $collq AND fromuser IN (" . implode(
117✔
2208
                        ',',
117✔
2209
                        $idsleft
117✔
2210
                    ) . ") $delq LEFT JOIN messages_postings ON messages.id = messages_postings.msgid HAVING postdate > ? ORDER BY postdate DESC;";
117✔
2211
            }
2212

2213
            if ($sql) {
117✔
2214
                $histories = $this->dbhr->preQuery(
117✔
2215
                    $sql,
117✔
2216
                    [
117✔
2217
                        $earliest
117✔
2218
                    ]
117✔
2219
                );
117✔
2220

2221
                foreach ($rets as $userid => $ret) {
117✔
2222
                    foreach ($histories as $history) {
117✔
2223
                        if ($history['fromuser'] == $ret['id']) {
34✔
2224
                            $history['arrival'] = Utils::pres('repostdate', $history) ? Utils::ISODate(
34✔
2225
                                $history['repostdate']
34✔
2226
                            ) : Utils::ISODate($history['arrival']);
34✔
2227
                            $history['date'] = Utils::ISODate($history['date']);
34✔
2228
                            $rets[$userid]['messagehistory'][] = $history;
34✔
2229
                        }
2230
                    }
2231
                }
2232
            }
2233

2234
            # Add in a count of recent "modmail" type logs which a mod might care about.
2235
            $modships = $me ? $me->getModeratorships() : [];
117✔
2236
            $modships = count($modships) == 0 ? [0] : $modships;
117✔
2237
            $sql = "SELECT COUNT(*) AS count, userid FROM `users_modmails` WHERE userid IN (" . implode(
117✔
2238
                    ',',
117✔
2239
                    $idsleft
117✔
2240
                ) . ") AND groupid IN (" . implode(',', $modships) . ") GROUP BY userid;";
117✔
2241
            $modmails = $this->dbhr->preQuery($sql, null, FALSE, FALSE);
117✔
2242

2243
            foreach ($idsleft as $userid) {
117✔
2244
                $rets[$userid]['modmails'] = 0;
117✔
2245
            }
2246

2247
            foreach ($rets as $userid => $ret) {
117✔
2248
                foreach ($modmails as $modmail) {
117✔
2249
                    if ($modmail['userid'] == $ret['id']) {
1✔
2250
                        $rets[$userid]['modmails'] = $modmail['count'] ? $modmail['count'] : 0;
1✔
2251
                    }
2252
                }
2253
            }
2254
        }
2255
    }
2256

2257
    public function getPublicMemberOf(&$rets, $me, $freeglemod, $memberof, $systemrole) {
2258
        $userids = [];
237✔
2259

2260
        foreach ($rets as $ret) {
237✔
2261
            $ret['activearea'] = NULL;
237✔
2262

2263
            if (!Utils::pres('memberof', $ret)) {
237✔
2264
                # We haven't provided the complete list already, e.g. because the user is suspect.
2265
                $userids[] = $ret['id'];
237✔
2266
            }
2267
        }
2268

2269
        if ($memberof &&
237✔
2270
            count($userids) &&
237✔
2271
            ($systemrole == User::ROLE_MODERATOR || $systemrole == User::SYSTEMROLE_ADMIN || $systemrole == User::SYSTEMROLE_SUPPORT)
237✔
2272
        ) {
2273
            # Gt the recent ones (which preserves some privacy for the user but allows us to spot abuse) and any which
2274
            # are on our groups.
2275
            $addmax = ($systemrole == User::SYSTEMROLE_ADMIN || $systemrole == User::SYSTEMROLE_SUPPORT) ? PHP_INT_MAX : 31;
76✔
2276
            $modids = array_merge([0], $me->getModeratorships());
76✔
2277
            $freegleq = $freeglemod ? " OR groups.type = 'Freegle' " : '';
76✔
2278
            $sql = "SELECT DISTINCT memberships.*, memberships.collection AS coll, groups.onhere, groups.nameshort, groups.namefull, groups.lat, groups.lng, groups.type FROM memberships INNER JOIN `groups` ON memberships.groupid = groups.id WHERE userid IN (" . implode(',', $userids) . ") AND (DATEDIFF(NOW(), memberships.added) <= $addmax OR memberships.groupid IN (" . implode(',', $modids) . ") $freegleq);";
76✔
2279
            $groups = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
76✔
2280
            #error_log("Get groups $sql, {$this->id}");
2281

2282
            foreach ($rets as &$ret) {
76✔
2283
                $ret['memberof'] = [];
76✔
2284
                $ourEmailId = NULL;
76✔
2285

2286
                if (Utils::pres('emails', $ret)) {
76✔
2287
                    foreach ($ret['emails'] as $email) {
62✔
2288
                        if (Mail::ourDomain($email['email'])) {
62✔
2289
                            $ourEmailId = $email['id'];
6✔
2290
                        }
2291
                    }
2292
                }
2293

2294
                foreach ($groups as $group) {
76✔
2295
                    if ($ret['id'] ==  $group['userid']) {
65✔
2296
                        $name = $group['namefull'] ? $group['namefull'] : $group['nameshort'];
65✔
2297
                        $added = Utils::ISODate(Utils::pres('yadded', $group) ? $group['yadded'] : $group['added']);
65✔
2298
                        $addedago = floor((time() - strtotime($added)) / 86400);
65✔
2299

2300
                        $ret['memberof'][] = [
65✔
2301
                            'id' => $group['groupid'],
65✔
2302
                            'membershipid' => $group['id'],
65✔
2303
                            'namedisplay' => $name,
65✔
2304
                            'nameshort' => $group['nameshort'],
65✔
2305
                            'added' => $added,
65✔
2306
                            'collection' => $group['coll'],
65✔
2307
                            'role' => $group['role'],
65✔
2308
                            'emailfrequency' => $group['emailfrequency'],
65✔
2309
                            'eventsallowed' => $group['eventsallowed'],
65✔
2310
                            'volunteeringallowed' => $group['volunteeringallowed'],
65✔
2311
                            'ourpostingstatus' => $group['ourPostingStatus'],
65✔
2312
                            'type' => $group['type'],
65✔
2313
                            'onhere' => $group['onhere'],
65✔
2314
                            'reviewrequestedat' => $group['reviewrequestedat'] ? Utils::ISODate($group['reviewrequestedat']) : NULL,
65✔
2315
                            'reviewreason' => $group['reviewreason'],
65✔
2316
                            'reviewedat' => $group['reviewedat'] ? Utils::ISODate($group['reviewedat']) : NULL,
65✔
2317
                            'heldby' => $group['heldby'],
65✔
2318
                        ];
65✔
2319

2320

2321
                        // Counts as active if recently joined.
2322
                        if ($group['lat'] && $group['lng'] && $addedago <= 31) {
65✔
2323
                            $box = Utils::presdef('activearea', $ret, NULL);
2✔
2324

2325
                            $ret['activearea'] = [
2✔
2326
                                'swlat' => is_null($box)? $group['lat'] : min($group['lat'], $box['swlat']),
2✔
2327
                                'swlng' => is_null($box)? $group['lng'] : min($group['lng'], $box['swlng']),
2✔
2328
                                'nelng' => is_null($box)? $group['lng'] : max($group['lng'], $box['nelng']),
2✔
2329
                                'nelat' => is_null($box)? $group['lat'] : max($group['lat'], $box['nelat'])
2✔
2330
                            ];
2✔
2331
                        }
2332
                    }
2333
                }
2334
            }
2335
        }
2336
    }
2337

2338
    public function getPublicApplied(&$rets, $users, $applied, $systemrole) {
2339
        if ($applied &&
237✔
2340
            $systemrole == User::ROLE_MODERATOR ||
179✔
2341
            $systemrole == User::SYSTEMROLE_ADMIN ||
204✔
2342
            $systemrole == User::SYSTEMROLE_SUPPORT
237✔
2343
        ) {
2344
            $idsleft = [];
77✔
2345

2346
            foreach ($rets as $userid => $ret) {
77✔
2347
                if (Utils::pres($userid, $users)) {
77✔
2348
                    if (array_key_exists('applied', $users[$userid])) {
1✔
2349
                        $rets[$userid]['applied'] = $users[$userid]['applied'];
1✔
2350
                        $rets[$userid]['activedistance'] = $users[$userid]['activedistance'];
1✔
2351
                    } else {
2352
                        $idsleft[] = $userid;
1✔
2353
                    }
2354
                } else {
2355
                    $idsleft[] = $userid;
77✔
2356
                }
2357
            }
2358

2359
            if (count($idsleft)) {
77✔
2360
                # As well as being a member of a group, they might have joined and left, or applied and been rejected.
2361
                # This is useful info for moderators.
2362
                $sql = "SELECT DISTINCT memberships_history.*, groups.nameshort, groups.namefull, groups.lat, groups.lng FROM memberships_history INNER JOIN `groups` ON memberships_history.groupid = groups.id WHERE userid IN (" . implode(
77✔
2363
                        ',',
77✔
2364
                        $idsleft
77✔
2365
                    ) . ") AND DATEDIFF(NOW(), added) <= 31 AND groups.publish = 1 AND groups.onmap = 1 ORDER BY added DESC;";
77✔
2366
                $membs = $this->dbhr->preQuery($sql);
77✔
2367

2368
                foreach ($rets as &$ret) {
77✔
2369
                    $ret['applied'] = [];
77✔
2370
                    $ret['activedistance'] = null;
77✔
2371

2372
                    foreach ($membs as $memb) {
77✔
2373
                        if ($ret['id'] == $memb['userid']) {
67✔
2374
                            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
67✔
2375
                            $memb['namedisplay'] = $name;
67✔
2376
                            $memb['added'] = Utils::ISODate($memb['added']);
67✔
2377
                            $memb['id'] = $memb['groupid'];
67✔
2378
                            unset($memb['groupid']);
67✔
2379

2380
                            if ($memb['lat'] && $memb['lng']) {
67✔
2381
                                $box = Utils::presdef('activearea', $ret, null);
2✔
2382

2383
                                $box = [
2✔
2384
                                    'swlat' => is_null($box)? $memb['lat'] : min($memb['lat'], $box['swlat']),
2✔
2385
                                    'swlng' => is_null($box)? $memb['lng'] : min($memb['lng'], $box['swlng']),
2✔
2386
                                    'nelng' => is_null($box)? $memb['lng'] : max($memb['lng'], $box['nelng']),
2✔
2387
                                    'nelat' => is_null($box)? $memb['lat'] : max($memb['lat'], $box['nelat'])
2✔
2388
                                ];
2✔
2389

2390
                                $ret['activearea'] = $box;
2✔
2391

2392
                                if ($box) {
2✔
2393
                                    $ret['activedistance'] = round(
2✔
2394
                                        Location::getDistance(
2✔
2395
                                            $box['swlat'],
2✔
2396
                                            $box['swlng'],
2✔
2397
                                            $box['nelat'],
2✔
2398
                                            $box['nelng']
2✔
2399
                                        )
2✔
2400
                                    );
2✔
2401
                                }
2402
                            }
2403

2404
                            $ret['applied'][] = $memb;
67✔
2405
                        }
2406
                    }
2407
                }
2408
            }
2409
        }
2410
    }
2411

2412
    public function getPublicSpammer(&$rets, $me, $systemrole) {
2413
        # We want to check for spammers.  If we have suitable rights then we can
2414
        # return detailed info; otherwise just that they are on the list.
2415
        #
2416
        # We don't do this for our own logged in user, otherwise we recurse to death.
2417
        $myid = $me ? $me->getId() : NULL;
237✔
2418
        $userids = array_filter(array_keys($rets), function($val) use ($myid) {
237✔
2419
            return($val != $myid);
237✔
2420
        });
237✔
2421

2422
        if (count($userids)) {
237✔
2423
            # Fetch the users.  There are so many users that there is no point trying to use the query cache.
2424
            $sql = "SELECT * FROM spam_users WHERE userid IN (" . implode(',', $userids) . ");";
156✔
2425

2426
            $users = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
156✔
2427

2428
            foreach ($rets as &$ret) {
156✔
2429
                foreach ($users as &$user) {
156✔
2430
                    if ($user['userid'] == $ret['id']) {
3✔
2431
                        if (Session::modtools() && ($systemrole == User::ROLE_MODERATOR ||
3✔
2432
                                $systemrole == User::SYSTEMROLE_ADMIN ||
3✔
2433
                                $systemrole == User::SYSTEMROLE_SUPPORT)) {
3✔
2434
                            $ret['spammer'] = [];
2✔
2435
                            foreach (['id', 'userid', 'byuserid', 'added', 'collection', 'reason'] as $att) {
2✔
2436
                                $ret['spammer'][$att]= $user[$att];
2✔
2437
                            }
2438

2439
                            $ret['spammer']['added'] = Utils::ISODate($ret['spammer']['added']);
2✔
2440
                        } else if ($user['collection'] == Spam::TYPE_SPAMMER) {
2✔
2441
                            # Only return to members that they are a spammer once approved.
2442
                            $ret['spammer'] = TRUE;
2✔
2443
                        }
2444
                    }
2445
                }
2446
            }
2447
        }
2448
    }
2449

2450
    public function getEmailHistory(&$rets) {
2451
        $userids = array_keys($rets);
3✔
2452

2453
        $emails = $this->dbhr->preQuery("SELECT * FROM logs_emails WHERE userid IN (" . implode(',', $userids) . ");", NULL, FALSE, FALSE);
3✔
2454

2455
        foreach ($rets as $retind => $ret) {
3✔
2456
            $rets[$retind]['emailhistory'] = [];
3✔
2457

2458
            foreach ($emails as $email) {
3✔
2459
                if ($rets[$retind]['id'] == $email['userid']) {
1✔
2460
                    $email['timestamp'] = Utils::ISODate($email['timestamp']);
1✔
2461
                    unset($email['userid']);
1✔
2462
                    $rets[$retind]['emailhistory'][] = $email;
1✔
2463
                }
2464
            }
2465
        }
2466
    }
2467

2468
    public function getPublicsById($uids, $groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [MessageCollection::APPROVED], $historyfull = FALSE) {
2469
        $rets = [];
135✔
2470

2471
        # We might have some of these in cache, especially ourselves.
2472
        $uidsleft = [];
135✔
2473

2474
        foreach ($uids as $uid) {
135✔
2475
            $u = User::get($this->dbhr, $this->dbhm, $uid, TRUE, TRUE);
132✔
2476

2477
            if ($u) {
132✔
2478
                $rets[$uid] = $u->getPublic($groupids, $history, $comments, $memberof, $applied, $modmailsonly, $emailhistory, $msgcoll, $historyfull);
126✔
2479
            } else {
2480
                $uidsleft[] = $uid;
9✔
2481
            }
2482
        }
2483

2484
        $uidsleft = array_filter($uidsleft);
135✔
2485

2486
        if (count($uidsleft)) {
135✔
2487
            $us = $this->dbhr->preQuery("SELECT * FROM users WHERE id IN (" . implode(',', $uidsleft) . ");", NULL, FALSE, FALSE);
8✔
2488
            $users = [];
8✔
2489
            foreach ($us as $u) {
8✔
2490
                $users[$u['id']] = $u;
7✔
2491
            }
2492

2493
            if (count($users)) {
8✔
2494
                $users = $this->getPublics($users, $groupids, $history, $comments, $memberof, $applied, $modmailsonly, $emailhistory, $msgcoll, $historyfull);
7✔
2495

2496
                foreach ($users as $user) {
7✔
2497
                    $rets[$user['id']] = $user;
7✔
2498
                }
2499
            }
2500
        }
2501

2502
        return($rets);
135✔
2503
    }
2504

2505
    public function isTN() {
2506
        return strpos($this->getEmailPreferred(), '@user.trashnothing.com') !== FALSE;
74✔
2507
    }
2508

2509
    public function isLJ() {
2510
        return $this->user['ljuserid'] !== NULL;
25✔
2511
    }
2512

2513
    public function getPublicEmails(&$rets) {
2514
        $userids = array_keys($rets);
97✔
2515
        $emails = $this->getEmailsById($userids);
97✔
2516

2517
        foreach ($rets as &$ret) {
97✔
2518
            if (Utils::pres($ret['id'], $emails)) {
96✔
2519
                $ret['emails'] = $emails[$ret['id']];
79✔
2520
            }
2521
        }
2522
    }
2523

2524
    public static function purgedUser($id) {
2525
        return [
1✔
2526
            'id' => $id,
1✔
2527
            'displayname' => 'Purged user #' . $id,
1✔
2528
            'systemrole' => User::SYSTEMROLE_USER
1✔
2529
        ];
1✔
2530
    }
2531

2532
    public function getPublicLogs($me, &$rets, $modmailsonly, &$ctx, $suppress = TRUE, $seeall = FALSE) {
2533
        # Add in the log entries we have for this user.  We exclude some logs of little interest to mods.
2534
        # - creation - either of ourselves or others during syncing.
2535
        # - deletion of users due to syncing
2536
        # Don't cache as there might be a lot, they're rarely used, and it can cause UT issues.
2537
        $myid = $me ? $me->getId() : NULL;
18✔
2538
        $uids = array_keys($rets);
18✔
2539
        $startq = $ctx ? (" AND id < " . intval($ctx['id']) . " ") : '';
18✔
2540
        $modships = $me ? $me->getModeratorships() : [];
18✔
2541
        $groupq = count($modships) ? (" AND groupid IN (" . implode(',', $modships) . ")") : '';
18✔
2542
        $modmailq = " AND ((type = 'Message' AND subtype IN ('Rejected', 'Deleted', 'Replied')) OR (type = 'User' AND subtype IN ('Mailed', 'Rejected', 'Deleted'))) $groupq";
18✔
2543
        $modq = $modmailsonly ? $modmailq : '';
18✔
2544
        $suppq = $suppress ? " AND NOT (type = 'User' AND subtype IN('Created', 'Merged', 'YahooConfirmed')) " : '';
18✔
2545
        $sql = "SELECT DISTINCT * FROM logs WHERE (user IN (" . implode(',', $uids) . ") OR byuser IN (" . implode(',', $uids) . ")) $startq $suppq $modq ORDER BY id DESC LIMIT 50;";
18✔
2546
        $logs = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
18✔
2547
        $groups = [];
18✔
2548
        $users = [];
18✔
2549
        $configs = [];
18✔
2550

2551
        # Get all log users in a single go.
2552
        $loguids = array_filter(array_merge(array_column($rets, 'user'), array_column($rets, 'byuser')));
18✔
2553

2554
        if (count($loguids)) {
18✔
2555
            $u = new User($this->dbhr, $this->dbhm);
×
2556
            $users = $u->getPublicsById($loguids, NULL, FALSE, FALSE, FALSE, FALSE);
×
2557
        }
2558

2559
        if (!$ctx) {
18✔
2560
            $ctx = ['id' => 0];
18✔
2561
        }
2562

2563
        foreach ($rets as $uid => $ret) {
18✔
2564
            $rets[$uid]['logs'] = [];
18✔
2565

2566
            foreach ($logs as $log) {
18✔
2567
                if ($log['user'] == $ret['id'] || $log['byuser'] == $ret['id']) {
17✔
2568
                    $ctx['id'] = $ctx['id'] == 0 ? $log['id'] : intval(min($ctx['id'], $log['id']));
17✔
2569

2570
                    if (Utils::pres('byuser', $log)) {
17✔
2571
                        if (!Utils::pres($log['byuser'], $users)) {
13✔
2572
                            $u = User::get($this->dbhr, $this->dbhm, $log['byuser']);
10✔
2573

2574
                            if ($u->getId() == $log['byuser']) {
10✔
2575
                                $users[$log['byuser']] = $u->getPublic(NULL, FALSE);
10✔
2576
                            } else {
2577
                                $users[$log['byuser']] = User::purgedUser($log['byuser']);
×
2578
                            }
2579
                        }
2580

2581
                        $log['byuser'] = $users[$log['byuser']];
13✔
2582
                    }
2583

2584
                    if (Utils::pres('user', $log)) {
17✔
2585
                        if (!Utils::pres($log['user'], $users)) {
16✔
2586
                            $u = User::get($this->dbhr, $this->dbhm, $log['user']);
14✔
2587

2588
                            if ($u->getId() == $log['user']) {
14✔
2589
                                $users[$log['user']] = $u->getPublic(NULL, FALSE);
14✔
2590
                            } else {
2591
                                $users[$log['user']] = User::purgedUser($log['user']);
×
2592
                            }
2593
                        }
2594

2595
                        $log['user'] = $users[$log['user']];
16✔
2596
                    }
2597

2598
                    if (Utils::pres('groupid', $log)) {
17✔
2599
                        if (!Utils::pres($log['groupid'], $groups)) {
13✔
2600
                            $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
13✔
2601

2602
                            if ($g->getId()) {
13✔
2603
                                $groups[$log['groupid']] = $g->getPublic();
13✔
2604
                                $groups[$log['groupid']]['myrole'] = $me ? $me->getRoleForGroup($log['groupid']) : User::ROLE_NONMEMBER;
13✔
2605
                            }
2606
                        }
2607

2608
                        # We can see logs for ourselves.
2609
                        if (!($myid != NULL && Utils::pres('user', $log) && Utils::presdef('id', $log['user'], NULL) == $myid) &&
13✔
2610
                            $g->getId() &&
13✔
2611
                            $groups[$log['groupid']]['myrole'] != User::ROLE_OWNER &&
13✔
2612
                            $groups[$log['groupid']]['myrole'] != User::ROLE_MODERATOR &&
13✔
2613
                            !$seeall
13✔
2614
                        ) {
2615
                            # We can only see logs for this group if we have a mod role, or if we have appropriate system
2616
                            # rights.  Skip this log.
2617
                            continue;
2✔
2618
                        }
2619

2620
                        $log['group'] = Utils::presdef($log['groupid'], $groups, NULL);
13✔
2621
                    }
2622

2623
                    if (Utils::pres('configid', $log)) {
17✔
2624
                        if (!Utils::pres($log['configid'], $configs)) {
2✔
2625
                            $c = new ModConfig($this->dbhr, $this->dbhm, $log['configid']);
2✔
2626

2627
                            if ($c->getId()) {
2✔
2628
                                $configs[$log['configid']] = $c->getPublic();
2✔
2629
                            }
2630
                        }
2631

2632
                        if (Utils::pres($log['configid'], $configs)) {
2✔
2633
                            $log['config'] = $configs[$log['configid']];
2✔
2634
                        }
2635
                    }
2636

2637
                    if (Utils::pres('stdmsgid', $log)) {
17✔
2638
                        $s = new StdMessage($this->dbhr, $this->dbhm, $log['stdmsgid']);
2✔
2639
                        $log['stdmsg'] = $s->getPublic();
2✔
2640
                    }
2641

2642
                    if (Utils::pres('msgid', $log)) {
17✔
2643
                        $m = new Message($this->dbhr, $this->dbhm, $log['msgid']);
8✔
2644

2645
                        if ($m->getID()) {
8✔
2646
                            $log['message'] = $m->getPublic(FALSE);
8✔
2647

2648
                            # If we're a mod (which we must be because we're accessing logs) we need to see the
2649
                            # envelopeto, because this is displayed in MT.  No real privacy issues in that.
2650
                            $log['message']['envelopeto'] = $m->getPrivate('envelopeto');
8✔
2651
                        } else {
2652
                            # The message has been deleted.
2653
                            $log['message'] = [
1✔
2654
                                'id' => $log['msgid'],
1✔
2655
                                'deleted' => TRUE
1✔
2656
                            ];
1✔
2657

2658
                            # See if we can find out why.
2659
                            $sql = "SELECT * FROM logs WHERE msgid = ? AND type = 'Message' AND subtype = 'Deleted' ORDER BY id DESC LIMIT 1;";
1✔
2660
                            $deletelogs = $this->dbhr->preQuery($sql, [$log['msgid']]);
1✔
2661
                            foreach ($deletelogs as $deletelog) {
1✔
2662
                                $log['message']['deletereason'] = $deletelog['text'];
1✔
2663
                            }
2664
                        }
2665

2666
                        # Prune large attributes.
2667
                        unset($log['message']['textbody']);
8✔
2668
                        unset($log['message']['htmlbody']);
8✔
2669
                        unset($log['message']['message']);
8✔
2670
                    }
2671

2672
                    $log['timestamp'] = Utils::ISODate($log['timestamp']);
17✔
2673

2674
                    $rets[$uid]['logs'][] = $log;
17✔
2675
                }
2676
            }
2677
        }
2678

2679
        # Get merge history
2680
        $merges = [];
18✔
2681
        do {
2682
            $added = FALSE;
18✔
2683
            $sql = "SELECT * FROM logs WHERE type = 'User' AND subtype = 'Merged' AND user IN (" . implode(',', $uids) . ");";
18✔
2684
            $logs = $this->dbhr->preQuery($sql);
18✔
2685
            foreach ($logs as $log) {
18✔
2686
                #error_log("Consider merge log {$log['text']}");
2687
                if (preg_match('/Merged (.*) into (.*?) \((.*)\)/', $log['text'], $matches)) {
1✔
2688
                    #error_log("Matched " . var_export($matches, TRUE));
2689
                    #error_log("Check ids {$matches[1]} and {$matches[2]}");
2690
                    foreach ([$matches[1], $matches[2]] as $id) {
1✔
2691
                        if (!in_array($id, $uids, TRUE)) {
1✔
2692
                            $added = TRUE;
1✔
2693
                            $uids[] = $id;
1✔
2694
                            $merges[] = ['timestamp' => Utils::ISODate($log['timestamp']), 'from' => $matches[1], 'to' => $matches[2], 'reason' => $matches[3]];
1✔
2695
                        }
2696
                    }
2697
                }
2698
            }
2699
        } while ($added);
18✔
2700

2701
        $merges = array_unique($merges, SORT_REGULAR);
18✔
2702

2703
        foreach ($rets as $uid => $ret) {
18✔
2704
            $rets[$uid]['merges'] = [];
18✔
2705

2706
            foreach ($merges as $merge) {
18✔
2707
                if ($merge['from'] == $ret['id'] || $merge['to'] == $ret['id']) {
1✔
2708
                    $rets[$uid]['merges'][] = $merge;
1✔
2709
                }
2710
            }
2711
        }
2712
    }
2713

2714
    public function getPublics($users, $groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [MessageCollection::APPROVED], $historyfull = FALSE)
2715
    {
2716
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
255✔
2717
        $systemrole = $me ? $me->getPrivate('systemrole') : User::SYSTEMROLE_USER;
255✔
2718
        $freeglemod = $me && $me->isFreegleMod();
255✔
2719

2720
        $rets = [];
255✔
2721

2722
        $this->getPublicAtts($rets, $users, $me);
255✔
2723
        $this->getPublicProfiles($rets, $users);
255✔
2724
        $this->getSupporters($rets, $users);
255✔
2725

2726
        if ($systemrole == User::ROLE_MODERATOR || $systemrole == User::SYSTEMROLE_ADMIN || $systemrole == User::SYSTEMROLE_SUPPORT) {
255✔
2727
            $this->getPublicEmails($rets);
96✔
2728
        }
2729

2730
        if ($history) {
255✔
2731
            $this->getPublicHistory($me, $rets, $users, $historyfull, $systemrole, $msgcoll);
117✔
2732
        }
2733

2734
        if (Session::modtools()) {
255✔
2735
            $this->getPublicMemberOf($rets, $me, $freeglemod, $memberof, $systemrole);
237✔
2736
            $this->getPublicApplied($rets, $users, $applied, $systemrole);
237✔
2737
            $this->getPublicSpammer($rets, $me, $systemrole);
237✔
2738

2739
            if ($comments) {
237✔
2740
                $this->getComments($me, $rets);
194✔
2741
            }
2742

2743
            if ($emailhistory) {
237✔
2744
                $this->getEmailHistory($rets);
3✔
2745
            }
2746
        }
2747

2748
        return ($rets);
255✔
2749
    }
2750

2751
    public function isAdmin()
2752
    {
2753
        return ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN);
12✔
2754
    }
2755

2756
    public function isAdminOrSupport()
2757
    {
2758
        $ret = ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN || $this->user['systemrole'] == User::SYSTEMROLE_SUPPORT);
96✔
2759

2760
        # Use array_key_exists so that old sessions which predate this fix can continue.  Prevents shock of the new.
2761
        if ($ret && array_key_exists('supportAllowed', $_SESSION) && !$_SESSION['supportAllowed']) {
96✔
2762
            // TODO Disabled for now.
2763
//            $ret = FALSE;
2764
        }
2765

2766
        return $ret;
96✔
2767
    }
2768

2769
    public function isModerator()
2770
    {
2771
        return ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN ||
271✔
2772
            $this->user['systemrole'] == User::SYSTEMROLE_SUPPORT ||
271✔
2773
            $this->user['systemrole'] == User::SYSTEMROLE_MODERATOR);
271✔
2774
    }
2775

2776
    public function systemRoleMax($role1, $role2)
2777
    {
2778
        $role = User::SYSTEMROLE_USER;
20✔
2779

2780
        if ($role1 == User::SYSTEMROLE_MODERATOR || $role2 == User::SYSTEMROLE_MODERATOR) {
20✔
2781
            $role = User::SYSTEMROLE_MODERATOR;
7✔
2782
        }
2783

2784
        if ($role1 == User::SYSTEMROLE_SUPPORT || $role2 == User::SYSTEMROLE_SUPPORT) {
20✔
2785
            $role = User::SYSTEMROLE_SUPPORT;
3✔
2786
        }
2787

2788
        if ($role1 == User::SYSTEMROLE_ADMIN || $role2 == User::SYSTEMROLE_ADMIN) {
20✔
2789
            $role = User::SYSTEMROLE_ADMIN;
2✔
2790
        }
2791

2792
        return ($role);
20✔
2793
    }
2794

2795
    public function roleMax($role1, $role2)
2796
    {
2797
        $role = User::ROLE_NONMEMBER;
20✔
2798

2799
        if ($role1 == User::ROLE_MEMBER || $role2 == User::ROLE_MEMBER) {
20✔
2800
            $role = User::ROLE_MEMBER;
17✔
2801
        }
2802

2803
        if ($role1 == User::ROLE_MODERATOR || $role2 == User::ROLE_MODERATOR) {
20✔
2804
            $role = User::ROLE_MODERATOR;
10✔
2805
        }
2806

2807
        if ($role1 == User::ROLE_OWNER || $role2 == User::ROLE_OWNER) {
20✔
2808
            $role = User::ROLE_OWNER;
3✔
2809
        }
2810

2811
        return ($role);
20✔
2812
    }
2813

2814
    public function roleMin($role1, $role2)
2815
    {
2816
        $role = User::ROLE_OWNER;
12✔
2817

2818
        if ($role1 == User::ROLE_MODERATOR || $role2 == User::ROLE_MODERATOR) {
12✔
2819
            $role = User::ROLE_MODERATOR;
8✔
2820
        }
2821

2822
        if ($role1 == User::ROLE_MEMBER || $role2 == User::ROLE_MEMBER) {
12✔
2823
            $role = User::ROLE_MEMBER;
9✔
2824
        }
2825

2826
        if ($role1 == User::ROLE_NONMEMBER || $role2 == User::ROLE_NONMEMBER) {
12✔
2827
            $role = User::ROLE_NONMEMBER;
3✔
2828
        }
2829

2830
        return ($role);
12✔
2831
    }
2832

2833
    public function merge($id1, $id2, $reason, $forcemerge = FALSE)
2834
    {
2835
        error_log("Merge $id2 into $id1, $reason");
16✔
2836

2837
        # We might not be able to merge them, if one or the other has the setting to prevent that.
2838
        $u1 = User::get($this->dbhr, $this->dbhm, $id1);
16✔
2839
        $u2 = User::get($this->dbhr, $this->dbhm, $id2);
16✔
2840
        $ret = FALSE;
16✔
2841

2842
        if ($id1 != $id2 && (($u1->canMerge() && $u2->canMerge()) || ($forcemerge))) {
16✔
2843
            #
2844
            # We want to merge two users.  At present we just merge the memberships, comments, emails and logs; we don't try to
2845
            # merge any conflicting settings.
2846
            #
2847
            # Both users might have membership of the same group, including at different levels.
2848
            #
2849
            # A useful query to find foreign key references is of this form:
2850
            #
2851
            # USE information_schema; SELECT * FROM KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'iznik' AND REFERENCED_TABLE_NAME = 'users';
2852
            #
2853
            # We avoid too much use of quoting in preQuery/preExec because quoted numbers can't use a numeric index and therefore
2854
            # perform slowly.
2855
            #error_log("Merge $id2 into $id1");
2856
            $l = new Log($this->dbhr, $this->dbhm);
14✔
2857
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
14✔
2858

2859
            $rc = $this->dbhm->beginTransaction();
14✔
2860
            $rollback = FALSE;
14✔
2861

2862
            if ($rc) {
14✔
2863
                try {
2864
                    #error_log("Started transaction");
2865
                    $rollback = TRUE;
14✔
2866

2867
                    # Merge the top-level memberships
2868
                    $id2membs = $this->dbhr->preQuery("SELECT * FROM memberships WHERE userid = $id2;");
14✔
2869
                    foreach ($id2membs as $id2memb) {
14✔
2870
                        $rc2 = $rc;
8✔
2871
                        # Jiggery-pokery with $rc for UT purposes.
2872
                        #error_log("$id2 member of {$id2memb['groupid']} ");
2873
                        $id1membs = $this->dbhr->preQuery("SELECT * FROM memberships WHERE userid = $id1 AND groupid = {$id2memb['groupid']};");
8✔
2874

2875
                        if (count($id1membs) == 0) {
8✔
2876
                            # id1 is not already a member.  Just change our id2 membership to id1.
2877
                            #error_log("...$id1 not a member, UPDATE");
2878
                            $rc2 = $this->dbhm->preExec("UPDATE IGNORE memberships SET userid = $id1 WHERE userid = $id2 AND groupid = {$id2memb['groupid']};");
3✔
2879

2880
                            #error_log("Membership UPDATE merge returned $rc2");
2881
                        } else {
2882
                            # id1 is already a member, so we really have to merge.
2883
                            #
2884
                            # Our new membership has the highest role.
2885
                            $id1memb = $id1membs[0];
7✔
2886
                            $role = User::roleMax($id1memb['role'], $id2memb['role']);
7✔
2887
                            #error_log("...as is $id1, roles {$id1memb['role']} vs {$id2memb['role']} => $role");
2888

2889
                            if ($role != $id1memb['role']) {
7✔
2890
                                $rc2 = $this->dbhm->preExec("UPDATE memberships SET role = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
2✔
2891
                                    $role
2✔
2892
                                ]);
2✔
2893
                                #error_log("Set role $rc2");
2894
                            }
2895

2896
                            if ($rc2) {
7✔
2897
                                #  Our added date should be the older of the two.
2898
                                $date = min(strtotime($id1memb['added']), strtotime($id2memb['added']));
7✔
2899
                                $mysqltime = date("Y-m-d H:i:s", $date);
7✔
2900
                                $rc2 = $this->dbhm->preExec("UPDATE memberships SET added = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
7✔
2901
                                    $mysqltime
7✔
2902
                                ]);
7✔
2903
                                #error_log("Added $rc2");
2904
                            }
2905

2906
                            # There are several attributes we want to take the non-NULL version.
2907
                            foreach (['configid', 'settings', 'heldby'] as $key) {
6✔
2908
                                #error_log("Check {$id2memb['groupid']} memb $id2 $key = " . Utils::presdef($key, $id2memb, NULL));
2909
                                if ($id2memb[$key]) {
6✔
2910
                                    if ($rc2) {
1✔
2911
                                        $rc2 = $this->dbhm->preExec("UPDATE memberships SET $key = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
1✔
2912
                                            $id2memb[$key]
1✔
2913
                                        ]);
1✔
2914
                                        #error_log("Set att $key = {$id2memb[$key]} $rc2");
2915
                                    }
2916
                                }
2917
                            }
2918
                        }
2919

2920
                        $rc = $rc2 && $rc ? $rc2 : 0;
7✔
2921
                    }
2922

2923
                    # Merge the emails.  Both might have a primary address; if so then id1 wins.
2924
                    # There is a unique index, so there can't be a conflict on email.
2925
                    if ($rc) {
13✔
2926
                        $primary = NULL;
13✔
2927
                        $foundprim = FALSE;
13✔
2928
                        $sql = "SELECT * FROM users_emails WHERE userid = $id2 AND preferred = 1;";
13✔
2929
                        $emails = $this->dbhr->preQuery($sql);
13✔
2930
                        foreach ($emails as $email) {
13✔
2931
                            $primary = $email['id'];
7✔
2932
                            $foundprim = TRUE;
7✔
2933
                        }
2934

2935
                        $sql = "SELECT * FROM users_emails WHERE userid = $id1 AND preferred = 1;";
13✔
2936
                        $emails = $this->dbhr->preQuery($sql);
13✔
2937
                        foreach ($emails as $email) {
13✔
2938
                            $primary = $email['id'];
11✔
2939
                            $foundprim = TRUE;
11✔
2940
                        }
2941

2942
                        if (!$foundprim) {
13✔
2943
                            # No primary.  Whatever we would choose for id1 should become the new one.
2944
                            $pemail = $u1->getEmailPreferred();
1✔
2945
                            $emails = $this->dbhr->preQuery("SELECT * FROM users_emails WHERE email LIKE ?;", [
1✔
2946
                                $pemail
1✔
2947
                            ]);
1✔
2948

2949
                            foreach ($emails as $email) {
1✔
2950
                                $primary = $email['id'];
1✔
2951
                            }
2952
                        }
2953

2954
                        #error_log("Merge emails");
2955
                        $sql = "UPDATE users_emails SET userid = $id1, preferred = 0 WHERE userid = $id2;";
13✔
2956
                        $rc = $this->dbhm->preExec($sql);
13✔
2957

2958
                        if ($primary) {
13✔
2959
                            $sql = "UPDATE users_emails SET preferred = 1 WHERE id = $primary;";
13✔
2960
                            $rc = $this->dbhm->preExec($sql);
13✔
2961
                        }
2962

2963
                        #error_log("Emails now " . var_export($this->dbhm->preQuery("SELECT * FROM users_emails WHERE userid = $id1;"), TRUE));
2964
                        #error_log("Email merge returned $rc");
2965
                    }
2966

2967
                    if ($rc) {
13✔
2968
                        # Merge other foreign keys where success is less important.  For some of these there might already
2969
                        # be entries, so we do an IGNORE.
2970
                        $this->dbhm->preExec("UPDATE locations_excluded SET userid = $id1 WHERE userid = $id2;");
13✔
2971
                        $this->dbhm->preExec("UPDATE IGNORE chat_roster SET userid = $id1 WHERE userid = $id2;");
13✔
2972
                        $this->dbhm->preExec("UPDATE IGNORE sessions SET userid = $id1 WHERE userid = $id2;");
13✔
2973
                        $this->dbhm->preExec("UPDATE IGNORE spam_users SET userid = $id1 WHERE userid = $id2;");
13✔
2974
                        $this->dbhm->preExec("UPDATE IGNORE spam_users SET byuserid = $id1 WHERE byuserid = $id2;");
13✔
2975
                        $this->dbhm->preExec("UPDATE IGNORE users_addresses SET userid = $id1 WHERE userid = $id2;");
13✔
2976
                        $this->dbhm->preExec("UPDATE users_comments SET userid = $id1 WHERE userid = $id2;");
13✔
2977
                        $this->dbhm->preExec("UPDATE users_comments SET byuserid = $id1 WHERE byuserid = $id2;");
13✔
2978
                        $this->dbhm->preExec("UPDATE IGNORE users_donations SET userid = $id1 WHERE userid = $id2;");
13✔
2979
                        $this->dbhm->preExec("UPDATE IGNORE users_images SET userid = $id1 WHERE userid = $id2;");
13✔
2980
                        $this->dbhm->preExec("UPDATE IGNORE users_invitations SET userid = $id1 WHERE userid = $id2;");
13✔
2981
                        $this->dbhm->preExec("UPDATE users_logins SET userid = $id1 WHERE userid = $id2;");
13✔
2982
                        $this->dbhm->preExec("UPDATE IGNORE users_logins SET uid = $id1 WHERE userid = $id1 AND `type` = ?;", [
13✔
2983
                            User::LOGIN_NATIVE
13✔
2984
                        ]);
13✔
2985
                        $this->dbhm->preExec("UPDATE IGNORE users_nearby SET userid = $id1 WHERE userid = $id2;");
13✔
2986
                        $this->dbhm->preExec("UPDATE IGNORE users_notifications SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
2987
                        $this->dbhm->preExec("UPDATE IGNORE users_notifications SET touser = $id1 WHERE touser = $id2;");
13✔
2988
                        $this->dbhm->preExec("UPDATE IGNORE users_nudges SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
2989
                        $this->dbhm->preExec("UPDATE IGNORE users_nudges SET touser = $id1 WHERE touser = $id2;");
13✔
2990
                        $this->dbhm->preExec("UPDATE IGNORE users_push_notifications SET userid = $id1 WHERE userid = $id2;");
13✔
2991
                        $this->dbhm->preExec("UPDATE IGNORE users_requests SET userid = $id1 WHERE userid = $id2;");
13✔
2992
                        $this->dbhm->preExec("UPDATE IGNORE users_requests SET completedby = $id1 WHERE completedby = $id2;");
13✔
2993
                        $this->dbhm->preExec("UPDATE IGNORE users_searches SET userid = $id1 WHERE userid = $id2;");
13✔
2994
                        $this->dbhm->preExec("UPDATE IGNORE newsfeed SET userid = $id1 WHERE userid = $id2;");
13✔
2995
                        $this->dbhm->preExec("UPDATE IGNORE messages_reneged SET userid = $id1 WHERE userid = $id2;");
13✔
2996
                        $this->dbhm->preExec("UPDATE IGNORE users_stories SET userid = $id1 WHERE userid = $id2;");
13✔
2997
                        $this->dbhm->preExec("UPDATE IGNORE users_stories_likes SET userid = $id1 WHERE userid = $id2;");
13✔
2998
                        $this->dbhm->preExec("UPDATE IGNORE users_stories_requested SET userid = $id1 WHERE userid = $id2;");
13✔
2999
                        $this->dbhm->preExec("UPDATE IGNORE users_thanks SET userid = $id1 WHERE userid = $id2;");
13✔
3000
                        $this->dbhm->preExec("UPDATE IGNORE modnotifs SET userid = $id1 WHERE userid = $id2;");
13✔
3001
                        $this->dbhm->preExec("UPDATE IGNORE teams_members SET userid = $id1 WHERE userid = $id2;");
13✔
3002
                        $this->dbhm->preExec("UPDATE IGNORE users_aboutme SET userid = $id1 WHERE userid = $id2;");
13✔
3003
                        $this->dbhm->preExec("UPDATE IGNORE ratings SET rater = $id1 WHERE rater = $id2;");
13✔
3004
                        $this->dbhm->preExec("UPDATE IGNORE ratings SET ratee = $id1 WHERE ratee = $id2;");
13✔
3005
                        $this->dbhm->preExec("UPDATE IGNORE users_replytime SET userid = $id1 WHERE userid = $id2;");
13✔
3006
                        $this->dbhm->preExec("UPDATE IGNORE messages_promises SET userid = $id1 WHERE userid = $id2;");
13✔
3007
                        $this->dbhm->preExec("UPDATE IGNORE messages_by SET userid = $id1 WHERE userid = $id2;");
13✔
3008
                        $this->dbhm->preExec("UPDATE IGNORE trysts SET user1 = $id1 WHERE user1 = $id2;");
13✔
3009
                        $this->dbhm->preExec("UPDATE IGNORE trysts SET user2 = $id1 WHERE user2 = $id2;");
13✔
3010
                        $this->dbhm->preExec("UPDATE IGNORE isochrones_users SET userid = $id1 WHERE userid = $id2;");
13✔
3011
                        $this->dbhm->preExec("UPDATE IGNORE microactions SET userid = $id1 WHERE userid = $id2;");
13✔
3012

3013
                        # Handle the bans.
3014
                        $this->dbhm->preExec("UPDATE IGNORE users_banned SET userid = $id1 WHERE userid = $id2;");
13✔
3015
                        $this->dbhm->preExec("UPDATE IGNORE users_banned SET byuser = $id1 WHERE byuser = $id2;");
13✔
3016

3017

3018
                        $bans = $this->dbhm->preQuery("SELECT * FROM users_banned WHERE userid = $id1");
13✔
3019
                        foreach ($bans as $ban) {
13✔
3020
                            # Make sure we are not a member; this could happen if one of the users is banned and
3021
                            # the other is not.
3022
                            $this->dbhm->preExec("DELETE FROM memberships WHERE userid = ? AND groupid = ?", [
1✔
3023
                                $id1,
1✔
3024
                                $ban['groupid']
1✔
3025
                            ]);
1✔
3026
                        }
3027

3028
                        # Merge chat rooms.  There might have be two separate rooms already, which means that we need
3029
                        # to make sure that messages from both end up in the same one.
3030
                        $rooms = $this->dbhr->preQuery("SELECT * FROM chat_rooms WHERE (user1 = $id2 OR user2 = $id2) AND chattype IN (?,?);", [
13✔
3031
                            ChatRoom::TYPE_USER2MOD,
13✔
3032
                            ChatRoom::TYPE_USER2USER
13✔
3033
                        ]);
13✔
3034

3035
                        foreach ($rooms as $room) {
13✔
3036
                            # Now see if there is already a chat room between the destination user and whatever this
3037
                            # one is.
3038
                            switch ($room['chattype']) {
1✔
3039
                                case ChatRoom::TYPE_USER2MOD;
1✔
3040
                                    $sql = "SELECT id FROM chat_rooms WHERE user1 = $id1 AND groupid = {$room['groupid']};";
1✔
3041
                                    break;
1✔
3042
                                case ChatRoom::TYPE_USER2USER;
1✔
3043
                                    $other = $room['user1'] == $id2 ? $room['user2'] : $room['user1'];
1✔
3044
                                    $sql = "SELECT id FROM chat_rooms WHERE (user1 = $id1 AND user2 = $other) OR (user2 = $id1 AND user1 = $other);";
1✔
3045
                                    break;
1✔
3046
                            }
3047

3048
                            $alreadys = $this->dbhr->preQuery($sql);
1✔
3049
                            #error_log("Check room {$room['id']} {$room['user1']} => {$room['user2']} $sql " . count($alreadys));
3050

3051
                            if (count($alreadys) > 0) {
1✔
3052
                                # Yes, there already is one.
3053
                                $this->dbhm->preExec("UPDATE chat_messages SET chatid = {$alreadys[0]['id']} WHERE chatid = {$room['id']}");
1✔
3054

3055
                                # Make sure latestmessage is set correctly.
3056
                                $this->dbhm->preExec("UPDATE chat_rooms SET latestmessage = GREATEST(latestmessage, ?) WHERE id = ?", [
1✔
3057
                                    $room['latestmessage'],
1✔
3058
                                    $alreadys[0]['id']
1✔
3059
                                ]);
1✔
3060
                            } else {
3061
                                # No, there isn't, so we can update our old one.
3062
                                $sql = $room['user1'] == $id2 ? "UPDATE chat_rooms SET user1 = $id1 WHERE id = {$room['id']};" : "UPDATE chat_rooms SET user2 = $id1 WHERE id = {$room['id']};";
1✔
3063
                                $this->dbhm->preExec($sql);
1✔
3064
                            }
3065
                        }
3066

3067
                        $this->dbhm->preExec("UPDATE chat_messages SET userid = $id1 WHERE userid = $id2;");
13✔
3068
                    }
3069

3070
                    # Merge attributes we want to keep if we have them in id2 but not id1.  Some will have unique
3071
                    # keys, so update to delete them.
3072
                    foreach (['fullname', 'firstname', 'lastname', 'yahooid'] as $att) {
13✔
3073
                        $users = $this->dbhm->preQuery("SELECT $att FROM users WHERE id = $id2;");
13✔
3074
                        foreach ($users as $user) {
13✔
3075
                            $this->dbhm->preExec("UPDATE users SET $att = NULL WHERE id = $id2;");
13✔
3076
                            User::clearCache($id1);
13✔
3077
                            User::clearCache($id2);
13✔
3078

3079
                            if (!$u1->getPrivate($att)) {
13✔
3080
                                if ($att != 'fullname') {
13✔
3081
                                    $this->dbhm->preExec("UPDATE users SET $att = ? WHERE id = $id1 AND $att IS NULL;", [$user[$att]]);
13✔
3082
                                } else if (stripos($user[$att], 'fbuser') === FALSE && stripos($user[$att], '-owner') === FALSE) {
2✔
3083
                                    # We don't want to overwrite a name with FBUser or a -owner address.
3084
                                    $this->dbhm->preExec("UPDATE users SET $att = ? WHERE id = $id1;", [$user[$att]]);
2✔
3085
                                }
3086
                            }
3087
                        }
3088
                    }
3089

3090
                    # Merge the logs.  There should be logs both about and by each user, so we can use the rc to check success.
3091
                    if ($rc) {
13✔
3092
                        $rc = $this->dbhm->preExec("UPDATE logs SET user = $id1 WHERE user = $id2;");
13✔
3093

3094
                        #error_log("Log merge 1 returned $rc");
3095
                    }
3096

3097
                    if ($rc) {
13✔
3098
                        $rc = $this->dbhm->preExec("UPDATE logs SET byuser = $id1 WHERE byuser = $id2;");
13✔
3099

3100
                        #error_log("Log merge 2 returned $rc");
3101
                    }
3102

3103
                    # Merge the fromuser in messages.  There might not be any, and it's not the end of the world
3104
                    # if this info isn't correct, so ignore the rc.
3105
                    #error_log("Merge messages, current rc $rc");
3106
                    if ($rc) {
13✔
3107
                        $this->dbhm->preExec("UPDATE messages SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
3108
                    }
3109

3110
                    # Merge the history
3111
                    #error_log("Merge history, current rc $rc");
3112
                    if ($rc) {
13✔
3113
                        $this->dbhm->preExec("UPDATE messages_history SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
3114
                        $this->dbhm->preExec("UPDATE memberships_history SET userid = $id1 WHERE userid = $id2;");
13✔
3115
                    }
3116

3117
                    # Merge the systemrole.
3118
                    $u1s = $this->dbhr->preQuery("SELECT systemrole FROM users WHERE id = $id1;");
13✔
3119
                    foreach ($u1s as $u1) {
13✔
3120
                        $u2s = $this->dbhr->preQuery("SELECT systemrole FROM users WHERE id = $id2;");
13✔
3121
                        foreach ($u2s as $u2) {
13✔
3122
                            $rc = $this->dbhm->preExec("UPDATE users SET systemrole = ? WHERE id = $id1;", [
13✔
3123
                                $this->systemRoleMax($u1['systemrole'], $u2['systemrole'])
13✔
3124
                            ]);
13✔
3125
                        }
3126
                        User::clearCache($id1);
13✔
3127
                    }
3128

3129
                    # Merge the add date.
3130
                    $u1 = User::get($this->dbhr, $this->dbhm, $id1);
13✔
3131
                    $u2 = User::get($this->dbhr, $this->dbhm, $id2);
13✔
3132
                    $this->dbhm->preExec("UPDATE users SET added = ? WHERE id = $id1;", [
13✔
3133
                        strtotime($u1->getPrivate('added')) < strtotime($u2->getPrivate('added')) ? $u1->getPrivate('added') : $u2->getPrivate('added')
13✔
3134
                    ]);
13✔
3135

3136
                    $this->dbhm->preExec("UPDATE users SET lastupdated = NOW() WHERE id = ?;", [
13✔
3137
                        $id1
13✔
3138
                    ]);
13✔
3139

3140
                    $tnid1 = $u2->getPrivate('tnuserid');
13✔
3141
                    $tnid2 = $u2->getPrivate('tnuserid');
13✔
3142

3143
                    if (!$tnid1 && $tnid2) {
13✔
3144
                        $u2->setPrivate('tnuserid', NULL);
×
3145
                        $u1->setPrivate('tnuserid', $tnid2);
×
3146
                    }
3147

3148
                    # Merge any gift aid.  We currently only support one declaration per user so we want to take
3149
                    # the most favourable.
3150
                    $giftaids = $this->dbhr->preQuery("SELECT * FROM giftaid WHERE userid IN ($id1, $id2) ORDER BY id ASC;");
13✔
3151

3152
                    if (count($giftaids)) {
13✔
3153
                        $weights = [
1✔
3154
                            Donations::PERIOD_PAST_4_YEARS_AND_FUTURE => 0,
1✔
3155
                            Donations::PERIOD_SINCE => 1,
1✔
3156
                            Donations::PERIOD_FUTURE=> 2,
1✔
3157
                            Donations::PERIOD_THIS => 3,
1✔
3158
                            Donations::PERIOD_DECLINED => 4
1✔
3159
                        ];
1✔
3160

3161
                        $best = NULL;
1✔
3162
                        foreach ($giftaids as $giftaid) {
1✔
3163
                            if ($best == NULL ||
1✔
3164
                                $weights[$giftaid['period']] < $weights[$best['period']]) {
1✔
3165
                                $best = $giftaid;
1✔
3166
                            }
3167
                        }
3168

3169
                        foreach ($giftaids as $giftaid) {
1✔
3170
                            if ($giftaid['id'] != $best['id']) {
1✔
3171
                                $this->dbhm->preExec("DELETE FROM giftaid WHERE id = ?;", [
1✔
3172
                                    $giftaid['id']
1✔
3173
                                ]);
1✔
3174
                            }
3175
                        }
3176

3177
                        $this->dbhm->preExec("UPDATE giftaid SET userid = ? WHERE id = ?;", [
1✔
3178
                            $id1,
1✔
3179
                            $best['id']
1✔
3180
                        ]);
1✔
3181
                    }
3182

3183
                    if ($rc) {
13✔
3184
                        # Log the merge - before the delete otherwise we will fail to log it.
3185
                        $l->log([
13✔
3186
                            'type' => Log::TYPE_USER,
13✔
3187
                            'subtype' => Log::SUBTYPE_MERGED,
13✔
3188
                            'user' => $id2,
13✔
3189
                            'byuser' => $me ? $me->getId() : NULL,
13✔
3190
                            'text' => "Merged $id2 into $id1 ($reason)"
13✔
3191
                        ]);
13✔
3192

3193
                        # Log under both users to make sure we can trace it.
3194
                        $l->log([
13✔
3195
                            'type' => Log::TYPE_USER,
13✔
3196
                            'subtype' => Log::SUBTYPE_MERGED,
13✔
3197
                            'user' => $id1,
13✔
3198
                            'byuser' => $me ? $me->getId() : NULL,
13✔
3199
                            'text' => "Merged $id2 into $id1 ($reason)"
13✔
3200
                        ]);
13✔
3201
                    }
3202

3203
                    if ($rc) {
13✔
3204
                        # Everything worked.
3205
                        $rollback = FALSE;
13✔
3206

3207
                        # We might have merged ourself!
3208
                        if (Utils::pres('id', $_SESSION) == $id2) {
13✔
3209
                            $_SESSION['id'] = $id1;
13✔
3210
                        }
3211
                    }
3212
                } catch (\Exception $e) {
1✔
3213
                    error_log("Merge exception " . $e->getMessage());
1✔
3214
                    $rollback = TRUE;
1✔
3215
                }
3216
            }
3217

3218
            if ($rollback) {
14✔
3219
                # Something went wrong.
3220
                #error_log("Merge failed, rollback");
3221
                $this->dbhm->rollBack();
1✔
3222
                $ret = FALSE;
1✔
3223
            } else {
3224
                #error_log("Merge worked, commit");
3225
                $ret = $this->dbhm->commit();
13✔
3226

3227
                if ($ret) {
13✔
3228
                    # Finally, delete id2.  We used to this inside the transaction, but the result was that
3229
                    # fromuser sometimes got set to NULL on messages owned by id2, despite them having been set to
3230
                    # id1 earlier on.  Either we're dumb, or there's a subtle interaction between transactions,
3231
                    # foreign keys and Percona clusters.  This is safer and proves to be more reliable.
3232
                    #
3233
                    # Make sure we don't pick up an old cached version, as we've just changed it quite a bit.
3234
                    error_log("Merged $id1 < $id2, $reason");
13✔
3235
                    $deleteme = new User($this->dbhm, $this->dbhm, $id2);
13✔
3236
                    $rc = $deleteme->delete(NULL, NULL, NULL, FALSE);
13✔
3237
                }
3238
            }
3239
        }
3240

3241
        return ($ret);
16✔
3242
    }
3243

3244
    public function mailer($user, $modmail, $toname, $to, $bcc, $fromname, $from, $subject, $text) {
3245
        try {
3246
            #error_log(session_id() . " mail " . microtime(TRUE));
3247

3248
            list ($transport, $mailer) = Mail::getMailer();
4✔
3249

3250
            $message = \Swift_Message::newInstance()
4✔
3251
                ->setSubject($subject)
4✔
3252
                ->setFrom([$from => $fromname])
4✔
3253
                ->setTo([$to => $toname])
4✔
3254
                ->setBody($text);
4✔
3255

3256
            # We add some headers so that if we receive this back, we can identify it as a mod mail.
3257
            $headers = $message->getHeaders();
4✔
3258

3259
            if ($user) {
4✔
3260
                $headers->addTextHeader('X-Iznik-From-User', $user->getId());
4✔
3261
            }
3262

3263
            $headers->addTextHeader('X-Iznik-ModMail', $modmail);
4✔
3264

3265
            if ($bcc) {
4✔
3266
                $message->setBcc(explode(',', $bcc));
1✔
3267
            }
3268

3269
            Mail::addHeaders($this->dbhr, $this->dbhm, $message,Mail::MODMAIL, $user->getId());
4✔
3270

3271
            $this->sendIt($mailer, $message);
4✔
3272

3273
            # Stop the transport, otherwise the message doesn't get sent until the UT script finishes.
3274
            $transport->stop();
4✔
3275

3276
            #error_log(session_id() . " mailed " . microtime(TRUE));
3277
        } catch (\Exception $e) {
4✔
3278
            # Not much we can do - shouldn't really happen given the failover transport.
3279
            // @codeCoverageIgnoreStart
3280
            error_log("Send failed with " . $e->getMessage());
3281
            // @codeCoverageIgnoreEnd
3282
        }
3283
    }
3284

3285
    private function maybeMail($groupid, $subject, $body, $action)
3286
    {
3287
        if ($body) {
4✔
3288
            # We have a mail to send.
3289
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3290
            $myid = $me->getId();
4✔
3291

3292
            $g = Group::get($this->dbhr, $this->dbhm, $groupid);
4✔
3293
            $atts = $g->getPublic();
4✔
3294

3295
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3296

3297
            # Find who to send it from.  If we have a config to use for this group then it will tell us.
3298
            $name = $me->getName();
4✔
3299
            $c = new ModConfig($this->dbhr, $this->dbhm);
4✔
3300
            $cid = $c->getForGroup($me->getId(), $groupid);
4✔
3301
            $c = new ModConfig($this->dbhr, $this->dbhm, $cid);
4✔
3302
            $fromname = $c->getPrivate('fromname');
4✔
3303
            $name = ($fromname == 'Groupname Moderator') ? '$groupname Moderator' : $name;
4✔
3304

3305
            # We can do a simple substitution in the from name.
3306
            $name = str_replace('$groupname', $atts['namedisplay'], $name);
4✔
3307

3308
            $bcc = $c->getBcc($action);
4✔
3309

3310
            if ($bcc) {
4✔
3311
                $bcc = str_replace('$groupname', $atts['nameshort'], $bcc);
1✔
3312
            }
3313

3314
            # We add the message into chat.
3315
            $r = new ChatRoom($this->dbhr, $this->dbhm);
4✔
3316
            $rid = $r->createUser2Mod($this->id, $groupid);
4✔
3317
            $m = NULL;
4✔
3318

3319
            $to = $this->getEmailPreferred();
4✔
3320

3321
            if ($rid) {
4✔
3322
                # Create the message.  Mark it as needing review to prevent timing window.
3323
                $m = new ChatMessage($this->dbhr, $this->dbhm);
4✔
3324
                list ($mid, $banned) = $m->create($rid,
4✔
3325
                    $myid,
4✔
3326
                    "$subject\r\n\r\n$body",
4✔
3327
                    ChatMessage::TYPE_MODMAIL,
4✔
3328
                    NULL,
4✔
3329
                    TRUE,
4✔
3330
                    NULL,
4✔
3331
                    NULL,
4✔
3332
                    NULL,
4✔
3333
                    NULL,
4✔
3334
                    NULL,
4✔
3335
                    TRUE,
4✔
3336
                    TRUE);
4✔
3337

3338
                $this->mailer($me, TRUE, $this->getName(), $bcc, NULL, $name, $g->getModsEmail(), $subject, "(This is a BCC of a message sent to Freegle user #" . $this->id . " $to)\n\n" . $body);
4✔
3339
            }
3340

3341
            if ($to && !Mail::ourDomain($to)) {
4✔
3342
                # For users who we host, we leave the message unseen; that will then later generate a notification
3343
                # to them.  Otherwise we mail them the message and mark it as seen, because they would get
3344
                # confused by a mail in our notification format.
3345
                $this->mailer($me, TRUE, $this->getName(), $to, NULL, $name, $g->getModsEmail(), $subject, $body);
4✔
3346

3347
                # We've mailed the message out so they are up to date with this chat.
3348
                $r->upToDate($this->id);
4✔
3349
            }
3350

3351
            if ($m) {
4✔
3352
                # We, as a mod, have seen this message - update the roster to show that.  This avoids this message
3353
                # appearing as unread to us.
3354
                $r->updateRoster($myid, $mid);
4✔
3355

3356
                # Ensure that the other mods are present in the roster with the message seen/unseen depending on
3357
                # whether that's what we want.
3358
                $mods = $g->getMods();
4✔
3359
                foreach ($mods as $mod) {
4✔
3360
                    if ($mod != $myid) {
3✔
3361
                        if ($c->getPrivate('chatread')) {
2✔
3362
                            # We want to mark it as seen for all mods.
3363
                            $r->updateRoster($mod, $mid, ChatRoom::STATUS_AWAY);
1✔
3364
                        } else {
3365
                            # Leave it unseen, but make sure they're in the roster.
3366
                            $r->updateRoster($mod, NULL, ChatRoom::STATUS_AWAY);
1✔
3367
                        }
3368
                    }
3369
                }
3370

3371
                if ($c->getPrivate('chatread')) {
4✔
3372
                    $m->setPrivate('mailedtoall', 1);
1✔
3373
                    $m->setPrivate('seenbyall', 1);
1✔
3374
                }
3375

3376
                # Allow mailing to happen.
3377
                $m->setPrivate('reviewrequired', 0);
4✔
3378
            }
3379
        }
3380
    }
3381

3382
    public function mail($groupid, $subject, $body, $stdmsgid, $action = NULL)
3383
    {
3384
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3385

3386
        $this->log->log([
4✔
3387
            'type' => Log::TYPE_USER,
4✔
3388
            'subtype' => Log::SUBTYPE_MAILED,
4✔
3389
            'user' => $this->id,
4✔
3390
            'byuser' => $me ? $me->getId() : NULL,
4✔
3391
            'text' => $subject,
4✔
3392
            'groupid' => $groupid,
4✔
3393
            'stdmsgid' => $stdmsgid
4✔
3394
        ]);
4✔
3395

3396
        $this->maybeMail($groupid, $subject, $body, $action);
4✔
3397
    }
3398

3399
    public function happinessReviewed($happinessid) {
3400
        $this->dbhm->preExec("UPDATE messages_outcomes SET reviewed = 1 WHERE id = ?", [
1✔
3401
            $happinessid
1✔
3402
        ]);
1✔
3403
    }
3404

3405
    public function getCommentsForSingleUser($userid) {
3406
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3407
        $rets = [
4✔
3408
            $userid => [
4✔
3409
                'id' => $userid
4✔
3410
            ]
4✔
3411
        ];
4✔
3412

3413
        $this->getComments($me, $rets);
4✔
3414

3415
        return Utils::presdef('comments', $rets[$userid], NULL);
4✔
3416
    }
3417

3418
    public function getComments($me, &$rets)
3419
    {
3420
        $userids = array_keys($rets);
196✔
3421

3422
        if ($me && $me->isModerator()) {
196✔
3423
            # Generally there will be no or few comments.  It's quicker (because of indexing) to get them all and filter
3424
            # by groupid than it is to construct a query which includes groupid.  Likewise it's not really worth
3425
            # optimising the calls for byuser, since there won't be any for most users.
3426
            $sql = "SELECT * FROM users_comments WHERE userid IN (" . implode(',', $userids) . ") ORDER BY date DESC;";
78✔
3427
            $comments = $this->dbhr->preQuery($sql);
78✔
3428
            #error_log("Got comments " . var_export($comments, TRUE));
3429

3430
            $commentuids = [];
78✔
3431
            foreach ($comments as $comment) {
78✔
3432
                if (Utils::pres('byuserid', $comment)) {
2✔
3433
                    $commentuids[] = $comment['byuserid'];
2✔
3434
                }
3435
            }
3436

3437
            $commentusers = [];
78✔
3438

3439
            if ($commentuids && count($commentuids)) {
78✔
3440
                $commentusers = $this->getPublicsById($commentuids, NULL, FALSE, FALSE, FALSE, FALSE);
2✔
3441

3442
                foreach ($commentusers as &$commentuser) {
2✔
3443
                    $commentuser['settings'] = NULL;
2✔
3444
                }
3445
            }
3446

3447
            foreach ($rets as $retind => $ret) {
78✔
3448
                $rets[$retind]['comments'] = [];
78✔
3449

3450
                for ($commentind = 0; $commentind < count($comments); $commentind++) {
78✔
3451
                    if ($comments[$commentind]['userid'] == $rets[$retind]['id']) {
2✔
3452
                        $comments[$commentind]['date'] = Utils::ISODate($comments[$commentind]['date']);
2✔
3453
                        $comments[$commentind]['reviewed'] = Utils::ISODate($comments[$commentind]['reviewed']);
2✔
3454

3455
                        if (Utils::pres('byuserid', $comments[$commentind])) {
2✔
3456
                            $comments[$commentind]['byuser'] = $commentusers[$comments[$commentind]['byuserid']];
2✔
3457
                        }
3458

3459
                        $rets[$retind]['comments'][] = $comments[$commentind];
2✔
3460
                    }
3461
                }
3462
            }
3463
        }
3464
    }
3465

3466
    public function listComments(&$ctx, $groupid = NULL) {
3467
        $comments = [];
1✔
3468
        $ctxq = '';
1✔
3469

3470
        if ($ctx && Utils::pres('reviewed', $ctx)) {
1✔
3471
            $ctxq = "users_comments.reviewed < " . $this->dbhr->quote($ctx['reviewed']) . " AND ";
1✔
3472
        }
3473

3474
        $groupq = $groupid ? " groupid = $groupid AND " : '';
1✔
3475

3476
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
3477
        $groupids = $me ? $me->getModeratorships() : [];
1✔
3478

3479
        if (count($groupids)) {
1✔
3480
            $byq = $groupid ? '' : (' OR users_comments.byuserid = ' . $me->getId());
1✔
3481
            $sql = "SELECT * FROM users_comments WHERE $groupq $ctxq (groupid IN (" . implode(',', $groupids) . ") $byq) ORDER BY reviewed desc LIMIT 10;";
1✔
3482
            $comments = $this->dbhr->preQuery($sql);
1✔
3483

3484
            $uids = array_unique(array_merge(array_column($comments, 'byuserid'), array_column($comments, 'userid')));
1✔
3485
            $u = new User($this->dbhr, $this->dbhm);
1✔
3486
            $users = $u->getPublicsById($uids, NULL, FALSE, FALSE, FALSE, FALSE);
1✔
3487

3488
            foreach ($comments as &$comment) {
1✔
3489
                $comment['date'] = Utils::ISODate($comment['date']);
1✔
3490
                $comment['reviewed'] = Utils::ISODate($comment['reviewed']);
1✔
3491

3492
                if (Utils::pres('userid', $comment)) {
1✔
3493
                    $comment['user'] = $users[$comment['userid']];
1✔
3494
                    unset($comment['userid']);
1✔
3495
                }
3496

3497
                if (Utils::pres('byuserid', $comment)) {
1✔
3498
                    $comment['byuser'] = $users[$comment['byuserid']];
1✔
3499
                    unset($comment['byuserid']);
1✔
3500
                }
3501

3502
                $ctx['reviewed'] = $comment['reviewed'];
1✔
3503
            }
3504
        }
3505

3506
        return $comments;
1✔
3507
    }
3508

3509
    public function getComment($id)
3510
    {
3511
        # We can only see comments on groups on which we have mod status.
3512
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3513
        $groupids = $me ? $me->getModeratorships() : [];
2✔
3514
        $groupids = count($groupids) == 0 ? [0] : $groupids;
2✔
3515

3516
        $sql = "SELECT * FROM users_comments WHERE id = ? AND groupid IN (" . implode(',', $groupids) . ") ORDER BY date DESC;";
2✔
3517
        $comments = $this->dbhr->preQuery($sql, [$id]);
2✔
3518

3519
        foreach ($comments as &$comment) {
2✔
3520
            $comment['date'] = Utils::ISODate($comment['date']);
2✔
3521
            $comment['reviewed'] = Utils::ISODate($comment['reviewed']);
2✔
3522

3523
            if (Utils::pres('byuserid', $comment)) {
2✔
3524
                $u = User::get($this->dbhr, $this->dbhm, $comment['byuserid']);
2✔
3525
                $comment['byuser'] = $u->getPublic();
2✔
3526
            }
3527

3528
            return ($comment);
2✔
3529
        }
3530

3531
        return (NULL);
1✔
3532
    }
3533

3534
    public function addComment($groupid, $user1 = NULL, $user2 = NULL, $user3 = NULL, $user4 = NULL, $user5 = NULL,
3535
                               $user6 = NULL, $user7 = NULL, $user8 = NULL, $user9 = NULL, $user10 = NULL,
3536
                               $user11 = NULL, $byuserid = NULL, $checkperms = TRUE, $flag = FALSE)
3537
    {
3538
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
7✔
3539

3540
        # By any supplied user else logged in user if any.
3541
        $byuserid = $byuserid ? $byuserid : ($me ? $me->getId() : NULL);
7✔
3542

3543
        # Can only add comments for a group on which we're a mod, or if we are Support adding a global comment.
3544
        $rc = NULL;
7✔
3545
        $groups = $checkperms ? ($me ? $me->getModeratorships() : [0]) : [$groupid];
7✔
3546
        $added = FALSE;
7✔
3547

3548
        foreach ($groups as $modgroupid) {
7✔
3549
            if ($groupid == $modgroupid) {
7✔
3550
                $sql = "INSERT INTO users_comments (userid, groupid, byuserid, user1, user2, user3, user4, user5, user6, user7, user8, user9, user10, user11, flag) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
6✔
3551
                $this->dbhm->preExec($sql, [
6✔
3552
                    $this->id,
6✔
3553
                    $groupid,
6✔
3554
                    $byuserid,
6✔
3555
                    $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
6✔
3556
                    $flag ? 1 : 0
6✔
3557
                ]);
6✔
3558

3559
                $rc = $this->dbhm->lastInsertId();
6✔
3560

3561
                $added = TRUE;
6✔
3562
            }
3563
        }
3564

3565
        if (!$added && $me && $me->isAdminOrSupport()) {
7✔
3566
            $sql = "INSERT INTO users_comments (userid, groupid, byuserid, user1, user2, user3, user4, user5, user6, user7, user8, user9, user10, user11, flag) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
1✔
3567
            $this->dbhm->preExec($sql, [
1✔
3568
                $this->id,
1✔
3569
                NULL,
1✔
3570
                $byuserid,
1✔
3571
                $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
1✔
3572
                $flag ? 1 : 0
1✔
3573
            ]);
1✔
3574

3575
            $rc = $this->dbhm->lastInsertId();
1✔
3576
        }
3577

3578
        if ($rc && $flag) {
7✔
3579
            $this->flagOthers($groupid);
1✔
3580
        }
3581

3582
        return ($rc);
7✔
3583
    }
3584

3585
    private function flagOthers($groupid) {
3586
        # We want to flag this to any other groups that the member is on.
3587
        $membs = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ? AND groupid != ?;", [
2✔
3588
            $this->id,
2✔
3589
            $groupid
2✔
3590
        ]);
2✔
3591

3592
        foreach ($membs as $memb) {
2✔
3593
            $this->memberReview($memb['groupid'], TRUE, 'Note flagged to other groups');
2✔
3594
        }
3595
    }
3596

3597
    public function editComment($id, $user1 = NULL, $user2 = NULL, $user3 = NULL, $user4 = NULL, $user5 = NULL,
3598
                                $user6 = NULL, $user7 = NULL, $user8 = NULL, $user9 = NULL, $user10 = NULL,
3599
                                $user11 = NULL, $flag = FALSE)
3600
    {
3601
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
3602

3603
        # Update to logged in user if any.
3604
        $byuserid = $me ? $me->getId() : NULL;
3✔
3605

3606
        # Can only edit comments for a group on which we're a mod.  This code isn't that efficient but it doesn't
3607
        # happen often.
3608
        $rc = NULL;
3✔
3609
        $comments = $this->dbhr->preQuery("SELECT id, groupid FROM users_comments WHERE id = ?;", [
3✔
3610
            $id
3✔
3611
        ]);
3✔
3612

3613
        foreach ($comments as $comment) {
3✔
3614
            if ($me && ($me->isAdminOrSupport() || $me->isModOrOwner($comment['groupid']))) {
3✔
3615
                $sql = "UPDATE users_comments SET byuserid = ?, user1 = ?, user2 = ?, user3 = ?, user4 = ?, user5 = ?, user6 = ?, user7 = ?, user8 = ?, user9 = ?, user10 = ?, user11 = ?, reviewed = NOW(), flag = ? WHERE id = ?;";
3✔
3616
                $rc = $this->dbhm->preExec($sql, [
3✔
3617
                    $byuserid,
3✔
3618
                    $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
3✔
3619
                    $flag,
3✔
3620
                    $comment['id']
3✔
3621
                ]);
3✔
3622

3623
                if ($rc && $flag) {
3✔
3624
                    $this->flagOthers($comment['groupid']);
1✔
3625
                }
3626
            }
3627
        }
3628

3629
        return ($rc);
3✔
3630
    }
3631

3632
    public function deleteComment($id)
3633
    {
3634
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3635

3636
        # Can only delete comments for a group on which we're a mod.
3637
        $rc = FALSE;
2✔
3638

3639
        $comments = $this->dbhr->preQuery("SELECT id, groupid FROM users_comments WHERE id = ?;", [
2✔
3640
            $id
2✔
3641
        ]);
2✔
3642

3643
        foreach ($comments as $comment) {
2✔
3644
            if ($me && ($me->isAdminOrSupport() || $me->isModOrOwner($comment['groupid']))) {
2✔
3645
                $rc = $this->dbhm->preExec("DELETE FROM users_comments WHERE id = ?;", [$id]);
2✔
3646
            }
3647
        }
3648

3649
        return ($rc);
2✔
3650
    }
3651

3652
    public function deleteComments()
3653
    {
3654
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
3655

3656
        # Can only delete comments for a group on which we're a mod.
3657
        $rc = FALSE;
1✔
3658
        $groups = $me ? $me->getModeratorships() : [];
1✔
3659
        foreach ($groups as $modgroupid) {
1✔
3660
            $rc = $this->dbhm->preExec("DELETE FROM users_comments WHERE userid = ? AND groupid = ?;", [$this->id, $modgroupid]);
1✔
3661
        }
3662

3663
        return ($rc);
1✔
3664
    }
3665

3666
    public function split($email, $name = NULL)
3667
    {
3668
        # We want to ensure that the current user has no reference to these values.
3669
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3670
        $l = new Log($this->dbhr, $this->dbhm);
2✔
3671
        if ($email) {
2✔
3672
            $this->removeEmail($email);
2✔
3673
        }
3674

3675
        $l->log([
2✔
3676
            'type' => Log::TYPE_USER,
2✔
3677
            'subtype' => Log::SUBTYPE_SPLIT,
2✔
3678
            'user' => $this->id,
2✔
3679
            'byuser' => $me ? $me->getId() : NULL,
2✔
3680
            'text' => "Split out $email"
2✔
3681
        ]);
2✔
3682

3683
        $u = new User($this->dbhr, $this->dbhm);
2✔
3684
        $uid2 = $u->create(NULL, NULL, $name);
2✔
3685
        $u->addEmail($email);
2✔
3686

3687
        # We might be able to move some messages over.
3688
        $this->dbhm->preExec("UPDATE messages SET fromuser = ? WHERE fromaddr = ?;", [
2✔
3689
            $uid2,
2✔
3690
            $email
2✔
3691
        ]);
2✔
3692
        $this->dbhm->preExec("UPDATE messages_history SET fromuser = ? WHERE fromaddr = ?;", [
2✔
3693
            $uid2,
2✔
3694
            $email
2✔
3695
        ]);
2✔
3696

3697
        # Chats which reference the messages sent from that email must also be intended for the split user.
3698
        $chats = $this->dbhr->preQuery("SELECT DISTINCT chat_rooms.* FROM chat_rooms INNER JOIN chat_messages ON chat_messages.chatid = chat_rooms.id WHERE refmsgid IN (SELECT id FROM messages WHERE fromaddr = ?);", [
2✔
3699
            $email
2✔
3700
        ]);
2✔
3701

3702
        foreach ($chats as $chat) {
2✔
3703
            if ($chat['user1'] == $this->id) {
1✔
3704
                $this->dbhm->preExec("UPDATE chat_rooms SET user1 = ? WHERE id = ?;", [
1✔
3705
                    $uid2,
1✔
3706
                    $chat['id']
1✔
3707
                ]);
1✔
3708
            }
3709

3710
            if ($chat['user2'] == $this->id) {
1✔
3711
                $this->dbhm->preExec("UPDATE chat_rooms SET user2 = ? WHERE id = ?;", [
1✔
3712
                    $uid2,
1✔
3713
                    $chat['id']
1✔
3714
                ]);
1✔
3715
            }
3716
        }
3717

3718
        # We might have a name.
3719
        $this->dbhm->preExec("UPDATE users SET fullname = (SELECT fromname FROM messages WHERE fromaddr = ? LIMIT 1) WHERE id = ?;", [
2✔
3720
            $email,
2✔
3721
            $uid2
2✔
3722
        ]);
2✔
3723

3724
        # Zap any existing sessions for either.
3725
        $this->dbhm->preExec("DELETE FROM sessions WHERE userid IN (?, ?);", [$this->id, $uid2]);
2✔
3726

3727
        # We can't tell which user any existing logins relate to.  So remove them all.  If they log in with native,
3728
        # then they'll have to get a new password.  If they use social login, then it should hook the user up again
3729
        # when they next do.
3730
        $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ?;", [$this->id]);
2✔
3731

3732
        return ($uid2);
2✔
3733
    }
3734

3735
    public function welcome($email, $password)
3736
    {
3737
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
6✔
3738
        $twig = new \Twig_Environment($loader);
6✔
3739

3740
        $html = $twig->render('welcome/welcome.html', [
6✔
3741
            'email' => $email,
6✔
3742
            'password' => $password
6✔
3743
        ]);
6✔
3744

3745
        $message = \Swift_Message::newInstance()
6✔
3746
            ->setSubject("Welcome to " . SITE_NAME . "!")
6✔
3747
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
6✔
3748
            ->setTo($email)
6✔
3749
            ->setBody("Thanks for joining" . SITE_NAME . "!" . ($password ? "  Here's your password: $password." : ''));
6✔
3750

3751
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3752
        # Outlook.
3753
        $htmlPart = \Swift_MimePart::newInstance();
6✔
3754
        $htmlPart->setCharset('utf-8');
6✔
3755
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
6✔
3756
        $htmlPart->setContentType('text/html');
6✔
3757
        $htmlPart->setBody($html);
6✔
3758
        $message->attach($htmlPart);
6✔
3759

3760
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::WELCOME, $this->getId());
6✔
3761

3762
        list ($transport, $mailer) = Mail::getMailer();
6✔
3763
        $this->sendIt($mailer, $message);
6✔
3764
    }
3765

3766
    public function FBL()
3767
    {
3768
        $settings = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FBL, TRUE);
1✔
3769
        $unsubscribe = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FBL, TRUE);
1✔
3770

3771
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
1✔
3772
        $twig = new \Twig_Environment($loader);
1✔
3773

3774
        $html = $twig->render('fbl.html', [
1✔
3775
            'email' => $this->getEmailPreferred(),
1✔
3776
            'unsubscribe' => $unsubscribe,
1✔
3777
            'settings' => $settings
1✔
3778
        ]);
1✔
3779

3780
        $message = \Swift_Message::newInstance()
1✔
3781
            ->setSubject("We've turned off emails for you")
1✔
3782
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
1✔
3783
            ->setTo($this->getEmailPreferred())
1✔
3784
//            ->setBcc('log@ehibbert.org.uk')
1✔
3785
            ->setBody("You marked a mail as spam, so we've turned off your emails.  You can leave Freegle completely from $unsubscribe or turn them back on from $settings.");
1✔
3786

3787
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3788
        # Outlook.
3789
        $htmlPart = \Swift_MimePart::newInstance();
1✔
3790
        $htmlPart->setCharset('utf-8');
1✔
3791
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
3792
        $htmlPart->setContentType('text/html');
1✔
3793
        $htmlPart->setBody($html);
1✔
3794
        $message->attach($htmlPart);
1✔
3795

3796
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::FBL_OFF, $this->getId());
1✔
3797

3798
        list ($transport, $mailer) = Mail::getMailer();
1✔
3799
        $this->sendIt($mailer, $message);
1✔
3800
    }
3801

3802
    public function forgotPassword($email)
3803
    {
3804
        $link = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FORGOT_PASSWORD, TRUE);
1✔
3805

3806
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/welcome');
1✔
3807
        $twig = new \Twig_Environment($loader);
1✔
3808

3809
        $html = $twig->render('forgotpassword.html', [
1✔
3810
            'email' => $this->getEmailPreferred(),
1✔
3811
            'url' => $link,
1✔
3812
        ]);
1✔
3813

3814
        $message = \Swift_Message::newInstance()
1✔
3815
            ->setSubject("Forgot your password?")
1✔
3816
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
1✔
3817
            ->setTo($email)
1✔
3818
            ->setBody("To set a new password, just log in here: $link");
1✔
3819

3820
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3821
        # Outlook.
3822
        $htmlPart = \Swift_MimePart::newInstance();
1✔
3823
        $htmlPart->setCharset('utf-8');
1✔
3824
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
3825
        $htmlPart->setContentType('text/html');
1✔
3826
        $htmlPart->setBody($html);
1✔
3827
        $message->attach($htmlPart);
1✔
3828

3829
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::FORGOT_PASSWORD, $this->getId());
1✔
3830

3831
        list ($transport, $mailer) = Mail::getMailer();
1✔
3832
        $this->sendIt($mailer, $message);
1✔
3833
    }
3834

3835
    public function verifyEmail($email, $force = FALSE)
3836
    {
3837
        # If this is one of our current emails, then we can just make it the primary.
3838
        $emails = $this->getEmails();
6✔
3839
        $handled = FALSE;
6✔
3840

3841
        if (!$force) {
6✔
3842
            foreach ($emails as $anemail) {
6✔
3843
                if (User::canonMail($anemail['email']) == User::canonMail($email)) {
6✔
3844
                    # It's one of ours already; make sure it's flagged as primary.
3845
                    $this->addEmail($email, 1);
3✔
3846
                    $handled = TRUE;
3✔
3847
                }
3848
            }
3849
        }
3850

3851
        if (!$handled) {
6✔
3852
            # This email is new to this user.  It may or may not currently be in use for another user.  Either
3853
            # way we want to send a verification mail.
3854
            $usersite = strpos($_SERVER['HTTP_HOST'], USER_SITE) !== FALSE || strpos($_SERVER['HTTP_HOST'], 'fdapi') !== FALSE;
5✔
3855
            $headers = "From: " . SITE_NAME . " <" . NOREPLY_ADDR . ">\nContent-Type: multipart/alternative; boundary=\"_I_Z_N_I_K_\"\nMIME-Version: 1.0";
5✔
3856
            $canon = User::canonMail($email);
5✔
3857

3858
            # We might be in the process of validating - sometimes people get confused and do this repeatedly.  So
3859
            # don't alter the key if it's only recently been set.
3860
            $existing = $this->dbhr->preQuery("SELECT validatekey FROM users_emails WHERE canon = ? AND TIMESTAMPDIFF(SECOND, validatetime, NOW()) < 600;", [
5✔
3861
                $canon
5✔
3862
            ]);
5✔
3863

3864
            $key = count($existing) ? $existing[0]['validatekey'] : NULL;
5✔
3865

3866
            if (!$key) {
5✔
3867
                do {
3868
                    # Loop in case of clash on the key we happen to invent.
3869
                    $key = uniqid();
5✔
3870
                    $sql = "INSERT INTO users_emails (email, canon, validatekey, backwards) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE validatekey = ?;";
5✔
3871
                    $this->dbhm->preExec($sql,
5✔
3872
                                         [$email, $canon, $key, strrev($canon), $key]);
5✔
3873
                } while (!$this->dbhm->rowsAffected());
5✔
3874
            }
3875

3876
            $confirm = $this->loginLink(USER_SITE, $this->id, "/settings/confirmmail/" . urlencode($key), 'changeemail', TRUE);
5✔
3877

3878
            list ($transport, $mailer) = Mail::getMailer();
5✔
3879
            $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
5✔
3880
            $twig = new \Twig_Environment($loader);
5✔
3881

3882
            $html = $twig->render('verifymail.html', [
5✔
3883
                'email' => $email,
5✔
3884
                'confirm' => $confirm
5✔
3885
            ]);
5✔
3886

3887
            $message = \Swift_Message::newInstance()
5✔
3888
                ->setSubject("Please verify your email")
5✔
3889
                ->setFrom([NOREPLY_ADDR => SITE_NAME])
5✔
3890
                ->setReturnPath($this->getBounce())
5✔
3891
                ->setTo([$email => $this->getName()])
5✔
3892
                ->setBody("Someone, probably you, has said that $email is their email address.\n\nIf this was you, please click on the link below to verify the address; if this wasn't you, please just ignore this mail.\n\n$confirm");
5✔
3893

3894
            # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3895
            # Outlook.
3896
            $htmlPart = \Swift_MimePart::newInstance();
5✔
3897
            $htmlPart->setCharset('utf-8');
5✔
3898
            $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
5✔
3899
            $htmlPart->setContentType('text/html');
5✔
3900
            $htmlPart->setBody($html);
5✔
3901
            $message->attach($htmlPart);
5✔
3902

3903
            Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::VERIFY_EMAIL, $this->getId());
5✔
3904

3905
            $this->sendIt($mailer, $message);
5✔
3906
        }
3907

3908
        return ($handled);
6✔
3909
    }
3910

3911
    public function confirmEmail($key)
3912
    {
3913
        $rc = FALSE;
2✔
3914
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3915

3916
        $sql = "SELECT * FROM users_emails WHERE validatekey = ?;";
2✔
3917
        $mails = $this->dbhr->preQuery($sql, [$key]);
2✔
3918

3919
        foreach ($mails as $mail) {
2✔
3920
            if ($mail['userid'] && $mail['userid'] != $me->getId()) {
2✔
3921
                # This email belongs to another user.  But we've confirmed that it is ours.  So merge.
3922
                $this->merge($this->id, $mail['userid'], "Verified ownership of email {$mail['email']}");
2✔
3923
            }
3924

3925
            $this->dbhm->preExec("UPDATE users_emails SET preferred = 0 WHERE userid = ?;", [$this->id]);
2✔
3926
            $this->dbhm->preExec("UPDATE users_emails SET userid = ?, preferred = 1, validated = NOW(), validatekey = NULL WHERE id = ?;", [$this->id, $mail['id']]);
2✔
3927
            $this->addEmail($mail['email'], 1);
2✔
3928

3929
            $rc = $this->id;
2✔
3930
        }
3931

3932
        return ($rc);
2✔
3933
    }
3934

3935
    public function confirmUnsubscribe()
3936
    {
3937
        list ($transport, $mailer) = Mail::getMailer();
2✔
3938

3939
        $link = $this->getUnsubLink(USER_SITE, $this->id, NULL, TRUE) . "&confirm=1";
2✔
3940

3941
        $message = \Swift_Message::newInstance()
2✔
3942
            ->setSubject("Please confirm you want to leave Freegle")
2✔
3943
            ->setFrom(NOREPLY_ADDR)
2✔
3944
            ->setReplyTo(SUPPORT_ADDR)
2✔
3945
            ->setTo($this->getEmailPreferred())
2✔
3946
            ->setDate(time())
2✔
3947
            ->setBody("Please click here to leave Freegle:\r\n\r\n$link\r\n\r\nThis will remove all your data and cannot be undone.  If you just want to leave a Freegle or reduce the number of emails you get, please sign in and go to Settings.\r\n\r\nIf you didn't try to leave, please ignore this mail.\r\n\r\nThanks for freegling, and do please come back in the future.");
2✔
3948

3949
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::UNSUBSCRIBE);
2✔
3950
        $this->sendIt($mailer, $message);
2✔
3951
    }
3952

3953
    public function inventEmail($force = FALSE)
3954
    {
3955
        # An invented email is one on our domain that doesn't give away too much detail, but isn't just a string of
3956
        # numbers (ideally).  We may already have one.
3957
        $email = NULL;
46✔
3958

3959
        if (!$force) {
46✔
3960
            # We want the most recent of our own emails.
3961
            $emails = $this->getEmails(TRUE);
46✔
3962
            foreach ($emails as $thisemail) {
46✔
3963
                if (strpos($thisemail['email'], USER_DOMAIN) !== FALSE) {
28✔
3964
                    $email = $thisemail['email'];
15✔
3965
                    break;
15✔
3966
                }
3967
            }
3968
        }
3969

3970
        if (!$email) {
46✔
3971
            # If they have a Yahoo ID, that'll do nicely - it's public info.  But some Yahoo IDs are actually
3972
            # email addresses (don't ask) and we don't want those.  And some are stupidly long.
3973
            $yahooid = $this->getPrivate('yahooid');
33✔
3974

3975
            if (!$force && strlen(str_replace(' ', '', $yahooid)) && strpos($yahooid, '@') === FALSE && strlen($yahooid) <= 16) {
33✔
3976
                $email = str_replace(' ', '', $yahooid) . '-' . $this->id . '@' . USER_DOMAIN;
1✔
3977
            } else {
3978
                # Their own email might already be of that nature, which would be lovely.
3979
                if (!$force) {
33✔
3980
                    $email = $this->getEmailPreferred();
33✔
3981

3982
                    if ($email) {
33✔
3983
                        foreach (['firstname', 'lastname', 'fullname'] as $att) {
14✔
3984
                            $words = explode(' ', $this->user[$att]);
14✔
3985
                            foreach ($words as $word) {
14✔
3986
                                if (strlen($word) && stripos($email, $word) !== FALSE) {
14✔
3987
                                    # Unfortunately not - it has some personal info in it.
3988
                                    $email = NULL;
14✔
3989
                                }
3990
                            }
3991
                        }
3992

3993
                        if (stripos($email, '%') !== FALSE) {
14✔
3994
                            # This may indicate a case where the real email is encoded on the LHS, eg gtempaccount.com
3995
                            $email = NULL;
1✔
3996
                        }
3997

3998
                        if (stripos($email, SITE_NAME) !== FALSE || stripos($email, 'freegle') !== FALSE) {
14✔
3999
                            $email = NULL;
×
4000
                        }
4001
                    }
4002
                }
4003

4004
                if ($email) {
33✔
4005
                    # We have an email which is fairly anonymous.  Use the LHS.
4006
                    $p = strpos($email, '@');
1✔
4007
                    $email = str_replace(' ', '', $p > 0 ? substr($email, 0, $p) : $email) . '-' . $this->id . '@' . USER_DOMAIN;
1✔
4008
                } else {
4009
                    # We can't make up something similar to their existing email address so invent from scratch.
4010
                    $lengths = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/distinct_word_lengths.json'), TRUE);
33✔
4011
                    $bigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/word_start_bigrams.json'), TRUE);
33✔
4012
                    $trigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/trigrams.json'), TRUE);
33✔
4013

4014
                    do {
4015
                        $length = \Wordle\array_weighted_rand($lengths);
33✔
4016
                        $start = \Wordle\array_weighted_rand($bigrams);
33✔
4017
                        $email = strtolower(\Wordle\fill_word($start, $length, $trigrams)) . '-' . $this->id . '@' . USER_DOMAIN;
33✔
4018

4019
                        # We might just happen to have invented an email with their personal information in it.  This
4020
                        # actually happened in the UT with "test".
4021
                        foreach (['firstname', 'lastname', 'fullname'] as $att) {
33✔
4022
                            $words = explode(' ', $this->user[$att]);
33✔
4023
                            foreach ($words as $word) {
33✔
4024
                                $word = trim($word);
33✔
4025
                                if (strlen($word)) {
33✔
4026
                                    $p = stripos($email, $word);
23✔
4027
                                    $q = strpos($email, '-');
23✔
4028

4029
                                    if ($word !== '-') {
23✔
4030
                                        # Dash is always present, which is fine.
4031
                                        $email = ($p !== FALSE && $p < $q) ? NULL : $email;
23✔
4032
                                    }
4033
                                }
4034
                            }
4035
                        }
4036
                    } while (!$email);
33✔
4037
                }
4038
            }
4039
        }
4040

4041
        return ($email);
46✔
4042
    }
4043

4044
    public function delete($groupid = NULL, $subject = NULL, $body = NULL, $log = TRUE)
4045
    {
4046
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
18✔
4047

4048
        # Delete memberships.  This will remove any Yahoo memberships.
4049
        $membs = $this->getMemberships();
18✔
4050
        #error_log("Members in delete " . var_export($membs, TRUE));
4051
        foreach ($membs as $memb) {
18✔
4052
            $this->removeMembership($memb['id']);
8✔
4053
        }
4054

4055
        $rc = $this->dbhm->preExec("DELETE FROM users WHERE id = ?;", [$this->id]);
18✔
4056

4057
        if ($rc && $log) {
18✔
4058
            $this->log->log([
5✔
4059
                'type' => Log::TYPE_USER,
5✔
4060
                'subtype' => Log::SUBTYPE_DELETED,
5✔
4061
                'user' => $this->id,
5✔
4062
                'byuser' => $me ? $me->getId() : NULL,
5✔
4063
                'text' => $this->getName()
5✔
4064
            ]);
5✔
4065
        }
4066

4067
        return ($rc);
18✔
4068
    }
4069

4070
    public function getUnsubLink($domain, $id, $type = NULL, $auto = FALSE)
4071
    {
4072
        return (User::loginLink($domain, $id, "/unsubscribe/$id", $type, $auto));
26✔
4073
    }
4074

4075
    public function listUnsubscribe($id, $type = NULL) {
4076
        # These are links which will completely unsubscribe the user.  This is necessary because of Yahoo and Gmail
4077
        # changes in 2024, and also useful for CAN-SPAM.  We want them to involve the key to prevent spoof unsubscribes.
4078
        #
4079
        # We only include the web link, because this providers a better user experience - we can tell them
4080
        # things afterwards.  This is valid - RFC8058 the RFC says you MUST include an HTTPS link, and you MAY
4081
        # include others.
4082
        $key = $id ? $this->getUserKey($id) : '1234';
137✔
4083
        $key = $key ? $key : '1234';
136✔
4084
        #$ret = "<mailto:unsubscribe-$id-$key-$type@" . USER_DOMAIN . "?subject=unsubscribe>, <https://" . USER_SITE . "/one-click-unsubscribe/$id/$key>";
4085
        $ret = "<https://" . USER_SITE . "/one-click-unsubscribe/$id/$key>";
136✔
4086
        #$ret = "<http://localhost:3002/one-click-unsubscribe/$id/$key>";
4087
        return $ret;
136✔
4088
    }
4089

4090
    public function loginLink($domain, $id, $url = '/', $type = NULL, $auto = FALSE)
4091
    {
4092
        $p = strpos($url, '?');
58✔
4093
        $ret = $p === FALSE ? "https://$domain$url?u=$id&src=$type" : "https://$domain$url&u=$id&src=$type";
58✔
4094

4095
        if ($auto) {
58✔
4096
            # Get a per-user link we can use to log in without a password.
4097
            $key = $this->getUserKey($id);
11✔
4098

4099
            # If this didn't work, we still return an URL - worst case they'll have to sign in.
4100
            $key = $key ? $key : null;
11✔
4101

4102
            $p = strpos($url, '?');
11✔
4103
            $src = $type ? "&src=$type" : "";
11✔
4104
            $ret = $p === FALSE ? ("https://$domain$url?u=$id&k=$key$src") : ("https://$domain$url&u=$id&k=$key$src");
11✔
4105
        }
4106

4107
        return ($ret);
58✔
4108
    }
4109

4110
    public function sendOurMails($g = NULL, $checkholiday = TRUE, $checkbouncing = TRUE)
4111
    {
4112
        if ($this->getPrivate('deleted')) {
102✔
4113
            return FALSE;
1✔
4114
        }
4115

4116
        # We have two kinds of email settings - the top-level Simple one, and more detailed per group ones.
4117
        # Where present the Simple one overrides the group ones, so check that first.
4118
        $simplemail = $this->getSetting('simplemail', NULL);
102✔
4119
        if ($simplemail === User::SIMPLE_MAIL_NONE) {
102✔
4120
            return FALSE;
×
4121
        }
4122

4123
        # We don't want to send emails to people who haven't been active for more than six months.  This improves
4124
        # our spam reputation, by avoiding honeytraps.
4125
        $sendit = FALSE;
102✔
4126
        $lastaccess = strtotime($this->getPrivate('lastaccess'));
102✔
4127

4128
        // This time is also present on the client in ModMember, and in Engage.
4129
        if (time() - $lastaccess <= Engage::USER_INACTIVE) {
102✔
4130
            $sendit = TRUE;
102✔
4131

4132
            if ($sendit && $checkholiday) {
102✔
4133
                # We might be on holiday.
4134
                $hol = $this->getPrivate('onholidaytill');
23✔
4135
                $till = $hol ? strtotime($hol) : 0;
23✔
4136
                #error_log("Holiday $till vs " . time());
4137

4138
                $sendit = time() > $till;
23✔
4139
            }
4140

4141
            if ($sendit && $checkbouncing) {
102✔
4142
                # And don't send if we're bouncing.
4143
                $sendit = !$this->getPrivate('bouncing');
23✔
4144
                #error_log("After bouncing $sendit");
4145
            }
4146
        }
4147

4148
        #error_log("Sendit? $sendit");
4149
        return ($sendit);
102✔
4150
    }
4151

4152
    public function getMembershipHistory()
4153
    {
4154
        # We get this from our logs.
4155
        $sql = "SELECT * FROM logs WHERE user = ? AND `type` = ? ORDER BY id DESC;";
5✔
4156
        $logs = $this->dbhr->preQuery($sql, [$this->id, Log::TYPE_GROUP]);
5✔
4157

4158
        $ret = [];
5✔
4159
        foreach ($logs as $log) {
5✔
4160
            $thisone = NULL;
3✔
4161
            switch ($log['subtype']) {
3✔
4162
                case Log::SUBTYPE_JOINED:
3✔
4163
                case Log::SUBTYPE_APPROVED:
1✔
4164
                case Log::SUBTYPE_REJECTED:
1✔
4165
                case Log::SUBTYPE_APPLIED:
1✔
4166
                case Log::SUBTYPE_LEFT:
1✔
4167
                    {
3✔
4168
                        $thisone = $log['subtype'];
3✔
4169
                        break;
3✔
4170
                    }
3✔
4171
            }
4172

4173
            #error_log("{$log['subtype']} gives $thisone {$log['groupid']}");
4174
            if ($thisone && $log['groupid']) {
3✔
4175
                $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
3✔
4176

4177
                if ($g->getId() ==  $log['groupid']) {
3✔
4178
                    $ret[] = [
3✔
4179
                        'timestamp' => Utils::ISODate($log['timestamp']),
3✔
4180
                        'type' => $thisone,
3✔
4181
                        'group' => [
3✔
4182
                            'id' => $log['groupid'],
3✔
4183
                            'nameshort' => $g->getPrivate('nameshort'),
3✔
4184
                            'namedisplay' => $g->getName()
3✔
4185
                        ],
3✔
4186
                        'text' => $log['text']
3✔
4187
                    ];
3✔
4188
                }
4189
            }
4190
        }
4191

4192
        return ($ret);
5✔
4193
    }
4194

4195
    public function search($search, $ctx)
4196
    {
4197
        if (preg_replace('/\-|\~/', '', $search) ==  '') {
5✔
4198
            # Most likely an encoded id.
4199
            $search = User::decodeId($search);
×
4200
        }
4201

4202
        if (preg_match('/story-(.*)/', $search, $matches)) {
5✔
4203
            # Story.
4204
            $s = new Story($this->dbhr, $this->dbhm, $matches[1]);
×
4205
            $search = $s->getPrivate('userid');
×
4206
        }
4207

4208
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
5✔
4209
        $id = intval(Utils::presdef('id', $ctx, 0));
5✔
4210
        $ctx = $ctx ? $ctx : [];
5✔
4211
        $q = $this->dbhr->quote("$search%");
5✔
4212
        $backwards = strrev($search);
5✔
4213
        $qb = $this->dbhr->quote("$backwards%");
5✔
4214

4215
        $canon = $this->dbhr->quote(User::canonMail($search) . "%");
5✔
4216

4217
        # canonMail might not strip the dots out if we don't have a full email to search on, but for this Support Tools
4218
        # search we want to try quite hard.
4219
        $canon2 = $this->dbhr->quote(User::canonMail(str_replace('.', '', $search)) . "%");
5✔
4220

4221
        # If we're searching for a notify address, switch to the user it.
4222
        $search = preg_match('/notify-(.*)-(.*)' . USER_DOMAIN . '/', $search, $matches) ? $matches[2] : $search;
5✔
4223

4224
        $sql = "SELECT DISTINCT userid FROM
5✔
4225
                ((SELECT userid FROM users_emails WHERE canon LIKE $canon OR backwards LIKE $qb) UNION
5✔
4226
                (SELECT userid FROM users_emails WHERE canon LIKE $canon2) UNION
5✔
4227
                (SELECT id AS userid FROM users WHERE fullname LIKE $q) UNION
5✔
4228
                (SELECT id AS userid FROM users WHERE yahooid LIKE $q) UNION
5✔
4229
                (SELECT id AS userid FROM users WHERE id = ?) UNION
4230
                (SELECT userid FROM users_logins WHERE uid LIKE $q)) t WHERE userid > ? ORDER BY userid ASC";
5✔
4231
        $users = $this->dbhr->preQuery($sql, [$search, $id]);
5✔
4232

4233
        $ret = [];
5✔
4234

4235
        foreach ($users as $user) {
5✔
4236
            $ctx['id'] = $user['userid'];
4✔
4237

4238
            $u = User::get($this->dbhr, $this->dbhm, $user['userid']);
4✔
4239

4240
            $thisone = $u->getPublic(NULL, TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, [
4✔
4241
                MessageCollection::PENDING,
4✔
4242
                MessageCollection::APPROVED
4✔
4243
            ], TRUE);
4✔
4244

4245
            # We might not have the emails.
4246
            $thisone['email'] = $u->getEmailPreferred();
4✔
4247
            $thisone['emails'] = $u->getEmails();
4✔
4248

4249
            $thisone['membershiphistory'] = $u->getMembershipHistory();
4✔
4250

4251
            # Make sure there's a link login as admin/support can use that to impersonate.
4252
            if ($me && ($me->isAdmin() || ($me->isAdminOrSupport() && !$u->isModerator()))) {
4✔
4253
                $thisone['loginlink'] = $u->loginLink(USER_SITE, $user['userid'], '/', NULL, TRUE);
×
4254
            }
4255

4256
            $thisone['logins'] = $u->getLogins($me && $me->isAdmin());
4✔
4257

4258
            # Also return the chats for this user.  Can't use ChatRooms::listForUser because that would exclude any
4259
            # chats on groups where we were no longer a member.
4260
            $rooms = array_filter(array_column($this->dbhr->preQuery("SELECT id FROM chat_rooms WHERE user1 = ? UNION SELECT id FROM chat_rooms WHERE chattype = ? AND user2 = ?;", [
4✔
4261
                $user['userid'],
4✔
4262
                ChatRoom::TYPE_USER2USER,
4✔
4263
                $user['userid'],
4✔
4264
            ]), 'id'));
4✔
4265

4266
            $thisone['chatrooms'] = [];
4✔
4267

4268
            if ($rooms) {
4✔
4269
                $r = new ChatRoom($this->dbhr, $this->dbhm);
×
4270
                $thisone['chatrooms'] = $r->fetchRooms($rooms, $user['userid'], FALSE);
×
4271
            }
4272

4273
            # Add the public location and best guess lat/lng
4274
            $thisone['info']['publiclocation'] = $u->getPublicLocation();
4✔
4275
            $latlng = $u->getLatLng(FALSE, TRUE);
4✔
4276
            $thisone['privateposition'] = [
4✔
4277
                'lat' => $latlng[0],
4✔
4278
                'lng' => $latlng[1],
4✔
4279
                'name' => $latlng[2]
4✔
4280
            ];
4✔
4281

4282
            $thisone['comments'] = $this->getCommentsForSingleUser($user['userid']);
4✔
4283
            $thisone['tnuserid'] = $u->getPrivate('tnuserid');
4✔
4284

4285
            $push = $this->dbhr->preQuery("SELECT MAX(lastsent) AS lastpush FROM users_push_notifications WHERE userid = ?;", [
4✔
4286
                $user['userid']
4✔
4287
            ]);
4✔
4288

4289
            foreach ($push as $p) {
4✔
4290
                $thisone['lastpush'] = Utils::ISODate($p['lastpush']);
4✔
4291
            }
4292

4293
            $thisone['info'] = $u->getInfo();
4✔
4294
            $thisone['trustlevel'] = $u->getPrivate('trustlevel');
4✔
4295

4296
            $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE userid = ?;", [
4✔
4297
                $u->getId()
4✔
4298
            ]);
4✔
4299

4300
            $thisone['bans'] = [];
4✔
4301

4302
            foreach ($bans as $ban) {
4✔
4303
                $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
4304
                $banner = User::get($this->dbhr, $this->dbhm, $ban['byuser']);
1✔
4305
                $thisone['bans'][] = [
1✔
4306
                    'date' => Utils::ISODate($ban['date']),
1✔
4307
                    'group' => $g->getName(),
1✔
4308
                    'byemail' => $banner->getEmailPreferred(),
1✔
4309
                    'byuserid' => $ban['byuser']
1✔
4310
                ];
1✔
4311
            }
4312

4313
            $d = new Donations($this->dbhr, $this->dbhm);
4✔
4314
            $thisone['giftaid'] = $d->getGiftAid($user['userid']);
4✔
4315

4316
            if ($me->hasPermission(User::PERM_GIFTAID)) {
4✔
4317
                $thisone['donations'] = $d->listByUser($user['userid']);
2✔
4318
            }
4319

4320
            $thisone['newsfeedmodstatus'] = $u->getPrivate('newsfeedmodstatus');
4✔
4321
            $thisone['newsfeed'] = $this->dbhr->preQuery("SELECT id, message, timestamp, hidden, hiddenby, deleted, deletedby FROM newsfeed WHERE userid = ? ORDER BY id DESC;", [
4✔
4322
                $user['userid']
4✔
4323
            ]);
4✔
4324

4325
            foreach ($thisone['newsfeed'] as &$nf) {
4✔
4326
                $nf['timestamp'] = Utils::ISODate($nf['timestamp']);
×
4327
                $nf['deleted'] = Utils::ISODate($nf['deleted']);
×
4328
                $nf['hidden'] = Utils::ISODate($nf['hidden']);
×
4329
            }
4330

4331
            $ret[] = $thisone;
4✔
4332
        }
4333

4334
        return ($ret);
5✔
4335
    }
4336

4337
    private function safeGetPostcode($val) {
4338
        $ret = [ NULL, NULL ];
59✔
4339

4340
        $settings = $val ? json_decode($val, TRUE) : [];
59✔
4341

4342
        if (Utils::pres('mylocation', $settings) &&
59✔
4343
            Utils::presdef('type', $settings['mylocation'], NULL) == 'Postcode') {
59✔
4344
            $ret = [
14✔
4345
                Utils::presdef('id', $settings['mylocation'], NULL),
14✔
4346
                Utils::presdef('name', $settings['mylocation'], NULL)
14✔
4347
            ];
14✔
4348
        }
4349

4350
        return $ret;
59✔
4351
    }
4352

4353
    public function setPrivate($att, $val)
4354
    {
4355
        if (!strcmp($att, 'settings') && $val) {
196✔
4356
            # Possible location change.
4357
            list ($oldid, $oldloc) = $this->safeGetPostcode($this->getPrivate('settings'));
59✔
4358
            list ($newid, $newloc) = $this->safeGetPostcode($val);
59✔
4359

4360
            if ($oldloc !== $newloc) {
59✔
4361
                # We have changed our location.
4362
                parent::setPrivate('lastlocation', $newid);
14✔
4363
                $i = new Isochrone($this->dbhr, $this->dbhm);
14✔
4364
                $i->deleteForUser($this->id);
14✔
4365

4366
                $this->log->log([
14✔
4367
                            'type' => Log::TYPE_USER,
14✔
4368
                            'subtype' => Log::SUBTYPE_POSTCODECHANGE,
14✔
4369
                            'user' => $this->id,
14✔
4370
                            'text' => $newloc
14✔
4371
                        ]);
14✔
4372
            }
4373

4374
            // Prune the info in the settings to remove any groupsnear info, which would use space and is not needed.
4375
            $val = User::pruneSettings($val);
59✔
4376
        }
4377

4378
        User::clearCache($this->id);
196✔
4379
        parent::setPrivate($att, $val);
196✔
4380
    }
4381

4382
    public static function pruneSettings($val) {
4383
        // Prune info from what we store in the user table to keep it smaller.
4384
        if (strpos($val, 'groupsnear') !== FALSE) {
59✔
4385
            $decoded = json_decode($val, TRUE);
×
4386
            if (Utils::pres('mylocation', $decoded) && Utils::pres('groupsnear', $decoded['mylocation'])) {
×
4387
                unset($decoded['mylocation']['groupsnear']);
×
4388
                $val = json_encode($decoded);
×
4389
            }
4390
        }
4391

4392
        return $val;
59✔
4393
    }
4394

4395
    public function canMerge()
4396
    {
4397
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
16✔
4398
        return (array_key_exists('canmerge', $settings) ? $settings['canmerge'] : TRUE);
16✔
4399
    }
4400

4401
    public function notifsOn($type, $groupid = NULL)
4402
    {
4403
        if ($this->getPrivate('deleted')) {
596✔
4404
            return FALSE;
1✔
4405
        }
4406

4407
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
596✔
4408
        $notifs = Utils::pres('notifications', $settings);
596✔
4409

4410
        $defs = [
596✔
4411
            self::NOTIFS_EMAIL => TRUE,
596✔
4412
            self::NOTIFS_EMAIL_MINE => FALSE,
596✔
4413
            self::NOTIFS_PUSH => TRUE,
596✔
4414
            self::NOTIFS_APP => TRUE
596✔
4415
        ];
596✔
4416

4417
        $ret = ($notifs && array_key_exists($type, $notifs)) ? $notifs[$type] : $defs[$type];
596✔
4418

4419
        if ($ret && $groupid) {
596✔
4420
            # Check we're an active mod on this group - if not then we don't want the notifications.
4421
            $ret = $this->activeModForGroup($groupid);
5✔
4422
        }
4423

4424
        #error_log("Notifs on for user #{$this->id} type $type ? $ret from " . var_export($notifs, TRUE));
4425
        return ($ret);
596✔
4426
    }
4427

4428
    public function getNotificationPayload($modtools)
4429
    {
4430
        # This gets a notification count/title/message for this user.
4431
        $notifcount = 0;
8✔
4432
        $title = '';
8✔
4433
        $message = NULL;
8✔
4434
        $chatids = [];
8✔
4435
        $route = NULL;
8✔
4436

4437
        if (!$modtools) {
8✔
4438
            # User notification.  We want to show a count of chat messages, or some of the message if there is just one.
4439
            $r = new ChatRoom($this->dbhr, $this->dbhm);
6✔
4440
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_USER2USER, ChatRoom::TYPE_USER2MOD], $modtools);
6✔
4441
            $chatcount = count($unseen);
6✔
4442
            $total = $chatcount;
6✔
4443
            foreach ($unseen as $un) {
6✔
4444
                $chatids[] = $un['chatid'];
3✔
4445
            };
4446

4447
            #error_log("Chats with unseen " . var_export($chatids, TRUE));
4448
            $n = new Notifications($this->dbhr, $this->dbhm);
6✔
4449
            $notifcount = $n->countUnseen($this->id);
6✔
4450

4451
            if ($total ==  1) {
6✔
4452
                $r = new ChatRoom($this->dbhr, $this->dbhm, $unseen[0]['chatid']);
2✔
4453
                $atts = $r->getPublic($this);
2✔
4454
                $title = $atts['name'];
2✔
4455
                list($msgs, $users) = $r->getMessages(100, 0);
2✔
4456

4457
                if (count($msgs) > 0) {
2✔
4458
                    $message = Utils::presdef('message', $msgs[count($msgs) - 1], "You have a message");
2✔
4459

4460
                    # Decode emoji escape sequences to actual emojis for display.
4461
                    $message = Utils::decodeEmojis($message);
2✔
4462

4463
                    $message = strlen($message) > 256 ? (substr($message, 0, 256) . "...") : $message;
2✔
4464
                }
4465

4466
                $route = "/chats/" . $unseen[0]['chatid'];
2✔
4467

4468
                if ($notifcount) {
2✔
4469
                    $total += $notifcount;
2✔
4470
                }
4471
            } else if ($total > 1) {
4✔
4472
                $title = "You have $total new messages";
1✔
4473
                $route = "/chats";
1✔
4474

4475
                if ($notifcount) {
1✔
4476
                    $total += $notifcount;
1✔
4477
                    $title .= " and $notifcount notification" . ($notifcount == 1 ? '' : 's');
1✔
4478
                }
4479
            } else {
4480
                # Add in the notifications you see primarily from the newsfeed.
4481
                if ($notifcount) {
4✔
4482
                    $total += $notifcount;
4✔
4483
                    $ctx = NULL;
4✔
4484
                    $notifs = $n->get($this->id, $ctx);
4✔
4485
                    $title = $n->getNotifTitle($notifs);
4✔
4486

4487
                    if ($title) {
4✔
4488
                        $route = '/';
4✔
4489

4490
                        if (count($notifs) > 0) {
4✔
4491
                            # For newsfeed notifications sent a route to the right place.
4492
                            switch ($notifs[0]['type']) {
4✔
4493
                                case Notifications::TYPE_COMMENT_ON_COMMENT:
4✔
4494
                                case Notifications::TYPE_COMMENT_ON_YOUR_POST:
4✔
4495
                                case Notifications::TYPE_LOVED_COMMENT:
4✔
4496
                                case Notifications::TYPE_LOVED_POST:
4✔
4497
                                    $route = '/chitchat/' . $notifs[0]['newsfeedid'];
1✔
4498
                                    break;
6✔
4499
                            }
4500
                        }
4501
                    }
4502
                }
4503
            }
4504
        } else {
4505
            # ModTools notification.  We show the count of work + chats.
4506
            $r = new ChatRoom($this->dbhr, $this->dbhm);
4✔
4507
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_MOD2MOD, ChatRoom::TYPE_USER2MOD], $modtools);
4✔
4508
            $chatcount = count($unseen);
4✔
4509

4510
            $work = $this->getWorkCounts();
4✔
4511
            $total = $work['total'] + $chatcount;
4✔
4512

4513
            // The order of these is important as the route will be the last matching.
4514
            $types = [
4✔
4515
                'pendingvolunteering' => [ 'volunteer op', 'volunteerops', '/modtools/volunteering' ],
4✔
4516
                'pendingevents' => [ 'event', 'events', '/modtools/communityevents' ],
4✔
4517
                'socialactions' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4✔
4518
                'popularposts' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4✔
4519
                'stories' => [ 'story', 'stories', '/modtools/members/stories' ],
4✔
4520
                'newsletterstories' => [ 'newsletter story', 'newsletter stories', '/modtools/members/newsletter' ],
4✔
4521
                'chatreview' => [ 'chat message to review', 'chat messages to review', '/modtools/chats/review' ],
4✔
4522
                'pendingadmins' => [ 'admin', 'admins', '/modtools/admins' ],
4✔
4523
                'spammembers' => [ 'member to review', 'members to review', '/modtools/members/review' ],
4✔
4524
                'relatedmembers' => [ 'related member to review', 'related members to review', '/modtools/members/related' ],
4✔
4525
                'editreview' => [ 'edit', 'edits', '/modtools/messages/edits' ],
4✔
4526
                'spam' => [ 'message to review', 'messages to review', '/modtools/messages/pending' ],
4✔
4527
                'pending' => [ 'pending message', 'pending messages', '/modtools/messages/pending' ]
4✔
4528
            ];
4✔
4529

4530
            $title = $chatcount ? ("$chatcount chat message" . ($chatcount != 1 ? 's' : '') . "\n") : '';
4✔
4531
            $route = NULL;
4✔
4532

4533
            foreach ($types as $type => $vals) {
4✔
4534
                if (Utils::presdef($type, $work, 0) > 0) {
4✔
4535
                    $title .= $work[$type] . ' ' . ($work[$type] != 1 ? $vals[1] : $vals[0] ) . "\n";
1✔
4536
                    $route = $vals[2];
1✔
4537
                }
4538
            }
4539

4540
            $title = $title == '' ? NULL : $title;
4✔
4541
        }
4542

4543

4544
        return ([$total, $chatcount, $notifcount, $title, $message, array_unique($chatids), $route]);
8✔
4545
    }
4546

4547
    public function hasPermission($perm)
4548
    {
4549
        $perms = $this->user['permissions'];
39✔
4550
        return ($perms && stripos($perms, $perm) !== FALSE);
39✔
4551
    }
4552

4553
    public function sendIt($mailer, $message)
4554
    {
4555
        $mailer->send($message);
34✔
4556
    }
4557

4558
    public function thankDonation()
4559
    {
4560
        try {
4561
            $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/donations');
1✔
4562
            $twig = new \Twig_Environment($loader);
1✔
4563
            list ($transport, $mailer) = Mail::getMailer();
1✔
4564

4565
            $message = \Swift_Message::newInstance()
1✔
4566
                ->setSubject("Thank you for supporting Freegle!")
1✔
4567
                ->setFrom(PAYPAL_THANKS_FROM)
1✔
4568
                ->setReplyTo(PAYPAL_THANKS_FROM)
1✔
4569
                ->setTo($this->getEmailPreferred())
1✔
4570
                ->setBody("Thank you for supporting Freegle!");
1✔
4571

4572
            Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::THANK_DONATION, $this->id);
1✔
4573

4574
            $html = $twig->render('thank.html', [
1✔
4575
                'name' => $this->getName(),
1✔
4576
                'email' => $this->getEmailPreferred(),
1✔
4577
                'unsubscribe' => $this->loginLink(USER_SITE, $this->getId(), "/unsubscribe", NULL)
1✔
4578
            ]);
1✔
4579

4580
            # Add HTML in base-64 as default quoted-printable encoding leads to problems on
4581
            # Outlook.
4582
            $htmlPart = \Swift_MimePart::newInstance();
1✔
4583
            $htmlPart->setCharset('utf-8');
1✔
4584
            $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
4585
            $htmlPart->setContentType('text/html');
1✔
4586
            $htmlPart->setBody($html);
1✔
4587
            $message->attach($htmlPart);
1✔
4588

4589
            Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::THANK_DONATION, $this->getId());
1✔
4590

4591
            $this->sendIt($mailer, $message);
1✔
4592
        } catch (\Exception $e) { error_log("Failed " . $e->getMessage()); };
×
4593
    }
4594

4595
    public function invite($email)
4596
    {
4597
        $ret = FALSE;
9✔
4598

4599
        # We can only invite logged in.
4600
        if ($this->id) {
9✔
4601
            # ...and only if we have spare.
4602
            if ($this->user['invitesleft'] > 0) {
9✔
4603
                # They might already be using us - but they might also have forgotten.  So allow that case.  However if
4604
                # they have actively declined a previous invitation we suppress this one.
4605
                $previous = $this->dbhr->preQuery("SELECT id FROM users_invitations WHERE email = ? AND outcome = ?;", [
9✔
4606
                    $email,
9✔
4607
                    User::INVITE_DECLINED
9✔
4608
                ]);
9✔
4609

4610
                if (count($previous) == 0) {
9✔
4611
                    # The table has a unique key on userid and email, so that means we can only invite the same person
4612
                    # once.  That avoids us pestering them.
4613
                    try {
4614
                        $this->dbhm->preExec("INSERT INTO users_invitations (userid, email) VALUES (?,?);", [
9✔
4615
                            $this->id,
9✔
4616
                            $email
9✔
4617
                        ]);
9✔
4618

4619
                        # We're ok to invite.
4620
                        $fromname = $this->getName();
9✔
4621
                        $frommail = $this->getEmailPreferred();
9✔
4622
                        $url = "https://" . USER_SITE . "/invite/" . $this->dbhm->lastInsertId();
9✔
4623

4624
                        list ($transport, $mailer) = Mail::getMailer();
9✔
4625
                        $message = \Swift_Message::newInstance()
9✔
4626
                            ->setSubject("$fromname has invited you to try Freegle!")
9✔
4627
                            ->setFrom([NOREPLY_ADDR => SITE_NAME])
9✔
4628
                            ->setReplyTo($frommail)
9✔
4629
                            ->setTo($email)
9✔
4630
                            ->setBody("$fromname ($email) thinks you might like Freegle, which helps you give and get things for free near you.  Click $url to try it.");
9✔
4631

4632
                        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::INVITATION);
9✔
4633

4634
                        $html = invite($fromname, $frommail, $url);
9✔
4635

4636
                        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
4637
                        # Outlook.
4638
                        $htmlPart = \Swift_MimePart::newInstance();
9✔
4639
                        $htmlPart->setCharset('utf-8');
9✔
4640
                        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
9✔
4641
                        $htmlPart->setContentType('text/html');
9✔
4642
                        $htmlPart->setBody($html);
9✔
4643
                        $message->attach($htmlPart);
9✔
4644

4645
                        $this->sendIt($mailer, $message);
9✔
4646
                        $ret = TRUE;
9✔
4647

4648
                        $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft - 1 WHERE id = ?;", [
9✔
4649
                            $this->id
9✔
4650
                        ]);
9✔
4651
                    } catch (\Exception $e) {
1✔
4652
                        # Probably a duplicate.
4653
                    }
4654
                }
4655
            }
4656
        }
4657

4658
        return ($ret);
9✔
4659
    }
4660

4661
    public function inviteOutcome($id, $outcome)
4662
    {
4663
        $invites = $this->dbhm->preQuery("SELECT * FROM users_invitations WHERE id = ?;", [
1✔
4664
            $id
1✔
4665
        ]);
1✔
4666

4667
        foreach ($invites as $invite) {
1✔
4668
            if ($invite['outcome'] == User::INVITE_PENDING) {
1✔
4669
                $this->dbhm->preExec("UPDATE users_invitations SET outcome = ?, outcometimestamp = NOW() WHERE id = ?;", [
1✔
4670
                    $outcome,
1✔
4671
                    $id
1✔
4672
                ]);
1✔
4673

4674
                if ($outcome == User::INVITE_ACCEPTED) {
1✔
4675
                    # Give the sender two more invites.  This means that if their invitations are unsuccessful, they will
4676
                    # stall, but if they do ok, they won't.  This isn't perfect - someone could fake up emails and do
4677
                    # successful invitations that way.
4678
                    $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft + 2 WHERE id = ?;", [
1✔
4679
                        $invite['userid']
1✔
4680
                    ]);
1✔
4681
                }
4682
            }
4683
        }
4684
    }
4685

4686
    public function listInvitations($since = "30 days ago")
4687
    {
4688
        $ret = [];
8✔
4689

4690
        # Don't show old invitations - unaccepted ones could languish for ages.
4691
        $mysqltime = date('Y-m-d', strtotime($since));
8✔
4692
        $invites = $this->dbhr->preQuery("SELECT id, email, date, outcome, outcometimestamp FROM users_invitations WHERE userid = ? AND date > '$mysqltime';", [
8✔
4693
            $this->id
8✔
4694
        ]);
8✔
4695

4696
        foreach ($invites as $invite) {
8✔
4697
            # Check if this email is now on the platform.
4698
            $invite['date'] = Utils::ISODate($invite['date']);
7✔
4699
            $invite['outcometimestamp'] = $invite['outcometimestamp'] ? Utils::ISODate($invite['outcometimestamp']) : NULL;
7✔
4700
            $ret[] = $invite;
7✔
4701
        }
4702

4703
        return ($ret);
8✔
4704
    }
4705

4706
    public function getLatLng($usedef = TRUE, $usegroup = TRUE, $blur = Utils::BLUR_NONE)
4707
    {
4708
        $ret = [ 0, 0, NULL ];
174✔
4709

4710
        if ($this->id) {
174✔
4711
            $locs = $this->getLatLngs([ $this->user ], $usedef, $usegroup, FALSE, [ $this->user ]);
174✔
4712
            $loc = $locs[$this->id];
174✔
4713

4714
            if ($loc) {
174✔
4715
                if ($blur && ($loc['lat'] || $loc['lng'])) {
173✔
4716
                    list ($loc['lat'], $loc['lng']) = Utils::blur($loc['lat'], $loc['lng'], $blur);
4✔
4717
                }
4718

4719
                $ret = [ $loc['lat'], $loc['lng'], Utils::presdef('loc', $loc, NULL) ];
173✔
4720
            }
4721
        }
4722

4723
        return $ret;
174✔
4724
    }
4725

4726
    public function getPublicLocations(&$users, $atts = NULL)
4727
    {
4728
        $idsleft = [];
108✔
4729
        
4730
        foreach ($users as $userid => $user) {
108✔
4731
            if (!Utils::pres('info', $user) || !Utils::pres('publiclocation', $user['info'])) {
108✔
4732
                $idsleft[] = $userid;
108✔
4733
            }
4734
        }
4735
        
4736
        $areas = NULL;
108✔
4737
        $membs = NULL;
108✔
4738

4739
        if ($idsleft && count($idsleft)) {
108✔
4740
            # First try to get the location from settings or last location.
4741
            $atts = $atts ? $atts : $this->dbhr->preQuery("SELECT id, settings, lastlocation FROM users WHERE id in (" . implode(',', $idsleft) . ");", NULL, FALSE, FALSE);
108✔
4742

4743
            foreach ($atts as $att) {
108✔
4744
                $loc = NULL;
108✔
4745
                $grp = NULL;
108✔
4746

4747
                $aid = NULL;
108✔
4748
                $lid = NULL;
108✔
4749
                $lat = NULL;
108✔
4750
                $lng = NULL;
108✔
4751

4752
                # Default to nowhere.
4753
                $users[$att['id']]['info']['publiclocation'] = [
108✔
4754
                    'display' => '',
108✔
4755
                    'location' => NULL,
108✔
4756
                    'groupname' => NULL
108✔
4757
                ];
108✔
4758

4759
                if (Utils::pres('settings', $att)) {
108✔
4760
                    $settings = $att['settings'];
23✔
4761
                    $settings = json_decode($settings, TRUE);
23✔
4762

4763
                    if (Utils::pres('mylocation', $settings) && Utils::pres('area', $settings['mylocation'])) {
23✔
4764
                        $loc = $settings['mylocation']['area']['name'];
7✔
4765
                        $lid = $settings['mylocation']['id'];
7✔
4766
                        $lat = $settings['mylocation']['lat'];
7✔
4767
                        $lng = $settings['mylocation']['lng'];
7✔
4768
                    }
4769
                }
4770

4771
                if (!$loc) {
108✔
4772
                    # Get the name of the last area we used.
4773
                    if (is_null($areas)) {
101✔
4774
                        $areas = $this->dbhr->preQuery("SELECT l2.id, l2.name, l2.lat, l2.lng, users.id AS userid FROM locations l1 
101✔
4775
                            INNER JOIN users ON users.lastlocation = l1.id
4776
                            INNER JOIN locations l2 ON l2.id = l1.areaid
4777
                            WHERE users.id IN (" . implode(',', $idsleft) . ");", NULL, FALSE, FALSE);
101✔
4778
                    }
4779

4780
                    foreach ($areas as $area) {
101✔
4781
                        if ($att['id'] ==  $area['userid']) {
25✔
4782
                            $loc = $area['name'];
25✔
4783
                            $lid = $area['id'];
25✔
4784
                            $lat = $area['lat'];
25✔
4785
                            $lng = $area['lng'];
25✔
4786
                        }
4787
                    }
4788
                }
4789

4790
                if (!$lid) {
108✔
4791
                    # Find the group of which we are a member which is closest to our location.  We do this because generally
4792
                    # the number of groups we're in is small and therefore this will be quick, whereas the groupsNear call is
4793
                    # fairly slow.
4794
                    $closestdist = PHP_INT_MAX;
101✔
4795
                    $closestname = NULL;
101✔
4796

4797
                    # Get all the memberships.
4798
                    if (!$membs) {
101✔
4799
                        $sql = "SELECT memberships.userid, groups.id, groups.nameshort, groups.namefull, groups.lat, groups.lng FROM `groups` INNER JOIN memberships ON groups.id = memberships.groupid WHERE memberships.userid IN (" . implode(
101✔
4800
                                ',',
101✔
4801
                                $idsleft
101✔
4802
                            ) . ") ORDER BY added ASC;";
101✔
4803
                        $membs = $this->dbhr->preQuery($sql);
101✔
4804
                    }
4805

4806
                    foreach ($membs as $memb) {
101✔
4807
                        if ($memb['userid'] == $att['id']) {
90✔
4808
                            $dist = \GreatCircle::getDistance($lat, $lng, $memb['lat'], $memb['lng']);
90✔
4809

4810
                            if ($dist < $closestdist) {
90✔
4811
                                $closestdist = $dist;
90✔
4812
                                $closestname = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
90✔
4813
                            }
4814
                        }
4815
                    }
4816

4817
                    if (!is_null($closestname)) {
101✔
4818
                        $grp = $closestname;
90✔
4819

4820
                        # The location name might be in the group name, in which case just use the group.
4821
                        $loc = stripos($grp, $loc) !== FALSE ? NULL : $loc;
90✔
4822
                    }
4823
                }
4824

4825
                if ($loc) {
108✔
4826
                    $display = $loc ? ($loc . ($grp ? ", $grp" : "")) : ($grp ? $grp : '');
32✔
4827

4828
                    $users[$att['id']]['info']['publiclocation'] = [
32✔
4829
                        'display' => $display,
32✔
4830
                        'location' => $loc,
32✔
4831
                        'groupname' => $grp
32✔
4832
                    ];
32✔
4833

4834
                    $idsleft = array_filter($idsleft, function($val) use ($att) {
32✔
4835
                        return($val != $att['id']);
32✔
4836
                    });
32✔
4837
                }
4838
            }
4839

4840
            if (count($idsleft) > 0) {
108✔
4841
                # We have some left which don't have explicit postcodes.  Try for a group name.
4842
                #
4843
                # First check the group we used most recently.
4844
                #error_log("Look for group name only for {$att['id']}");
4845
                $found = [];
101✔
4846
                foreach ($idsleft as $userid) {
101✔
4847
                    $messages = $this->dbhr->preQuery("SELECT subject FROM messages INNER JOIN messages_groups ON messages_groups.msgid = messages.id WHERE fromuser = ? ORDER BY messages.arrival DESC LIMIT 1;", [
101✔
4848
                        $userid
101✔
4849
                    ]);
101✔
4850

4851
                    foreach ($messages as $msg) {
101✔
4852
                        list ($type, $item, $location) = Message::parseSubject($msg['subject']);
63✔
4853

4854
                        if ($item) {
63✔
4855
                            $grp = $location;
44✔
4856

4857
                            // Handle some misformed locations which end up with spurious brackets.
4858
                            $grp = preg_replace('/\(|\)/', '', $grp);
44✔
4859

4860
                            $users[$userid]['info']['publiclocation'] = [
44✔
4861
                                'display' => $grp,
44✔
4862
                                'location' => NULL,
44✔
4863
                                'groupname' => $grp
44✔
4864
                            ];
44✔
4865

4866
                            $found[] = $userid;
44✔
4867
                        }
4868
                    }
4869
                }
4870

4871
                $idsleft = array_diff($idsleft, $found);
101✔
4872
                
4873
                # Now check just membership.
4874
                if (count($idsleft)) {
101✔
4875
                    if (!$membs) {
67✔
4876
                        $sql = "SELECT memberships.userid, groups.id, groups.nameshort, groups.namefull, groups.lat, groups.lng FROM `groups` INNER JOIN memberships ON groups.id = memberships.groupid WHERE memberships.userid IN (" . implode(
22✔
4877
                                ',',
22✔
4878
                                $idsleft
22✔
4879
                            ) . ") ORDER BY added ASC;";
22✔
4880
                        $membs = $this->dbhr->preQuery($sql);
22✔
4881
                    }
4882
                    
4883
                    foreach ($idsleft as $userid) {
67✔
4884
                        # Now check the group we joined most recently.
4885
                        foreach ($membs as $memb) {
67✔
4886
                            if ($memb['userid'] == $userid) {
49✔
4887
                                $grp = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
49✔
4888

4889
                                $users[$userid]['info']['publiclocation'] = [
49✔
4890
                                    'display' => $grp,
49✔
4891
                                    'location' => NULL,
49✔
4892
                                    'groupname' => $grp
49✔
4893
                                ];
49✔
4894
                            }
4895
                        }
4896
                    }
4897
                }
4898
            }
4899
        }
4900
    }
4901

4902
    public function getLatLngs($users, $usedef = TRUE, $usegroup = TRUE, $needgroup = FALSE, $atts = NULL, $blur = NULL)
4903
    {
4904
        $userids = array_filter(array_column($users, 'id'));
175✔
4905
        $ret = [];
175✔
4906

4907
        if ($userids && count($userids)) {
175✔
4908
            $atts = $atts ? $atts : $this->dbhr->preQuery("SELECT id, settings, lastlocation FROM users WHERE id in (" . implode(',', $userids) . ");", NULL, FALSE, FALSE);
175✔
4909

4910
            foreach ($atts as $att) {
175✔
4911
                $lat = NULL;
175✔
4912
                $lng = NULL;
175✔
4913
                $loc = NULL;
175✔
4914

4915
                if (Utils::pres('settings', $att)) {
175✔
4916
                    $settings = $att['settings'];
37✔
4917
                    $settings = json_decode($settings, TRUE);
37✔
4918

4919
                    if (Utils::pres('mylocation', $settings)) {
37✔
4920
                        $lat = $settings['mylocation']['lat'];
33✔
4921
                        $lng = $settings['mylocation']['lng'];
33✔
4922
                        $loc = Utils::presdef('name', $settings['mylocation'], NULL);
33✔
4923
                        #error_log("Got from mylocation $lat, $lng, $loc");
4924
                    }
4925
                }
4926

4927
                if (is_null($lat)) {
175✔
4928
                    $lid = $att['lastlocation'];
156✔
4929

4930
                    if ($lid) {
156✔
4931
                        $l = new Location($this->dbhr, $this->dbhm, $lid);
23✔
4932
                        $lat = $l->getPrivate('lat');
23✔
4933
                        $lng = $l->getPrivate('lng');
23✔
4934
                        $loc = $l->getPrivate('name');
23✔
4935
                        #error_log("Got from last location $lat, $lng, $loc");
4936
                    }
4937
                }
4938

4939
                if (!is_null($lat)) {
175✔
4940
                    $ret[$att['id']] = [
52✔
4941
                        'lat' => $lat,
52✔
4942
                        'lng' => $lng,
52✔
4943
                        'loc' => $loc,
52✔
4944
                    ];
52✔
4945

4946
                    $userids = array_filter($userids, function($id) use ($att) {
52✔
4947
                        return $id != $att['id'];
52✔
4948
                    });
52✔
4949
                }
4950
            }
4951
        }
4952

4953
        if ($userids && count($userids) && $usegroup) {
175✔
4954
            # Still some we haven't handled.  Get the last message posted on a group with a location, if any.
4955
            $membs = $this->dbhr->preQuery("SELECT fromuser AS userid, lat, lng FROM messages WHERE fromuser IN (" . implode(',', $userids) . ") AND lat IS NOT NULL AND lng IS NOT NULL ORDER BY arrival ASC;", NULL, FALSE, FALSE);
149✔
4956
            foreach ($membs as $memb) {
149✔
4957
                $ret[$memb['userid']] = [
3✔
4958
                    'lat' => $memb['lat'],
3✔
4959
                    'lng' => $memb['lng']
3✔
4960
                ];
3✔
4961

4962
                #error_log("Got from last message posted {$memb['lat']}, {$memb['lng']}");
4963

4964
                $userids = array_filter($userids, function($id) use ($memb) {
3✔
4965
                    return $id != $memb['userid'];
3✔
4966
                });
3✔
4967
            }
4968
        }
4969

4970
        if ($userids && count($userids) && $usegroup) {
175✔
4971
            # Still some we haven't handled.  Get the memberships.  Logic will choose most recently joined.
4972
            $membs = $this->dbhr->preQuery("SELECT userid, lat, lng, nameshort, namefull FROM `groups` INNER JOIN memberships ON memberships.groupid = groups.id WHERE userid IN (" . implode(',', $userids) . ") ORDER BY added ASC;", NULL, FALSE, FALSE);
147✔
4973
            foreach ($membs as $memb) {
147✔
4974
                $ret[$memb['userid']] = [
130✔
4975
                    'lat' => $memb['lat'],
130✔
4976
                    'lng' => $memb['lng'],
130✔
4977
                    'group' => Utils::presdef('namefull', $memb, $memb['nameshort'])
130✔
4978
                ];
130✔
4979

4980
                #error_log("Got from membership {$memb['lat']}, {$memb['lng']}, " . Utils::presdef('namefull', $memb, $memb['nameshort']));
4981

4982
                $userids = array_filter($userids, function($id) use ($memb) {
130✔
4983
                    return $id != $memb['userid'];
130✔
4984
                });
130✔
4985
            }
4986
        }
4987

4988
        if ($userids && count($userids)) {
175✔
4989
            # Still some we haven't handled.
4990
            foreach ($userids as $userid) {
25✔
4991
                if ($usedef) {
25✔
4992
                    $ret[$userid] = [
21✔
4993
                        'lat' => 53.9450,
21✔
4994
                        'lng' => -2.5209
21✔
4995
                    ];
21✔
4996
                } else {
4997
                    $ret[$userid] = NULL;
14✔
4998
                }
4999
            }
5000
        }
5001

5002
        if ($needgroup) {
175✔
5003
            # Get a group name.
5004
            $membs = $this->dbhr->preQuery("SELECT userid, nameshort, namefull FROM `groups` INNER JOIN memberships ON memberships.groupid = groups.id WHERE userid IN (" . implode(',', array_filter(array_column($users, 'id'))) . ") ORDER BY added ASC;", NULL, FALSE, FALSE);
7✔
5005
            foreach ($membs as $memb) {
7✔
5006
                $ret[$memb['userid']]['group'] = Utils::presdef('namefull', $memb, $memb['nameshort']);
7✔
5007
            }
5008
        }
5009

5010
        if ($blur) {
175✔
5011
            foreach ($ret as &$memb) {
7✔
5012
                if ($memb['lat'] || $memb['lng']) {
7✔
5013
                    list ($memb['lat'], $memb['lng']) = Utils::blur($memb['lat'], $memb['lng'], $blur);
7✔
5014
                }
5015
            }
5016
        }
5017

5018
        return ($ret);
175✔
5019
    }
5020

5021
    public function isFreegleMod()
5022
    {
5023
        $ret = FALSE;
172✔
5024

5025
        $this->cacheMemberships();
172✔
5026

5027
        foreach ($this->memberships as $mem) {
172✔
5028
            if ($mem['type'] == Group::GROUP_FREEGLE && ($mem['role'] == User::ROLE_OWNER || $mem['role'] == User::ROLE_MODERATOR)) {
145✔
5029
                $ret = TRUE;
43✔
5030
            }
5031
        }
5032

5033
        return ($ret);
172✔
5034
    }
5035

5036
    public function getKudos($id = NULL)
5037
    {
5038
        $id = $id ? $id : $this->id;
1✔
5039
        $kudos = [
1✔
5040
            'userid' => $id,
1✔
5041
            'posts' => 0,
1✔
5042
            'chats' => 0,
1✔
5043
            'newsfeed' => 0,
1✔
5044
            'events' => 0,
1✔
5045
            'vols' => 0,
1✔
5046
            'facebook' => 0,
1✔
5047
            'platform' => 0,
1✔
5048
            'kudos' => 0,
1✔
5049
        ];
1✔
5050

5051
        $kudi = $this->dbhr->preQuery("SELECT * FROM users_kudos WHERE userid = ?;", [
1✔
5052
            $id
1✔
5053
        ]);
1✔
5054

5055
        foreach ($kudi as $k) {
1✔
5056
            $kudos = $k;
1✔
5057
        }
5058

5059
        return ($kudos);
1✔
5060
    }
5061

5062
    public function updateKudos($id = NULL, $force = FALSE)
5063
    {
5064
        $current = $this->getKudos($id);
1✔
5065

5066
        # Only update if we don't have one or it's older than a day.  This avoids repeatedly updating the entry
5067
        # for the same user in some bulk operations.
5068
        if (!Utils::pres('timestamp', $current) || (time() - strtotime($current['timestamp']) > 24 * 60 * 60)) {
1✔
5069
            # We analyse a user's activity and assign them a level.
5070
            #
5071
            # Only interested in activity in the last year.
5072
            $id = $id ? $id : $this->id;
1✔
5073
            $start = date('Y-m-d', strtotime("365 days ago"));
1✔
5074

5075
            # First, the number of months in which they have posted.
5076
            $posts = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM messages WHERE fromuser = ? AND date >= '$start';", [
1✔
5077
                $id
1✔
5078
            ])[0]['count'];
1✔
5079

5080
            # Ditto communicated with people.
5081
            $chats = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM chat_messages WHERE userid = ? AND date >= '$start';", [
1✔
5082
                $id
1✔
5083
            ])[0]['count'];
1✔
5084

5085
            # Newsfeed posts
5086
            $newsfeed = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(timestamp), '-', MONTH(timestamp)))) AS count FROM newsfeed WHERE userid = ? AND added >= '$start';", [
1✔
5087
                $id
1✔
5088
            ])[0]['count'];
1✔
5089

5090
            # Events
5091
            $events = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM communityevents WHERE userid = ? AND added >= '$start';", [
1✔
5092
                $id
1✔
5093
            ])[0]['count'];
1✔
5094

5095
            # Volunteering
5096
            $vols = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM volunteering WHERE userid = ? AND added >= '$start';", [
1✔
5097
                $id
1✔
5098
            ])[0]['count'];
1✔
5099

5100
            # Do they have a Facebook login?
5101
            $facebook = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_logins WHERE userid = ? AND type = ?", [
1✔
5102
                    $id,
1✔
5103
                    User::LOGIN_FACEBOOK
1✔
5104
                ])[0]['count'] > 0;
1✔
5105

5106
            # Have they posted using the platform?
5107
            $platform = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM messages WHERE fromuser = ? AND arrival >= '$start' AND sourceheader = ?;", [
1✔
5108
                    $id,
1✔
5109
                    Message::PLATFORM
1✔
5110
                ])[0]['count'] > 0;
1✔
5111

5112
            $kudos = $posts + $chats + $newsfeed + $events + $vols;
1✔
5113

5114
            if ($kudos > 0 || $force) {
1✔
5115
                # No sense in creating entries which are blank or the same.
5116
                $current = $this->getKudos($id);
1✔
5117

5118
                if ($current['kudos'] != $kudos || $force) {
1✔
5119
                    $this->dbhm->preExec("REPLACE INTO users_kudos (userid, kudos, posts, chats, newsfeed, events, vols, facebook, platform) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", [
1✔
5120
                        $id,
1✔
5121
                        $kudos,
1✔
5122
                        $posts,
1✔
5123
                        $chats,
1✔
5124
                        $newsfeed,
1✔
5125
                        $events,
1✔
5126
                        $vols,
1✔
5127
                        $facebook,
1✔
5128
                        $platform
1✔
5129
                    ], FALSE);
1✔
5130
                }
5131
            }
5132
        }
5133
    }
5134

5135
    public function topKudos($gid, $limit = 10)
5136
    {
5137
        $limit = intval($limit);
1✔
5138

5139
        $kudos = $this->dbhr->preQuery("SELECT users_kudos.* FROM users_kudos INNER JOIN users ON users.id = users_kudos.userid INNER JOIN memberships ON memberships.userid = users_kudos.userid AND memberships.groupid = ? WHERE memberships.role = ? ORDER BY kudos DESC LIMIT $limit;", [
1✔
5140
            $gid,
1✔
5141
            User::ROLE_MEMBER
1✔
5142
        ]);
1✔
5143

5144
        $ret = [];
1✔
5145

5146
        foreach ($kudos as $k) {
1✔
5147
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5148
            $atts = $u->getPublic();
1✔
5149
            $atts['email'] = $u->getEmailPreferred();
1✔
5150

5151
            $thisone = [
1✔
5152
                'user' => $atts,
1✔
5153
                'kudos' => $k
1✔
5154
            ];
1✔
5155

5156
            $ret[] = $thisone;
1✔
5157
        }
5158

5159
        return ($ret);
1✔
5160
    }
5161

5162
    public function possibleMods($gid, $limit = 10)
5163
    {
5164
        # We look for users who are not mods with top kudos who also:
5165
        # - active in last 60 days
5166
        # - not bouncing
5167
        # - using a location which is in the group area
5168
        # - have posted with the platform, as we don't want loyal users of TN or Yahoo.
5169
        # - have a Facebook login, as they are more likely to do publicity.
5170
        $limit = intval($limit);
1✔
5171
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
5172
        $sql = "SELECT users_kudos.* FROM users_kudos INNER JOIN users ON users.id = users_kudos.userid INNER JOIN memberships ON memberships.userid = users_kudos.userid AND memberships.groupid = ? INNER JOIN `groups` ON groups.id = memberships.groupid INNER JOIN locations_spatial ON users.lastlocation = locations_spatial.locationid WHERE memberships.role = ? AND users_kudos.platform = 1 AND users_kudos.facebook = 1 AND ST_Contains(ST_GeomFromText(groups.poly, {$this->dbhr->SRID()}), locations_spatial.geometry) AND bouncing = 0 AND lastaccess >= '$start' ORDER BY kudos DESC LIMIT $limit;";
1✔
5173
        $kudos = $this->dbhr->preQuery($sql, [
1✔
5174
            $gid,
1✔
5175
            User::ROLE_MEMBER
1✔
5176
        ]);
1✔
5177

5178
        $ret = [];
1✔
5179

5180
        foreach ($kudos as $k) {
1✔
5181
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5182
            $atts = $u->getPublic();
1✔
5183
            $atts['email'] = $u->getEmailPreferred();
1✔
5184

5185
            $thisone = [
1✔
5186
                'user' => $atts,
1✔
5187
                'kudos' => $k
1✔
5188
            ];
1✔
5189

5190
            $ret[] = $thisone;
1✔
5191
        }
5192

5193
        return ($ret);
1✔
5194
    }
5195

5196
    public function requestExport($sync = FALSE)
5197
    {
5198
        $tag = Utils::randstr(64);
8✔
5199

5200
        # Flag sync ones as started to avoid window with background thread.
5201
        $sync = $sync ? "NOW()" : "NULL";
8✔
5202
        $this->dbhm->preExec("INSERT INTO users_exports (userid, tag, started) VALUES (?, ?, $sync);", [
8✔
5203
            $this->id,
8✔
5204
            $tag
8✔
5205
        ]);
8✔
5206

5207
        return ([$this->dbhm->lastInsertId(), $tag]);
8✔
5208
    }
5209

5210
    public function export($exportid, $tag)
5211
    {
5212
        $this->dbhm->preExec("UPDATE users_exports SET started = NOW() WHERE id = ? AND tag = ?;", [
7✔
5213
            $exportid,
7✔
5214
            $tag
7✔
5215
        ]);
7✔
5216

5217
        # For GDPR we support the ability for a user to export the data we hold about them.  Key points about this:
5218
        #
5219
        # - It needs to be at a high level of abstraction and understandable by the user, not just a cryptic data
5220
        #   dump.
5221
        # - It needs to include data provided by the user and data observed about the user, but not profiling
5222
        #   or categorisation based on that data.  This means that (for example) we need to return which
5223
        #   groups they have joined, but not whether joining those groups has flagged them up as a potential
5224
        #   spammer.
5225
        $ret = [];
7✔
5226
        error_log("...basic info");
7✔
5227

5228
        # Data in user table.
5229
        $d = [];
7✔
5230
        $d['Our_internal_ID_for_you'] = $this->getPrivate('id');
7✔
5231
        $d['Your_full_name'] = $this->getPrivate('fullname');
7✔
5232
        $d['Your_first_name'] = $this->getPrivate('firstname');
7✔
5233
        $d['Your_last_name'] = $this->getPrivate('lastname');
7✔
5234
        $d['Your_Yahoo_ID'] = $this->getPrivate('yahooid');
7✔
5235
        $d['Your_role_on_the_system'] = $this->getPrivate('systemrole');
7✔
5236
        $d['When_you_joined_the_site'] = Utils::ISODate($this->getPrivate('added'));
7✔
5237
        $d['When_you_last_accessed_the_site'] = Utils::ISODate($this->getPrivate('lastaccess'));
7✔
5238
        $d['When_we_last_checked_for_relevant_posts_for_you'] = Utils::ISODate($this->getPrivate('lastrelevantcheck'));
7✔
5239
        $d['Whether_your_email_is_bouncing'] = $this->getPrivate('bouncing') ? 'Yes' : 'No';
7✔
5240
        $d['Permissions_you_have_on_the_site'] = $this->getPrivate('permissions');
7✔
5241
        $d['Number_of_remaining_invitations_you_can_send_to_other_people'] = $this->getPrivate('invitesleft');
7✔
5242

5243
        $lastlocation = $this->user['lastlocation'];
7✔
5244

5245
        if ($lastlocation) {
7✔
5246
            $l = new Location($this->dbhr, $this->dbhm, $lastlocation);
×
5247
            $d['Last_location_you_posted_from'] = $l->getPrivate('name') . " (" . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
×
5248
        }
5249

5250
        $settings = $this->getPrivate('settings');
7✔
5251

5252
        if ($settings) {
7✔
5253
            $settings = json_decode($settings, TRUE);
7✔
5254

5255
            $location = Utils::presdef('id', Utils::presdef('mylocation', $settings, []), NULL);
7✔
5256

5257
            if ($location) {
7✔
5258
                $l = new Location($this->dbhr, $this->dbhm, $location);
6✔
5259
                $d['Last_location_you_entered'] = $l->getPrivate('name') . ' (' . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
6✔
5260
            }
5261

5262
            $notifications = Utils::pres('notifications', $settings);
7✔
5263

5264
            $d['Notifications']['Send_email_notifications_for_chat_messages'] = Utils::presdef('email', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5265
            $d['Notifications']['Send_email_notifications_of_chat_messages_you_send'] = Utils::presdef('emailmine', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5266
            $d['Notifications']['Send_notifications_for_apps'] = Utils::presdef('app', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5267
            $d['Notifications']['Send_push_notifications_to_web_browsers'] = Utils::presdef('push', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5268
            $d['Notifications']['Send_Facebook_notifications'] = Utils::presdef('facebook', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5269
            $d['Notifications']['Send_emails_about_notifications_on_the_site'] = Utils::presdef('notificationmails', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5270

5271
            $d['Hide_profile_picture'] = Utils::presdef('useprofile', $settings, TRUE) ? 'Yes' : 'No';
7✔
5272

5273
            if ($this->isModerator()) {
7✔
5274
                $d['Show_members_that_you_are_a_moderator'] = Utils::pres('showmod', $settings) ? 'Yes' : 'No';
1✔
5275

5276
                switch (Utils::presdef('modnotifs', $settings, 4)) {
1✔
5277
                    case 24:
1✔
5278
                        $d['Send_notifications_of_active_mod_work'] = 'After 24 hours';
×
5279
                        break;
×
5280
                    case 12:
1✔
5281
                        $d['Send_notifications_of_active_mod_work'] = 'After 12 hours';
×
5282
                        break;
×
5283
                    case 4:
1✔
5284
                        $d['Send_notifications_of_active_mod_work'] = 'After 4 hours';
1✔
5285
                        break;
1✔
5286
                    case 2:
×
5287
                        $d['Send_notifications_of_active_mod_work'] = 'After 2 hours';
×
5288
                        break;
×
5289
                    case 1:
×
5290
                        $d['Send_notifications_of_active_mod_work'] = 'After 1 hours';
×
5291
                        break;
×
5292
                    case 0:
×
5293
                        $d['Send_notifications_of_active_mod_work'] = 'Immediately';
×
5294
                        break;
×
5295
                    case -1:
5296
                        $d['Send_notifications_of_active_mod_work'] = 'Never';
×
5297
                        break;
×
5298
                }
5299

5300
                switch (Utils::presdef('backupmodnotifs', $settings, 12)) {
1✔
5301
                    case 24:
1✔
5302
                        $d['Send_notifications_of_backup_mod_work'] = 'After 24 hours';
×
5303
                        break;
×
5304
                    case 12:
1✔
5305
                        $d['Send_notifications_of_backup_mod_work'] = 'After 12 hours';
1✔
5306
                        break;
1✔
5307
                    case 4:
×
5308
                        $d['Send_notifications_of_backup_mod_work'] = 'After 4 hours';
×
5309
                        break;
×
5310
                    case 2:
×
5311
                        $d['Send_notifications_of_backup_mod_work'] = 'After 2 hours';
×
5312
                        break;
×
5313
                    case 1:
×
5314
                        $d['Send_notifications_of_backup_mod_work'] = 'After 1 hours';
×
5315
                        break;
×
5316
                    case 0:
×
5317
                        $d['Send_notifications_of_backup_mod_work'] = 'Immediately';
×
5318
                        break;
×
5319
                    case -1:
5320
                        $d['Send_notifications_of_backup_mod_work'] = 'Never';
×
5321
                        break;
×
5322
                }
5323

5324
                $d['Show_members_that_you_are_a_moderator'] = Utils::presdef('showmod', $settings, TRUE) ? 'Yes' : 'No';
1✔
5325
            }
5326
        }
5327

5328
        # Invitations.  Only show what we sent; the outcome is not this user's business.
5329
        error_log("...invitations");
7✔
5330
        $invites = $this->listInvitations("1970-01-01");
7✔
5331
        $d['invitations'] = [];
7✔
5332

5333
        foreach ($invites as $invite) {
7✔
5334
            $d['invitations'][] = [
6✔
5335
                'email' => $invite['email'],
6✔
5336
                'date' => Utils::ISODate($invite['date'])
6✔
5337
            ];
6✔
5338
        }
5339

5340
        error_log("...emails");
7✔
5341
        $d['emails'] = $this->getEmails();
7✔
5342

5343
        foreach ($d['emails'] as &$email) {
7✔
5344
            $email['added'] = Utils::ISODate($email['added']);
1✔
5345

5346
            if ($email['validated']) {
1✔
5347
                $email['validated'] = Utils::ISODate($email['validated']);
×
5348
            }
5349
        }
5350

5351
        error_log("...logins");
7✔
5352
        $d['logins'] = $this->dbhr->preQuery("SELECT type, uid, added, lastaccess FROM users_logins WHERE userid = ?;", [
7✔
5353
            $this->id
7✔
5354
        ]);
7✔
5355

5356
        foreach ($d['logins'] as &$dd) {
7✔
5357
            $dd['added'] = Utils::ISODate($dd['added']);
7✔
5358
            $dd['lastaccess'] = Utils::ISODate($dd['lastaccess']);
7✔
5359
        }
5360

5361
        error_log("...memberships");
7✔
5362
        $d['memberships'] = $this->getMemberships();
7✔
5363

5364
        error_log("...memberships history");
7✔
5365
        $sql = "SELECT DISTINCT memberships_history.*, groups.nameshort, groups.namefull FROM memberships_history INNER JOIN `groups` ON memberships_history.groupid = groups.id WHERE userid = ? ORDER BY added ASC;";
7✔
5366
        $membs = $this->dbhr->preQuery($sql, [$this->id]);
7✔
5367
        foreach ($membs as &$memb) {
7✔
5368
            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
7✔
5369
            $memb['namedisplay'] = $name;
7✔
5370
            $memb['added'] = Utils::ISODate($memb['added']);
7✔
5371
        }
5372

5373
        $d['membershipshistory'] = $membs;
7✔
5374

5375
        error_log("...searches");
7✔
5376
        $d['searches'] = $this->dbhr->preQuery("SELECT search_history.date, search_history.term, locations.name AS location FROM search_history LEFT JOIN locations ON search_history.locationid = locations.id WHERE search_history.userid = ? ORDER BY search_history.date ASC;", [
7✔
5377
            $this->id
7✔
5378
        ]);
7✔
5379

5380
        foreach ($d['searches'] as &$s) {
7✔
5381
            $s['date'] = Utils::ISODate($s['date']);
×
5382
        }
5383

5384
        error_log("...alerts");
7✔
5385
        $d['alerts'] = $this->dbhr->preQuery("SELECT subject, responded, response FROM alerts_tracking INNER JOIN alerts ON alerts_tracking.alertid = alerts.id WHERE userid = ? AND responded IS NOT NULL ORDER BY responded ASC;", [
7✔
5386
            $this->id
7✔
5387
        ]);
7✔
5388

5389
        foreach ($d['alerts'] as &$s) {
7✔
5390
            $s['responded'] = Utils::ISODate($s['responded']);
×
5391
        }
5392

5393
        error_log("...donations");
7✔
5394
        $d['donations'] = $this->dbhr->preQuery("SELECT * FROM users_donations WHERE userid = ? ORDER BY timestamp ASC;", [
7✔
5395
            $this->id
7✔
5396
        ]);
7✔
5397

5398
        foreach ($d['donations'] as &$s) {
7✔
5399
            $s['timestamp'] = Utils::ISODate($s['timestamp']);
1✔
5400
        }
5401

5402
        error_log("...bans");
7✔
5403
        $d['bans'] = [];
7✔
5404

5405
        $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE byuser = ?;", [
7✔
5406
            $this->id
7✔
5407
        ]);
7✔
5408

5409
        foreach ($bans as $ban) {
7✔
5410
            $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
5411
            $u = User::get($this->dbhr, $this->dbhm, $ban['userid']);
1✔
5412
            $d['bans'][] = [
1✔
5413
                'date' => Utils::ISODate($ban['date']),
1✔
5414
                'group' => $g->getName(),
1✔
5415
                'email' => $u->getEmailPreferred(),
1✔
5416
                'userid' => $ban['userid']
1✔
5417
            ];
1✔
5418
        }
5419

5420
        error_log("...spammers");
7✔
5421
        $d['spammers'] = $this->dbhr->preQuery("SELECT * FROM spam_users WHERE byuserid = ? ORDER BY added ASC;", [
7✔
5422
            $this->id
7✔
5423
        ]);
7✔
5424

5425
        foreach ($d['spammers'] as &$s) {
7✔
5426
            $s['added'] = Utils::ISODate($s['added']);
×
5427
            $u = User::get($this->dbhr, $this->dbhm, $s['userid']);
×
5428
            $s['email'] = $u->getEmailPreferred();
×
5429
        }
5430

5431
        $d['spamdomains'] = $this->dbhr->preQuery("SELECT domain, date FROM spam_whitelist_links WHERE userid = ?;", [
7✔
5432
            $this->id
7✔
5433
        ]);
7✔
5434

5435
        foreach ($d['spamdomains'] as &$s) {
7✔
5436
            $s['date'] = Utils::ISODate($s['date']);
×
5437
        }
5438

5439
        error_log("...images");
7✔
5440
        $images = $this->dbhr->preQuery("SELECT id, url FROM users_images WHERE userid = ?;", [
7✔
5441
            $this->id
7✔
5442
        ]);
7✔
5443

5444
        $d['images'] = [];
7✔
5445

5446
        foreach ($images as $image) {
7✔
5447
            if (Utils::pres('url', $image)) {
6✔
5448
                $d['images'][] = [
6✔
5449
                    'id' => $image['id'],
6✔
5450
                    'thumb' => $image['url']
6✔
5451
                ];
6✔
5452
            } else {
5453
                $a = new Attachment($this->dbhr, $this->dbhm, NULL, Attachment::TYPE_USER);
×
5454
                $d['images'][] = [
×
5455
                    'id' => $image['id'],
×
5456
                    'thumb' => $a->getPath(TRUE, $image['id'])
×
5457
                ];
×
5458
            }
5459
        }
5460

5461
        error_log("...notifications");
7✔
5462
        $d['notifications'] = $this->dbhr->preQuery("SELECT timestamp, url FROM users_notifications WHERE touser = ? AND seen = 1;", [
7✔
5463
            $this->id
7✔
5464
        ]);
7✔
5465

5466
        foreach ($d['notifications'] as &$n) {
7✔
5467
            $n['timestamp'] = Utils::ISODate($n['timestamp']);
×
5468
        }
5469

5470
        error_log("...addresses");
7✔
5471
        $d['addresses'] = [];
7✔
5472

5473
        $addrs = $this->dbhr->preQuery("SELECT * FROM users_addresses WHERE userid = ?;", [
7✔
5474
            $this->id
7✔
5475
        ]);
7✔
5476

5477
        foreach ($addrs as $addr) {
7✔
5478
            $a = new Address($this->dbhr, $this->dbhm, $addr['id']);
×
5479
            $d['addresses'][] = $a->getPublic();
×
5480
        }
5481

5482
        error_log("...events");
7✔
5483
        $d['communityevents'] = [];
7✔
5484

5485
        $events = $this->dbhr->preQuery("SELECT id FROM communityevents WHERE userid = ?;", [
7✔
5486
            $this->id
7✔
5487
        ]);
7✔
5488

5489
        foreach ($events as $event) {
7✔
5490
            $e = new CommunityEvent($this->dbhr, $this->dbhm, $event['id']);
×
5491
            $d['communityevents'][] = $e->getPublic();
×
5492
        }
5493

5494
        error_log("...volunteering");
7✔
5495
        $d['volunteering'] = [];
7✔
5496

5497
        $events = $this->dbhr->preQuery("SELECT id FROM volunteering WHERE userid = ?;", [
7✔
5498
            $this->id
7✔
5499
        ]);
7✔
5500

5501
        foreach ($events as $event) {
7✔
5502
            $e = new Volunteering($this->dbhr, $this->dbhm, $event['id']);
×
5503
            $d['volunteering'][] = $e->getPublic();
×
5504
        }
5505

5506
        error_log("...comments");
7✔
5507
        $d['comments'] = [];
7✔
5508
        $comms = $this->dbhr->preQuery("SELECT * FROM users_comments WHERE byuserid = ? ORDER BY date ASC;", [
7✔
5509
            $this->id
7✔
5510
        ]);
7✔
5511

5512
        foreach ($comms as &$comm) {
7✔
5513
            $u = User::get($this->dbhr, $this->dbhm, $comm['userid']);
1✔
5514
            $comm['email'] = $u->getEmailPreferred();
1✔
5515
            $comm['date'] = Utils::ISODate($comm['date']);
1✔
5516
            $d['comments'][] = $comm;
1✔
5517
        }
5518

5519
        error_log("...ratings");
7✔
5520
        $d['ratings'] = $this->getRated();
7✔
5521

5522
        error_log("...locations");
7✔
5523
        $d['locations'] = [];
7✔
5524

5525
        $locs = $this->dbhr->preQuery("SELECT * FROM locations_excluded WHERE userid = ?;", [
7✔
5526
            $this->id
7✔
5527
        ]);
7✔
5528

5529
        foreach ($locs as $loc) {
7✔
5530
            $g = Group::get($this->dbhr, $this->dbhm, $loc['groupid']);
×
5531
            $l = new Location($this->dbhr, $this->dbhm, $loc['locationid']);
×
5532
            $d['locations'][] = [
×
5533
                'group' => $g->getName(),
×
5534
                'location' => $l->getPrivate('name'),
×
5535
                'date' => Utils::ISODate($loc['date'])
×
5536
            ];
×
5537
        }
5538

5539
        error_log("...messages");
7✔
5540
        $msgs = $this->dbhr->preQuery("SELECT id FROM messages WHERE fromuser = ? ORDER BY arrival ASC;", [
7✔
5541
            $this->id
7✔
5542
        ]);
7✔
5543

5544
        $d['messages'] = [];
7✔
5545

5546
        foreach ($msgs as $msg) {
7✔
5547
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
×
5548

5549
            # Show all info here even moderator attributes.  This wouldn't normally be shown to users, but none
5550
            # of it is confidential really.
5551
            $thisone = $m->getPublic(FALSE, FALSE, TRUE);
×
5552

5553
            if (count($thisone['groups']) > 0) {
×
5554
                $g = Group::get($this->dbhr, $this->dbhm, $thisone['groups'][0]['groupid']);
×
5555
                $thisone['groups'][0]['namedisplay'] = $g->getName();
×
5556
            }
5557

5558
            $d['messages'][] = $thisone;
×
5559
        }
5560

5561
        # Chats.  Can't use listForUser as that filters on various things and has a ModTools vs FD distinction, and
5562
        # we're interested in information we have provided.  So we get the chats mentioned in the roster (we have
5563
        # provided information about being online) and where we have sent or reviewed a chat message.
5564
        error_log("...chats");
7✔
5565
        $chatids = $this->dbhr->preQuery("SELECT DISTINCT id, latestmessage FROM chat_rooms INNER JOIN (SELECT DISTINCT chatid FROM chat_roster WHERE userid = ? UNION SELECT DISTINCT chatid FROM chat_messages WHERE userid = ? OR reviewedby = ?) t ON t.chatid = chat_rooms.id ORDER BY latestmessage ASC;", [
7✔
5566
            $this->id,
7✔
5567
            $this->id,
7✔
5568
            $this->id
7✔
5569
        ]);
7✔
5570

5571
        $d['chatrooms'] = [];
7✔
5572
        $count = 0;
7✔
5573

5574
        foreach ($chatids as $chatid) {
7✔
5575
            # We don't return the chat name because it's too slow to produce.
5576
            $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid['id']);
6✔
5577
            $thisone = [
6✔
5578
                'id' => $chatid['id'],
6✔
5579
                'name' => $r->getPublic($this)['name'],
6✔
5580
                'messages' => []
6✔
5581
            ];
6✔
5582

5583
            $sql = "SELECT date, lastip FROM chat_roster WHERE `chatid` = ? AND userid = ?;";
6✔
5584
            $roster = $this->dbhr->preQuery($sql, [$chatid['id'], $this->id]);
6✔
5585
            foreach ($roster as $rost) {
6✔
5586
                $thisone['lastip'] = $rost['lastip'];
6✔
5587
                $thisone['date'] = Utils::ISODate($rost['date']);
6✔
5588
            }
5589

5590
            # Get the messages we have sent in this chat.
5591
            $msgs = $this->dbhr->preQuery("SELECT id FROM chat_messages WHERE chatid = ? AND (userid = ? OR reviewedby = ?);", [
6✔
5592
                $chatid['id'],
6✔
5593
                $this->id,
6✔
5594
                $this->id
6✔
5595
            ]);
6✔
5596

5597
            $userlist = NULL;
6✔
5598

5599
            foreach ($msgs as $msg) {
6✔
5600
                $cm = new ChatMessage($this->dbhr, $this->dbhm, $msg['id']);
6✔
5601
                $thismsg = $cm->getPublic(FALSE, $userlist);
6✔
5602

5603
                # Strip out most of the refmsg detail - it's not ours and we need to save volume of data.
5604
                $refmsg = Utils::pres('refmsg', $thismsg);
6✔
5605

5606
                if ($refmsg) {
6✔
5607
                    $thismsg['refmsg'] = [
×
5608
                        'id' => $msg['id'],
×
5609
                        'subject' => Utils::presdef('subject', $refmsg, NULL)
×
5610
                    ];
×
5611
                }
5612

5613
                $thismsg['mine'] = Utils::presdef('userid', $thismsg, NULL) == $this->id;
6✔
5614
                $thismsg['date'] = Utils::ISODate($thismsg['date']);
6✔
5615
                $thisone['messages'][] = $thismsg;
6✔
5616

5617
                $count++;
6✔
5618
//
5619
//                if ($count > 200) {
5620
//                    break 2;
5621
//                }
5622
            }
5623

5624
            if (count($thisone['messages']) > 0) {
6✔
5625
                $d['chatrooms'][] = $thisone;
6✔
5626
            }
5627
        }
5628

5629
        error_log("...newsfeed");
7✔
5630
        $newsfeeds = $this->dbhr->preQuery("SELECT * FROM newsfeed WHERE userid = ?;", [
7✔
5631
            $this->id
7✔
5632
        ]);
7✔
5633

5634
        $d['newsfeed'] = [];
7✔
5635

5636
        foreach ($newsfeeds as $newsfeed) {
7✔
5637
            $n = new Newsfeed($this->dbhr, $this->dbhm, $newsfeed['id']);
6✔
5638
            $thisone = $n->getPublic(FALSE, FALSE, FALSE, FALSE);
6✔
5639
            $d['newsfeed'][] = $thisone;
6✔
5640
        }
5641

5642
        $d['newsfeed_unfollows'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_unfollow WHERE userid = ?;", [
7✔
5643
            $this->id
7✔
5644
        ]);
7✔
5645

5646
        foreach ($d['newsfeed_unfollows'] as &$dd) {
7✔
5647
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5648
        }
5649

5650
        $d['newsfeed_likes'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_likes WHERE userid = ?;", [
7✔
5651
            $this->id
7✔
5652
        ]);
7✔
5653

5654
        foreach ($d['newsfeed_likes'] as &$dd) {
7✔
5655
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5656
        }
5657

5658
        $d['newsfeed_reports'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_reports WHERE userid = ?;", [
7✔
5659
            $this->id
7✔
5660
        ]);
7✔
5661

5662
        foreach ($d['newsfeed_reports'] as &$dd) {
7✔
5663
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5664
        }
5665

5666
        $d['aboutme'] = $this->dbhr->preQuery("SELECT timestamp, text FROM users_aboutme WHERE userid = ? AND LENGTH(text) > 5;", [
7✔
5667
            $this->id
7✔
5668
        ]);
7✔
5669

5670
        foreach ($d['aboutme'] as &$dd) {
7✔
5671
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5672
        }
5673

5674
        error_log("...stories");
7✔
5675
        $d['stories'] = $this->dbhr->preQuery("SELECT date, headline, story FROM users_stories WHERE userid = ?;", [
7✔
5676
            $this->id
7✔
5677
        ]);
7✔
5678

5679
        foreach ($d['stories'] as &$dd) {
7✔
5680
            $dd['date'] = Utils::ISODate($dd['date']);
×
5681
        }
5682

5683
        $d['stories_likes'] = $this->dbhr->preQuery("SELECT storyid FROM users_stories_likes WHERE userid = ?;", [
7✔
5684
            $this->id
7✔
5685
        ]);
7✔
5686

5687
        error_log("...exports");
7✔
5688
        $d['exports'] = $this->dbhr->preQuery("SELECT userid, started, completed FROM users_exports WHERE userid = ?;", [
7✔
5689
            $this->id
7✔
5690
        ]);
7✔
5691

5692
        foreach ($d['exports'] as &$dd) {
7✔
5693
            $dd['started'] = Utils::ISODate($dd['started']);
7✔
5694
            $dd['completed'] = Utils::ISODate($dd['completed']);
7✔
5695
        }
5696

5697
        error_log("...logs");
7✔
5698
        $l = new Log($this->dbhr, $this->dbhm);
7✔
5699
        $ctx = NULL;
7✔
5700
        $d['logs'] = $l->get(NULL, NULL, NULL, NULL, NULL, NULL, PHP_INT_MAX, $ctx, $this->id);
7✔
5701

5702
        error_log("...add group to logs");
7✔
5703
        $loggroups = [];
7✔
5704
        foreach ($d['logs'] as &$log) {
7✔
5705
            if (Utils::pres('groupid', $log)) {
7✔
5706
                # Don't put the whole group info in there, as it is slow to get.
5707
                if (!array_key_exists($log['groupid'], $loggroups)) {
7✔
5708
                    $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
7✔
5709

5710
                    if ($g->getId() == $log['groupid']) {
7✔
5711
                        $loggroups[$log['groupid']] = [
7✔
5712
                            'id' => $log['groupid'],
7✔
5713
                            'nameshort' => $g->getPrivate('nameshort'),
7✔
5714
                            'namedisplay' => $g->getName()
7✔
5715
                        ];
7✔
5716
                    } else {
5717
                        $loggroups[$log['groupid']] = [
×
5718
                            'id' => $log['groupid'],
×
5719
                            'nameshort' => "DeletedGroup{$log['groupid']}",
×
5720
                            'namedisplay' => "Deleted group #{$log['groupid']}"
×
5721
                        ];
×
5722
                    }
5723
                }
5724

5725
                $log['group'] = $loggroups[$log['groupid']];
7✔
5726
            }
5727
        }
5728

5729
        # Gift aid
5730
        $don = new Donations($this->dbhr, $this->dbhm);
7✔
5731
        $d['giftaid'] = $don->getGiftAid($this->id);
7✔
5732

5733
        $ret = $d;
7✔
5734

5735
        # There are some other tables with information which we don't return.  Here's what and why:
5736
        # - Not part of the current UI so can't have any user data
5737
        #     polls_users
5738
        # - Covered by data that we do return from other tables
5739
        #     messages_drafts, messages_history, messages_groups, messages_outcomes,
5740
        #     messages_promises, users_modmails, modnotifs, users_dashboard,
5741
        #     users_nudges
5742
        # - Transient logging data
5743
        #     logs_emails, logs_sql, logs_api, logs_errors, logs_src
5744
        # - Not provided by the user themselves
5745
        #     user_comments, messages_reneged, spam_users, users_banned, users_stories_requested,
5746
        #     users_thanks
5747
        # - Inferred or derived data.  These are not considered to be provided by the user (see p10 of
5748
        #   http://ec.europa.eu/newsroom/document.cfm?doc_id=44099)
5749
        #     users_kudos, visualise
5750

5751
        # Compress the data in the DB because it can be huge.
5752
        #
5753
        error_log("...filter");
7✔
5754
        Utils::filterResult($ret);
7✔
5755
        error_log("...encode");
7✔
5756
        $data = json_encode($ret, JSON_PARTIAL_OUTPUT_ON_ERROR);
7✔
5757
        error_log("...encoded length " . strlen($data) . ", now compress");
7✔
5758
        $data = gzdeflate($data);
7✔
5759
        $this->dbhm->preExec("UPDATE users_exports SET completed = NOW(), data = ? WHERE id = ? AND tag = ?;", [
7✔
5760
            $data,
7✔
5761
            $exportid,
7✔
5762
            $tag
7✔
5763
        ]);
7✔
5764
        error_log("...completed, length " . strlen($data));
7✔
5765

5766
        return ($ret);
7✔
5767
    }
5768

5769
    function getExport($userid, $id, $tag)
5770
    {
5771
        $ret = NULL;
2✔
5772

5773
        $exports = $this->dbhr->preQuery("SELECT * FROM users_exports WHERE userid = ? AND id = ? AND tag = ?;", [
2✔
5774
            $userid,
2✔
5775
            $id,
2✔
5776
            $tag
2✔
5777
        ]);
2✔
5778

5779
        foreach ($exports as $export) {
2✔
5780
            $ret = $export;
2✔
5781
            $ret['requested'] = $ret['requested'] ? Utils::ISODate($ret['requested']) : NULL;
2✔
5782
            $ret['started'] = $ret['started'] ? Utils::ISODate($ret['started']) : NULL;
2✔
5783
            $ret['completed'] = $ret['completed'] ? Utils::ISODate($ret['completed']) : NULL;
2✔
5784

5785
            if ($ret['completed']) {
2✔
5786
                # This has completed.  Return the data.  Will be zapped in cron exports..
5787
                $ret['data'] = json_decode(gzinflate($export['data']), TRUE);
2✔
5788
                $ret['infront'] = 0;
2✔
5789
            } else {
5790
                # Find how many are in front of us.
5791
                $infront = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_exports WHERE id < ? AND completed IS NULL;", [
2✔
5792
                    $id
2✔
5793
                ]);
2✔
5794

5795
                $ret['infront'] = $infront[0]['count'];
2✔
5796
            }
5797
        }
5798

5799
        return ($ret);
2✔
5800
    }
5801

5802
    public function limbo() {
5803
        # We set the deleted attribute, which will cause the v2 Go API not to return any personal data.  The user
5804
        # can still log in, and potentially recover their account by calling session with PATCH of deleted = NULL.
5805
        # Otherwise a background script will purge their account after a couple of weeks.
5806
        #
5807
        # This allows us to handle the fairly common case of users deleting their accounts by mistake, or changing
5808
        # their minds.  This often happens because one-click unsubscribe in emails, which we need to have for
5809
        # delivery.
5810
        $this->dbhm->preExec("UPDATE users SET deleted = NOW() WHERE id = ?;", [
5✔
5811
            $this->id
5✔
5812
        ]);
5✔
5813

5814
        # Send email notification about account removal
5815
        $email = $this->getEmailPreferred();
5✔
5816
        if ($email) {
5✔
5817
            try {
5818
                $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
5✔
5819
                $twig = new \Twig_Environment($loader);
5✔
5820

5821
                list ($transport, $mailer) = Mail::getMailer();
5✔
5822

5823
                $html = $twig->render('limbo.html', [
5✔
5824
                    'site_url' => 'https://' . USER_SITE
5✔
5825
                ]);
5✔
5826

5827
                $message = \Swift_Message::newInstance()
5✔
5828
                    ->setSubject("Your Freegle account has been removed as requested")
5✔
5829
                    ->setFrom([NOREPLY_ADDR => SITE_NAME])
5✔
5830
                    ->setReplyTo(SUPPORT_ADDR)
5✔
5831
                    ->setTo($email)
5✔
5832
                    ->setBody("We've removed your Freegle account as requested. Your personal data has been marked for deletion and will be completely removed from our systems within 14 days.\n\nIf you changed your mind or removed your account by mistake, don't worry! You can still reactivate it by visiting " . USER_SITE . " and logging back in.\n\nOnce you visit the site and log back in, your account will be automatically restored with all your data intact.\n\nIf you meant to remove your account, you can safely ignore this email. After 14 days, your data will be permanently deleted and cannot be recovered.");
5✔
5833

5834
                # Add HTML in base-64 as default quoted-printable encoding leads to problems on
5835
                # Outlook.
5836
                $htmlPart = \Swift_MimePart::newInstance();
5✔
5837
                $htmlPart->setCharset('utf-8');
5✔
5838
                $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
5✔
5839
                $htmlPart->setContentType('text/html');
5✔
5840
                $htmlPart->setBody($html);
5✔
5841
                $message->attach($htmlPart);
5✔
5842

5843
                Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::LIMBO, $this->id);
5✔
5844
                $this->sendIt($mailer, $message);
5✔
5845
            } catch (\Exception $e) {
×
5846
                error_log("Failed to send limbo email to user {$this->id}: " . $e->getMessage());
×
5847
            }
5848
        }
5849
    }
5850

5851
    public function processForgets($id = NULL) {
5852
        $count = 0;
1✔
5853

5854
        $idq = $id ? "AND id = $id" : "";
1✔
5855
        $users = $this->dbhr->preQuery("SELECT id, deleted FROM users WHERE deleted IS NOT NULL AND DATEDIFF(NOW(), deleted) > 14 AND forgotten IS NULL $idq;");
1✔
5856

5857
        foreach ($users as $user) {
1✔
5858
            $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
5859
            $u->forget('Grace period');
1✔
5860
            $count++;
1✔
5861
        }
5862

5863
        return $count;
1✔
5864
    }
5865

5866
    public function forget($reason)
5867
    {
5868
        # Wipe a user of personal data, for the GDPR right to be forgotten.  We don't delete the user entirely
5869
        # otherwise it would mess up the stats.
5870

5871
        # Clear name etc.
5872
        $this->setPrivate('firstname', NULL);
11✔
5873
        $this->setPrivate('lastname', NULL);
11✔
5874
        $this->setPrivate('fullname', "Deleted User #" . $this->id);
11✔
5875
        $this->setPrivate('settings', NULL);
11✔
5876
        $this->setPrivate('yahooid', NULL);
11✔
5877

5878
        # Delete emails which aren't ours.
5879
        $emails = $this->getEmails();
11✔
5880

5881
        foreach ($emails as $email) {
11✔
5882
            if (!$email['ourdomain']) {
8✔
5883
                $this->removeEmail($email['email']);
8✔
5884
            }
5885
        }
5886

5887
        # Delete all logins.
5888
        $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ?;", [
11✔
5889
            $this->id
11✔
5890
        ]);
11✔
5891

5892
        # Delete the content (but not subject) of any messages, and any email header information such as their
5893
        # name and email address.
5894
        $msgs = $this->dbhm->preQuery("SELECT id FROM messages WHERE fromuser = ? AND messages.type IN (?, ?);", [
11✔
5895
            $this->id,
11✔
5896
            Message::TYPE_OFFER,
11✔
5897
            Message::TYPE_WANTED
11✔
5898
        ]);
11✔
5899

5900
        foreach ($msgs as $msg) {
11✔
5901
            $this->dbhm->preExec("UPDATE messages SET fromip = NULL, message = NULL, envelopefrom = NULL, fromname = NULL, fromaddr = NULL, messageid = NULL, textbody = NULL, htmlbody = NULL, deleted = NOW() WHERE id = ?;", [
1✔
5902
                $msg['id']
1✔
5903
            ]);
1✔
5904

5905
            $this->dbhm->preExec("UPDATE messages_groups SET deleted = 1 WHERE msgid = ?;", [
1✔
5906
                $msg['id']
1✔
5907
            ]);
1✔
5908

5909
            # Delete outcome comments that they've added - just about might have personal data.
5910
            $this->dbhm->preExec("UPDATE messages_outcomes SET comments = NULL WHERE msgid = ?;", [
1✔
5911
                $msg['id']
1✔
5912
            ]);
1✔
5913

5914
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
1✔
5915

5916
            if (!$m->hasOutcome()) {
1✔
5917
                $m->withdraw('Withdrawn on user unsubscribe', NULL);
1✔
5918
            }
5919
        }
5920

5921
        # Remove all the content of all chat messages which they have sent (but not received).
5922
        $msgs = $this->dbhm->preQuery("SELECT id FROM chat_messages WHERE userid = ?;", [
11✔
5923
            $this->id
11✔
5924
        ]);
11✔
5925

5926
        foreach ($msgs as $msg) {
11✔
5927
            $this->dbhm->preExec("UPDATE chat_messages SET message = NULL WHERE id = ?;", [
1✔
5928
                $msg['id']
1✔
5929
            ]);
1✔
5930
        }
5931

5932
        # Delete completely any community events, volunteering opportunities, newsfeed posts, searches and stories
5933
        # they have created (their personal details might be in there), and any ratings by or about them.
5934
        $this->dbhm->preExec("DELETE FROM communityevents WHERE userid = ?;", [
11✔
5935
            $this->id
11✔
5936
        ]);
11✔
5937
        $this->dbhm->preExec("DELETE FROM volunteering WHERE userid = ?;", [
11✔
5938
            $this->id
11✔
5939
        ]);
11✔
5940
        $this->dbhm->preExec("DELETE FROM newsfeed WHERE userid = ?;", [
11✔
5941
            $this->id
11✔
5942
        ]);
11✔
5943
        $this->dbhm->preExec("DELETE FROM users_stories WHERE userid = ?;", [
11✔
5944
            $this->id
11✔
5945
        ]);
11✔
5946
        $this->dbhm->preExec("DELETE FROM users_searches WHERE userid = ?;", [
11✔
5947
            $this->id
11✔
5948
        ]);
11✔
5949
        $this->dbhm->preExec("DELETE FROM users_aboutme WHERE userid = ?;", [
11✔
5950
            $this->id
11✔
5951
        ]);
11✔
5952
        $this->dbhm->preExec("DELETE FROM ratings WHERE rater = ?;", [
11✔
5953
            $this->id
11✔
5954
        ]);
11✔
5955
        $this->dbhm->preExec("DELETE FROM ratings WHERE ratee = ?;", [
11✔
5956
            $this->id
11✔
5957
        ]);
11✔
5958

5959
        # Remove them from all groups.
5960
        $membs = $this->getMemberships();
11✔
5961

5962
        foreach ($membs as $memb) {
11✔
5963
            $this->removeMembership($memb['id']);
7✔
5964
        }
5965

5966
        # Delete any postal addresses
5967
        $this->dbhm->preExec("DELETE FROM users_addresses WHERE userid = ?;", [
11✔
5968
            $this->id
11✔
5969
        ]);
11✔
5970

5971
        # Delete any profile images
5972
        $this->dbhm->preExec("DELETE FROM users_images WHERE userid = ?;", [
11✔
5973
            $this->id
11✔
5974
        ]);
11✔
5975

5976
        # Remove any promises.
5977
        $this->dbhm->preExec("DELETE FROM messages_promises WHERE userid = ?;", [
11✔
5978
            $this->id
11✔
5979
        ]);
11✔
5980

5981
        $this->dbhm->preExec("UPDATE users SET forgotten = NOW(), tnuserid = NULL WHERE id = ?;", [
11✔
5982
            $this->id
11✔
5983
        ]);
11✔
5984

5985
        $this->dbhm->preExec("DELETE FROM sessions WHERE userid = ?;", [
11✔
5986
            $this->id
11✔
5987
        ]);
11✔
5988

5989
        $l = new Log($this->dbhr, $this->dbhm);
11✔
5990
        $l->log([
11✔
5991
            'type' => Log::TYPE_USER,
11✔
5992
            'subtype' => Log::SUBTYPE_DELETED,
11✔
5993
            'user' => $this->id,
11✔
5994
            'text' => $reason
11✔
5995
        ]);
11✔
5996
    }
5997

5998
    public function userRetention($userid = NULL)
5999
    {
6000
        # Find users who:
6001
        # - were added six months ago
6002
        # - are not on any groups
6003
        # - have not logged in for six months
6004
        # - are not on the spammer list
6005
        # - do not have mod notes
6006
        # - have no logs for six months
6007
        #
6008
        # We have no good reason to keep any data about them, and should therefore purge them.
6009
        $count = 0;
1✔
6010
        $userq = $userid ? " users.id = $userid AND " : '';
1✔
6011
        $mysqltime = date("Y-m-d", strtotime("6 months ago"));
1✔
6012
        
6013
        # First, delete users with @yahoogroups.com emails (test/old emails)
6014
        $yahoosql = "SELECT DISTINCT users.id FROM users 
1✔
6015
                     INNER JOIN users_emails ON users.id = users_emails.userid 
6016
                     WHERE $userq users_emails.email LIKE '%@yahoogroups.com' AND users.deleted IS NULL;";
1✔
6017
        $yahooUsers = $this->dbhr->preQuery($yahoosql);
1✔
6018
        
6019
        foreach ($yahooUsers as $user) {
1✔
6020
            error_log("Deleting Yahoo Groups user #{$user['id']}");
×
6021
            $u = new User($this->dbhr, $this->dbhm, $user['id']);
×
6022
            $u->delete();
×
6023
            $count++;
×
6024
            
6025
            # Prod garbage collection
6026
            User::clearCache();
×
6027
            gc_collect_cycles();
×
6028
        }
6029
        
6030
        $sql = "SELECT users.id FROM users LEFT JOIN memberships ON users.id = memberships.userid LEFT JOIN spam_users ON users.id = spam_users.userid LEFT JOIN users_comments ON users.id = users_comments.userid WHERE $userq memberships.userid IS NULL AND spam_users.userid IS NULL AND users.lastaccess < '$mysqltime' AND systemrole = ? AND users.deleted IS NULL;";
1✔
6031
        $users = $this->dbhr->preQuery($sql, [
1✔
6032
            User::SYSTEMROLE_USER
1✔
6033
        ]);
1✔
6034

6035
        foreach ($users as $user) {
1✔
6036
            $logs = $this->dbhr->preQuery("SELECT DATEDIFF(NOW(), timestamp) AS logsago FROM logs WHERE user = ? AND (type != ? OR (subtype != ? AND subtype != ?)) ORDER BY id DESC LIMIT 1;", [
1✔
6037
                $user['id'],
1✔
6038
                Log::TYPE_USER,
1✔
6039
                Log::SUBTYPE_CREATED,
1✔
6040
                Log::SUBTYPE_DELETED
1✔
6041
            ]);
1✔
6042

6043
            error_log("#{$user['id']} Found logs " . count($logs) . " age " . (count($logs) > 0 ? $logs['0']['logsago'] : ' none '));
1✔
6044

6045
            if (count($logs) == 0 || $logs[0]['logsago'] > 90) {
1✔
6046
                error_log("...forget user #{$user['id']} " . (count($logs) > 0 ? $logs[0]['logsago'] : ''));
1✔
6047
                $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
6048
                $u->forget('Inactive');
1✔
6049
                $count++;
1✔
6050
            }
6051

6052
            # Prod garbage collection, as we've seen high memory usage by this.
6053
            User::clearCache();
1✔
6054
            gc_collect_cycles();
1✔
6055
        }
6056

6057
        # The only reason for preserving deleted users is as a placeholder user for messages they sent.  If they
6058
        # don't have any messages, they can go.
6059
        $ids = $this->dbhr->preQuery("SELECT users.id FROM `users` LEFT JOIN messages ON messages.fromuser = users.id WHERE users.forgotten IS NOT NULL AND users.lastaccess < ? AND messages.id IS NULL LIMIT 100000;", [
1✔
6060
            $mysqltime
1✔
6061
        ]);
1✔
6062

6063
        $total = count($ids);
1✔
6064
        $count = 0;
1✔
6065

6066
        foreach ($ids as $id) {
1✔
6067
            $u = new User($this->dbhr, $this->dbhm, $id['id']);
1✔
6068
            #error_log("...delete user #{$id['id']}");
6069
            $u->delete();
1✔
6070

6071
            $count++;
1✔
6072

6073
            if ($count % 1000 == 0) {
1✔
6074
                error_log("...delete $count / $total");
×
6075
            }
6076

6077

6078
            # Prod garbage collection, as we've seen high memory usage by this.
6079
            User::clearCache();
1✔
6080
            gc_collect_cycles();
1✔
6081
        }
6082

6083
        return ($count);
1✔
6084
    }
6085

6086
    public function recordActive()
6087
    {
6088
        # We record this on an hourly basis.  Avoid pointless mod ops for cluster health.
6089
        $now = date("Y-m-d H:00:00", time());
2✔
6090
        $already = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ? AND timestamp = ?;", [
2✔
6091
            $this->id,
2✔
6092
            $now
2✔
6093
        ]);
2✔
6094

6095
        if (count($already) == 0) {
2✔
6096
            $this->dbhm->background("INSERT IGNORE INTO users_active (userid, timestamp) VALUES ({$this->id}, '$now');");
2✔
6097
        }
6098
    }
6099

6100
    public function getActive()
6101
    {
6102
        $active = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ?;", [$this->id]);
1✔
6103
        return ($active);
1✔
6104
    }
6105

6106
    public function mostActive($gid, $limit = 20)
6107
    {
6108
        $limit = intval($limit);
1✔
6109
        $earliest = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6110

6111
        $users = $this->dbhr->preQuery("SELECT users_active.userid, COUNT(*) AS count FROM users_active inner join users ON users.id = users_active.userid INNER JOIN memberships ON memberships.userid = users.id WHERE groupid = ? AND systemrole = ? AND timestamp >= ? GROUP BY users_active.userid ORDER BY count DESC LIMIT $limit", [
1✔
6112
            $gid,
1✔
6113
            User::SYSTEMROLE_USER,
1✔
6114
            $earliest
1✔
6115
        ]);
1✔
6116

6117
        $ret = [];
1✔
6118

6119
        foreach ($users as $user) {
1✔
6120
            $u = User::get($this->dbhr, $this->dbhm, $user['userid']);
1✔
6121
            $thisone = $u->getPublic();
1✔
6122
            $thisone['groupid'] = $gid;
1✔
6123
            $thisone['email'] = $u->getEmailPreferred();
1✔
6124

6125
            if (Utils::pres('memberof', $thisone)) {
1✔
6126
                foreach ($thisone['memberof'] as $group) {
1✔
6127
                    if ($group['id'] == $gid) {
1✔
6128
                        $thisone['joined'] = $group['added'];
1✔
6129
                    }
6130
                }
6131
            }
6132

6133
            $ret[] = $thisone;
1✔
6134
        }
6135

6136
        return ($ret);
1✔
6137
    }
6138

6139
    public function setAboutMe($text) {
6140
        $this->dbhm->preExec("INSERT INTO users_aboutme (userid, text) VALUES (?, ?);", [
3✔
6141
            $this->id,
3✔
6142
            $text
3✔
6143
        ]);
3✔
6144

6145
        $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id = {$this->id};");
3✔
6146

6147
        return($this->dbhm->lastInsertId());
3✔
6148
    }
6149

6150
    public function rate($rater, $ratee, $rating, $reason = NULL, $text = NULL) {
6151
        $ret = NULL;
2✔
6152

6153
        if ($rater != $ratee) {
2✔
6154
            # Can't rate yourself.
6155
            $review = $rating == User::RATING_DOWN && $reason && $text;
2✔
6156
            $this->dbhm->preExec("REPLACE INTO ratings (rater, ratee, rating, reason, text, timestamp, reviewrequired) VALUES (?, ?, ?, ?, ?, NOW(), ?);", [
2✔
6157
                $rater,
2✔
6158
                $ratee,
2✔
6159
                $rating,
2✔
6160
                $reason,
2✔
6161
                $text,
2✔
6162
                $review
2✔
6163
            ]);
2✔
6164

6165
            $ret = $this->dbhm->lastInsertId();
2✔
6166

6167
            $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id IN ($rater, $ratee);");
2✔
6168
        }
6169

6170
        return($ret);
2✔
6171
    }
6172

6173
    public function getRatings($uids) {
6174
        $mysqltime = date("Y-m-d", strtotime("Midnight 182 days ago"));
124✔
6175
        $ret = [];
124✔
6176
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
124✔
6177
        $myid = $me ? $me->getId() : NULL;
124✔
6178

6179
        # We show visible ratings, ones we have made ourselves, or those from TN.
6180
        $sql = "SELECT ratee, COUNT(*) AS count, rating FROM ratings WHERE ratee IN (" . implode(',', $uids) . ") AND (timestamp >= '$mysqltime' OR rater = ?) AND (tn_rating_id IS NOT NULL OR rater = ? OR visible = 1) GROUP BY rating, ratee;";
124✔
6181
        $ratings = $this->dbhr->preQuery($sql, [ $myid, $myid ]);
124✔
6182

6183
        foreach ($uids as $uid) {
124✔
6184
            $ret[$uid] = [
124✔
6185
                User::RATING_UP => 0,
124✔
6186
                User::RATING_DOWN => 0,
124✔
6187
                User::RATING_MINE => NULL
124✔
6188
            ];
124✔
6189

6190
            foreach ($ratings as $rate) {
124✔
6191
                if ($rate['ratee'] == $uid) {
1✔
6192
                    $ret[$uid][$rate['rating']] = $rate['count'];
1✔
6193
                }
6194
            }
6195
        }
6196

6197
        $ratings = $this->dbhr->preQuery("SELECT rating, ratee FROM ratings WHERE ratee IN (" . implode(',', $uids) . ") AND rater = ? AND timestamp >= '$mysqltime';", [
124✔
6198
            $myid
124✔
6199
        ]);
124✔
6200

6201
        foreach ($uids as $uid) {
124✔
6202
            if ($myid != $this->id) {
124✔
6203
                # We can't rate ourselves, so don't bother checking.
6204

6205
                foreach ($ratings as $rating) {
80✔
6206
                    if ($rating['ratee'] == $uid) {
1✔
6207
                        $ret[$uid][User::RATING_MINE] = $rating['rating'];
1✔
6208
                    }
6209
                }
6210
            }
6211
        }
6212

6213
        return($ret);
124✔
6214
    }
6215

6216
    public function getAllRatings($since) {
6217
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6218

6219
        $sql = "SELECT * FROM ratings WHERE timestamp >= ? AND visible = 1;";
1✔
6220
        $ratings = $this->dbhr->preQuery($sql, [
1✔
6221
            $mysqltime
1✔
6222
        ]);
1✔
6223

6224
        foreach ($ratings as &$rating) {
1✔
6225
            $rating['timestamp'] = Utils::ISODate($rating['timestamp']);
1✔
6226
        }
6227

6228
        return $ratings;
1✔
6229
    }
6230

6231
    public function getVisibleRatings($unreviewedonly, $since = '7 days ago') {
6232
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
3✔
6233
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
6234

6235
        $modships = $me->getModeratorships(NULL, TRUE);
3✔
6236

6237
        $ret = [];
3✔
6238
        $revq = $unreviewedonly ? " AND reviewrequired = 1" : '';
3✔
6239

6240
        if (count($modships)) {
3✔
6241
            $sql = "SELECT ratings.*, m1.groupid,
3✔
6242
       CASE WHEN u1.fullname IS NOT NULL THEN u1.fullname ELSE CONCAT(u1.firstname, ' ', u1.lastname) END AS raterdisplayname,
6243
       CASE WHEN u2.fullname IS NOT NULL THEN u2.fullname ELSE CONCAT(u2.firstname, ' ', u2.lastname) END AS rateedisplayname
6244
    FROM ratings 
6245
    INNER JOIN memberships m1 ON m1.userid = ratings.rater
6246
    INNER JOIN memberships m2 ON m2.userid = ratings.ratee
6247
    INNER JOIN users u1 ON ratings.rater = u1.id
6248
    INNER JOIN users u2 ON ratings.ratee = u2.id
6249
    WHERE ratings.timestamp >= ? AND 
6250
        m1.groupid IN (" . implode(',', $modships) . ") AND
3✔
6251
        m2.groupid IN (" . implode(',', $modships) . ") AND
3✔
6252
        m1.groupid = m2.groupid AND
6253
        ratings.rating IS NOT NULL 
6254
        $revq    
3✔
6255
        GROUP BY ratings.rater ORDER BY ratings.timestamp DESC;";
3✔
6256

6257
            $ret = $this->dbhr->preQuery($sql, [
3✔
6258
                $mysqltime
3✔
6259
            ]);
3✔
6260

6261
            foreach ($ret as &$r) {
3✔
6262
                $r['timestamp'] = Utils::ISODate($r['timestamp']);
1✔
6263
            }
6264
        }
6265

6266
        return $ret;
3✔
6267
    }
6268

6269
    public function ratingReviewed($ratingid) {
6270
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6271

6272
        $unreviewed = $me->getVisibleRatings(TRUE);
1✔
6273

6274
        foreach ($unreviewed as $r) {
1✔
6275
            if ($r['id'] == $ratingid) {
1✔
6276
                $this->dbhm->preExec("UPDATE ratings SET reviewrequired = 0 WHERE id = ?;", [
1✔
6277
                    $ratingid
1✔
6278
                ]);
1✔
6279
            }
6280
        }
6281
    }
6282

6283
    public function getChanges($since) {
6284
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6285

6286
        $users = $this->dbhr->preQuery("SELECT id, lastupdated FROM users WHERE lastupdated >= ?;", [
1✔
6287
            $mysqltime
1✔
6288
        ]);
1✔
6289

6290
        foreach ($users as &$user) {
1✔
6291
            $user['lastupdated'] = Utils::ISODate($user['lastupdated']);
1✔
6292
        }
6293

6294
        return $users;
1✔
6295
    }
6296

6297
    public function getRated() {
6298
        $rateds = $this->dbhr->preQuery("SELECT * FROM ratings WHERE rater = ?;", [
8✔
6299
            $this->id
8✔
6300
        ]);
8✔
6301

6302
        foreach ($rateds as &$rate) {
8✔
6303
            $rate['timestamp'] = Utils::ISODate($rate['timestamp']);
1✔
6304
        }
6305

6306
        return($rateds);
8✔
6307
    }
6308

6309
    public function getActiveSince($since, $createdbefore, $uid = NULL) {
6310
        $sincetime = date("Y-m-d H:i:s", strtotime($since));
1✔
6311
        $beforetime = date("Y-m-d H:i:s", strtotime($createdbefore));
1✔
6312
        $ids = $uid ? [
1✔
6313
            [
1✔
6314
                'id' => $uid
1✔
6315
            ]
1✔
6316
        ] : $this->dbhr->preQuery("SELECT id FROM users WHERE lastaccess >= ? AND added <= ?;", [
1✔
6317
            $sincetime,
1✔
6318
            $beforetime
1✔
6319
        ]);
1✔
6320

6321
        return(count($ids) ? array_filter(array_column($ids, 'id')) : []);
1✔
6322
    }
6323

6324
    public static function encodeId($id) {
6325
        # We're told that this is affecting our spam rating.  Let's see.
6326
        return '';
9✔
6327
//        $bin = base_convert($id, 10, 2);
6328
//        $bin = str_replace('0', '-', $bin);
6329
//        $bin = str_replace('1', '~', $bin);
6330
//        return($bin);
6331
    }
6332

6333
    public static function decodeId($enc) {
6334
        $enc = trim($enc);
×
6335
        $enc = str_replace('-', '0', $enc);
×
6336
        $enc = str_replace('~', '1', $enc);
×
6337
        $id  = base_convert($enc, 2, 10);
×
6338
        return($id);
×
6339
    }
6340

6341
    public function getCity()
6342
    {
6343
        $city = NULL;
23✔
6344

6345
        # Find the closest town
6346
        list ($lat, $lng, $loc) = $this->getLatLng(FALSE, TRUE);
23✔
6347

6348
        if ($lat || $lng) {
23✔
6349
            $sql = "SELECT id, name, ST_distance(position, ST_GeomFromText('POINT($lng $lat)', {$this->dbhr->SRID()})) AS dist FROM towns WHERE position IS NOT NULL ORDER BY dist ASC LIMIT 1;";
1✔
6350
            #error_log("Get $sql, $lng, $lat");
6351
            $towns = $this->dbhr->preQuery($sql);
1✔
6352

6353
            foreach ($towns as $town) {
1✔
6354
                $city = $town['name'];
1✔
6355
            }
6356
        }
6357

6358
        return([ $city, $lat, $lng ]);
23✔
6359
    }
6360

6361
    public function microVolunteering() {
6362
        // Are we on a group where microvolunteering is enabled.
6363
        $groups = $this->dbhr->preQuery("SELECT memberships.id FROM memberships INNER JOIN `groups` ON groups.id = memberships.groupid WHERE userid = ? AND microvolunteering = 1 LIMIT 1;", [
22✔
6364
            $this->id
22✔
6365
        ]);
22✔
6366

6367
        return count($groups);
22✔
6368
    }
6369

6370
    public function getJobAds() {
6371
        # We want to show a few job ads from nearby.
6372
        $search = NULL;
35✔
6373
        $ret = '<span class="jobads">';
35✔
6374

6375
        list ($lat, $lng) = $this->getLatLng();
35✔
6376

6377
        if ($lat || $lng) {
35✔
6378
            $j = new Jobs($this->dbhr, $this->dbhm);
5✔
6379
            $jobs = $j->query($lat, $lng, 4);
5✔
6380

6381
            foreach ($jobs as $job) {
5✔
6382
                $loc = Utils::presdef('location', $job, '');
3✔
6383
                $title = "{$job['title']}" . ($loc !== ' ' ? " ($loc)" : '');
3✔
6384

6385
                # Link via our site to avoid spam trap warnings.
6386
                $url = "https://" . USER_SITE . "/job/{$job['id']}";
3✔
6387
                $ret .= '<a href="' . $url . '" target="_blank" style="color:black; font-weight:bold;">' . htmlentities($title) . '</a><br />';
3✔
6388
            }
6389
        }
6390

6391
        $ret .= '</span>';
35✔
6392

6393
        return([
35✔
6394
            'location' => $search,
35✔
6395
            'jobs' => $ret
35✔
6396
        ]);
35✔
6397
    }
6398

6399
    public function updateModMails($uid = NULL) {
6400
        # We maintain a count of recent modmails by scanning logs regularly, and pruning old ones.  This means we can
6401
        # find the value in a well-indexed way without the disk overhead of having a two-column index on logs.
6402
        #
6403
        # Ignore logs where the user is the same as the byuser - for example a user can delete their own posts, and we are
6404
        # only interested in things where a mod has done something to another user.
6405
        $mysqltime = date("Y-m-d H:i:s", strtotime("10 minutes ago"));
1✔
6406
        $uidq = $uid ? " AND user = $uid " : '';
1✔
6407

6408
        $logs = $this->dbhr->preQuery("SELECT * FROM logs WHERE timestamp > ? AND ((type = 'Message' AND subtype IN ('Rejected', 'Deleted', 'Replied')) OR (type = 'User' AND subtype IN ('Mailed', 'Rejected', 'Deleted'))) AND byuser != user $uidq;", [
1✔
6409
            $mysqltime
1✔
6410
        ]);
1✔
6411

6412
        foreach ($logs as $log) {
1✔
6413
            $this->dbhm->preExec("INSERT IGNORE INTO users_modmails (userid, logid, timestamp, groupid) VALUES (?,?,?,?);", [
1✔
6414
                $log['user'],
1✔
6415
                $log['id'],
1✔
6416
                $log['timestamp'],
1✔
6417
                $log['groupid']
1✔
6418
            ]);
1✔
6419
        }
6420

6421
        # Prune old ones.
6422
        $mysqltime = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6423
        $uidq2 = $uid ? " AND userid = $uid " : '';
1✔
6424

6425
        $logs = $this->dbhr->preQuery("SELECT id FROM users_modmails WHERE timestamp < ? $uidq2;", [
1✔
6426
            $mysqltime
1✔
6427
        ]);
1✔
6428

6429
        foreach ($logs as $log) {
1✔
6430
            $this->dbhm->preExec("DELETE FROM users_modmails WHERE id = ?;", [ $log['id'] ], FALSE);
×
6431
        }
6432
    }
6433

6434
    public function getModGroupsByActivity() {
6435
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
6436
        $sql = "SELECT COUNT(*) AS count, CASE WHEN namefull IS NOT NULL THEN namefull ELSE nameshort END AS namedisplay FROM messages_groups INNER JOIN `groups` ON groups.id = messages_groups.groupid WHERE approvedby = ? AND arrival >= '$start' AND groups.publish = 1 AND groups.onmap = 1 AND groups.type = 'Freegle' GROUP BY groupid ORDER BY count DESC";
1✔
6437
        return $this->dbhr->preQuery($sql, [
1✔
6438
            $this->id
1✔
6439
        ]);
1✔
6440
    }
6441

6442
    public function related($userlist) {
6443
        $userlist = array_unique($userlist);
2✔
6444

6445
        foreach ($userlist as $user1) {
2✔
6446
            foreach ($userlist as $user2) {
2✔
6447
                if ($user1 && $user2 && $user1 !== $user2) {
2✔
6448
                    # We may be passed user ids which no longer exist.
6449
                    $u1 = User::get($this->dbhr, $this->dbhm, $user1);
2✔
6450
                    $u2 = User::get($this->dbhr, $this->dbhm, $user2);
2✔
6451

6452
                    if ($u1->getId() && $u2->getId() && !$u1->isAdminOrSupport() && !$u2->isAdminOrSupport()) {
2✔
6453
                        $this->dbhm->background("INSERT INTO users_related (user1, user2) VALUES ($user1, $user2) ON DUPLICATE KEY UPDATE timestamp = NOW();");
2✔
6454
                    }
6455
                }
6456
            }
6457
        }
6458
    }
6459

6460
    public function getRelated($userid, $since = "30 days ago") {
6461
        $starttime = date("Y-m-d H:i:s", strtotime($since));
1✔
6462
        $users = $this->dbhr->preQuery("SELECT * FROM users_related WHERE user1 = ? AND timestamp >= '$starttime';", [
1✔
6463
            $userid
1✔
6464
        ]);
1✔
6465

6466
        return ($users);
1✔
6467
    }
6468

6469
    public function listRelated($groupids, &$ctx, $limit = 10) {
6470
        # The < condition ensures we don't duplicate during a single run.
6471
        $limit = intval($limit);
1✔
6472
        $ret = [];
1✔
6473
        $backstop = 100;
1✔
6474

6475
        do {
6476
            $ctx = $ctx ? $ctx : [ 'id'  => NULL ];
1✔
6477

6478
            if ($groupids && count($groupids)) {
1✔
6479
                $ctxq = ($ctx && intval($ctx['id'])) ? (" WHERE id < " . intval($ctx['id'])) : '';
1✔
6480
                $groupq = "(" . implode(',', $groupids) . ")";
1✔
6481
                $sql = "SELECT DISTINCT id, user1, user2 FROM (
1✔
6482
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6483
INNER JOIN memberships ON users_related.user1 = memberships.userid 
6484
INNER JOIN users u1 ON users_related.user1 = u1.id AND u1.deleted IS NULL AND u1.systemrole = 'User'
6485
WHERE 
6486
user1 < user2 AND
6487
notified = 0 AND
6488
memberships.groupid IN $groupq UNION
1✔
6489
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6490
INNER JOIN memberships ON users_related.user2 = memberships.userid 
6491
INNER JOIN users u2 ON users_related.user2 = u2.id AND u2.deleted IS NULL AND u2.systemrole = 'User'
6492
WHERE 
6493
user1 < user2 AND
6494
notified = 0 AND
6495
memberships.groupid IN $groupq 
1✔
6496
) t $ctxq ORDER BY id DESC LIMIT $limit;";
1✔
6497
                $members = $this->dbhr->preQuery($sql);
1✔
6498
            } else {
6499
                $ctxq = ($ctx && intval($ctx['id'])) ? (" AND users_related.id < " . intval($ctx['id'])) : '';
1✔
6500
                $sql = "SELECT DISTINCT users_related.id, user1, user2 FROM users_related INNER JOIN users u1 ON u1.id = users_related.user1 AND u1.deleted IS NULL AND u1.systemrole = 'User' INNER JOIN users u2 ON u2.id = users_related.user2 AND u2.deleted IS NULL AND u2.systemrole = 'User' WHERE notified = 0 AND user1 < user2 $ctxq ORDER BY id DESC LIMIT $limit;";
1✔
6501
                $members = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
1✔
6502
            }
6503

6504
            $uids1 = array_column($members, 'user1');
1✔
6505
            $uids2 = array_column($members, 'user2');
1✔
6506

6507
            $related = [];
1✔
6508
            foreach ($members as $member) {
1✔
6509
                $related[$member['user1']] = $member['user2'];
1✔
6510
                $ctx['id'] = $member['id'];
1✔
6511
            }
6512

6513
            $users = $this->getPublicsById(array_merge($uids1, $uids2));
1✔
6514

6515
            foreach ($users as &$user1) {
1✔
6516
                if (Utils::pres($user1['id'], $related)) {
1✔
6517
                    $thisone = $user1;
1✔
6518

6519
                    foreach ($users as $user2) {
1✔
6520
                        if ($user2['id'] == $related[$user1['id']]) {
1✔
6521
                            $user2['userid'] = $user2['id'];
1✔
6522
                            $thisone['relatedto'] = $user2;
1✔
6523
                            break;
1✔
6524
                        }
6525
                    }
6526

6527
                    $logins = $this->getLogins(FALSE, $thisone['id'], TRUE);
1✔
6528
                    $rellogins = $this->getLogins(FALSE, $thisone['relatedto']['id'], TRUE);
1✔
6529

6530
                    if ($thisone['deleted'] ||
1✔
6531
                        $thisone['relatedto']['deleted'] ||
1✔
6532
                        $thisone['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6533
                        $thisone['relatedto']['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6534
                        !count($logins) ||
1✔
6535
                        !count($rellogins)) {
1✔
6536
                        # No sense in telling people about these.
6537
                        #
6538
                        # If there are n valid login types for one of the users - no way they can log in again so no point notifying.
6539
                        $this->dbhm->preExec("UPDATE users_related SET notified = 1 WHERE (user1 = ? AND user2 = ?) OR (user1 = ? AND user2 = ?);", [
1✔
6540
                            $thisone['id'],
1✔
6541
                            $thisone['relatedto']['id'],
1✔
6542
                            $thisone['relatedto']['id'],
1✔
6543
                            $thisone['id']
1✔
6544
                        ]);
1✔
6545
                    } else {
6546
                        $thisone['userid'] = $thisone['id'];
1✔
6547
                        $thisone['logins'] = $logins;
1✔
6548
                        $thisone['relatedto']['logins'] = $rellogins;
1✔
6549

6550
                        $ret[] = $thisone;
1✔
6551
                    }
6552
                }
6553
            }
6554

6555
            $backstop--;
1✔
6556
        } while ($backstop > 0 && count($ret) < $limit && count($members));
1✔
6557

6558
        return $ret;
1✔
6559
    }
6560

6561
    public function getExpectedReplies($uids, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6562
        # We count replies where the user has been active since the reply was requested, which means they've had
6563
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6564
        #
6565
        # $since here has to match the value in ChatRoom::
6566
        $starttime = date("Y-m-d H:i:s", strtotime($since));
124✔
6567
        $replies = $this->dbhr->preQuery("SELECT COUNT(*) AS count, expectee FROM users_expected INNER JOIN users ON users.id = users_expected.expectee INNER JOIN chat_messages ON chat_messages.id = users_expected.chatmsgid WHERE expectee IN (" . implode(',', $uids) . ") AND chat_messages.date >= '$starttime' AND replyexpected = 1 AND replyreceived = 0 AND TIMESTAMPDIFF(MINUTE, chat_messages.date, users.lastaccess) >= ?", [
124✔
6568
            $grace
124✔
6569
        ]);
124✔
6570

6571
        return($replies);
124✔
6572
    }
6573

6574
    public function listExpectedReplies($uid, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6575
        # We count replies where the user has been active since the reply was requested, which means they've had
6576
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6577
        #
6578
        # $since here has to match the value in ChatRoom::
6579
        $starttime = date("Y-m-d H:i:s", strtotime($since));
21✔
6580
        $replies = $this->dbhr->preQuery("SELECT chatid FROM users_expected INNER JOIN users ON users.id = users_expected.expectee INNER JOIN chat_messages ON chat_messages.id = users_expected.chatmsgid WHERE expectee = ? AND chat_messages.date >= '$starttime' AND replyexpected = 1 AND replyreceived = 0 AND TIMESTAMPDIFF(MINUTE, chat_messages.date, users.lastaccess) > ?", [
21✔
6581
            $uid,
21✔
6582
            $grace
21✔
6583
        ]);
21✔
6584

6585
        $ret = [];
21✔
6586

6587
        if (count($replies)) {
21✔
6588
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6589
            $myid = $me ? $me->getId() : NULL;
1✔
6590

6591
            $r = new ChatRoom($this->dbhr, $this->dbhm);
1✔
6592
            $rooms = $r->fetchRooms(array_column($replies, 'chatid'), $myid, TRUE);
1✔
6593

6594
            foreach ($rooms as $room) {
1✔
6595
                $ret[] = [
1✔
6596
                    'id' => $room['id'],
1✔
6597
                    'name' => $room['name']
1✔
6598
                ];
1✔
6599
            }
6600
        }
6601

6602
        return $ret;
21✔
6603
    }
6604
    
6605
    public function getWorkCounts($groups = NULL) {
6606
        # Tell them what mod work there is.  Similar code in Notifications.
6607
        $ret = [];
26✔
6608
        $total = 0;
26✔
6609

6610
        $national = $this->hasPermission(User::PERM_NATIONAL_VOLUNTEERS);
26✔
6611

6612
        if ($national) {
26✔
6613
            $v = new Volunteering($this->dbhr, $this->dbhm);
1✔
6614
            $ret['pendingvolunteering'] = $v->systemWideCount();
1✔
6615
        }
6616

6617
        $s = new Spam($this->dbhr, $this->dbhm);
26✔
6618
        $spamcounts = $s->collectionCounts();
26✔
6619
        $ret['spammerpendingadd'] = $spamcounts[Spam::TYPE_PENDING_ADD];
26✔
6620
        $ret['spammerpendingremove'] = $spamcounts[Spam::TYPE_PENDING_REMOVE];
26✔
6621

6622
        # Show social actions from last 4 days.
6623
        $ctx = NULL;
26✔
6624
        $f = new GroupFacebook($this->dbhr, $this->dbhm);
26✔
6625
        $ret['socialactions'] = 0; // FB DISABLED = count($f->listSocialActions($ctx,$this));
26✔
6626

6627
        $g = new Group($this->dbhr, $this->dbhm);
26✔
6628
        $ret['popularposts'] = 0; // FB DISABLED count($g->getPopularMessages());
26✔
6629

6630
        if ($this->hasPermission(User::PERM_GIFTAID)) {
26✔
6631
            $d = new Donations($this->dbhr, $this->dbhm);
1✔
6632
            $ret['giftaid'] = $d->countGiftAidReview();
1✔
6633
        }
6634

6635
        if (!$groups) {
26✔
6636
            $groups = $this->getMemberships(FALSE, NULL, TRUE, TRUE, $this->id, FALSE);
11✔
6637
        }
6638

6639
        foreach ($groups as &$group) {
26✔
6640
            if (Utils::pres('work', $group)) {
20✔
6641
                foreach ($group['work'] as $key => $work) {
18✔
6642
                    if (Utils::pres($key, $ret)) {
18✔
6643
                        $ret[$key] += $work;
2✔
6644
                    } else {
6645
                        $ret[$key] = $work;
18✔
6646
                    }
6647
                }
6648
            }
6649
        }
6650

6651
        $s = new Story($this->dbhr, $this->dbhm);
26✔
6652
        $ret['stories'] = $s->getReviewCount(FALSE, $this, $groups);
26✔
6653
        $ret['newsletterstories'] = $this->hasPermission(User::PERM_NEWSLETTER) ? $s->getReviewCount(TRUE) : 0;
26✔
6654

6655
        // All the types of work which are worth nagging about.
6656
        $worktypes = [
26✔
6657
            'pendingvolunteering',
26✔
6658
            'socialactions',
26✔
6659
            'popularposts',
26✔
6660
            'chatreview',
26✔
6661
            'relatedmembers',
26✔
6662
            'stories',
26✔
6663
            'newsletterstories',
26✔
6664
            'pending',
26✔
6665
            'spam',
26✔
6666
            'pendingmembers',
26✔
6667
            'pendingevents',
26✔
6668
            'spammembers',
26✔
6669
            'editreview',
26✔
6670
            'pendingadmins'
26✔
6671
        ];
26✔
6672

6673
        if ($this->isAdminOrSupport()) {
26✔
6674
            $worktypes[] = 'spammerpendingadd';
1✔
6675
            $worktypes[] = 'spammerpendingremove';
1✔
6676
        }
6677

6678
        foreach ($worktypes as $key) {
26✔
6679
            $total += Utils::presdef($key, $ret, 0);
26✔
6680
        }
6681

6682
        $ret['total'] = $total;
26✔
6683

6684
        return $ret;
26✔
6685
    }
6686

6687
    public function ratingVisibility($since = "1 hour ago") {
6688
        $mysqltime = date("Y-m-d", strtotime($since));
1✔
6689

6690
        $ratings = $this->dbhr->preQuery("SELECT * FROM ratings WHERE timestamp >= ?;", [
1✔
6691
            $mysqltime
1✔
6692
        ]);
1✔
6693

6694
        foreach ($ratings as $rating) {
1✔
6695
            # A rating is visible to others if there is a chat between the two members, and
6696
            # - the ratee replied to a post, or
6697
            # - there is at least one message from each of them.
6698
            # This means that has been an exchange substantial enough for the rating not to just be frivolous.  It
6699
            # deliberately excludes interactions on ChitChat, where we have seen some people go a bit overboard on
6700
            # rating people.
6701
            $visible = FALSE;
1✔
6702
            #error_log("Check {$rating['rater']} rating of {$rating['ratee']}");
6703

6704
            $chats = $this->dbhr->preQuery("SELECT id FROM chat_rooms WHERE (user1 = ? AND user2 = ?) OR (user2 = ? AND user1 = ?)", [
1✔
6705
                $rating['rater'],
1✔
6706
                $rating['ratee'],
1✔
6707
                $rating['rater'],
1✔
6708
                $rating['ratee'],
1✔
6709
            ]);
1✔
6710

6711
            foreach ($chats as $chat) {
1✔
6712
                $distincts = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(userid)) AS count FROM chat_messages WHERE chatid = ? AND refmsgid IS NULL AND message IS NOT NULL;", [
1✔
6713
                    $chat['id']
1✔
6714
                ]);
1✔
6715

6716
                if ($distincts[0]['count'] >= 2) {
1✔
6717
                    #error_log("At least one real message from each of them in {$chat['id']}");
6718
                    $visible = TRUE;
1✔
6719
                } else {
6720
                    $replies = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM chat_messages WHERE chatid = ? AND userid = ? AND refmsgid IS NOT NULL AND message IS NOT NULL;", [
1✔
6721
                        $chat['id'],
1✔
6722
                        $rating['ratee']
1✔
6723
                    ]);
1✔
6724

6725
                    if ($replies[0]['count']) {
1✔
6726
                        #error_log("Significant reply from {$rating['ratee']} in {$chat['id']}");
6727
                        $visible = TRUE;
1✔
6728
                    }
6729
                }
6730
            }
6731

6732
            #error_log("Use {$rating['rating']} from {$rating['rater']} ? " . ($visible ? 'yes': 'no'));
6733
            $oldvisible = intval($rating['visible']) ? TRUE : FALSE;
1✔
6734

6735
            if ($visible != $oldvisible) {
1✔
6736
                $this->dbhm->preExec("UPDATE ratings SET visible = ?, timestamp = NOW() WHERE id = ?;", [
1✔
6737
                    $visible,
1✔
6738
                    $rating['id']
1✔
6739
                ]);
1✔
6740
            }
6741
        }
6742
    }
6743

6744
    public function unban($groupid) {
6745
        $this->dbhm->preExec("DELETE FROM users_banned WHERE userid = ? AND groupid = ?;", [
4✔
6746
            $this->id,
4✔
6747
            $groupid
4✔
6748
        ]);
4✔
6749
    }
6750

6751
    public function hasFacebookLogin() {
6752
        $logins = $this->getLogins();
4✔
6753
        $ret = FALSE;
4✔
6754

6755
        foreach ($logins as $login) {
4✔
6756
            if ($login['type'] == User::LOGIN_FACEBOOK) {
4✔
6757
                $ret = TRUE;
1✔
6758
            }
6759
        }
6760

6761
        return $ret;
4✔
6762
    }
6763

6764
    public function memberReview($groupid, $request, $reason) {
6765
        $mysqltime = date('Y-m-d H:i');
6✔
6766

6767
        if ($request) {
6✔
6768
            # Requesting review.  Leave reviewedat unchanged, so that we can use it to avoid asking too
6769
            # frequently.
6770
            $this->setMembershipAtt($groupid, 'reviewreason', $reason);
4✔
6771
            $this->setMembershipAtt($groupid, 'reviewrequestedat', $mysqltime);
4✔
6772
        } else {
6773
            # We have reviewed.  Note that they might have been removed, in which case the set will do nothing.
6774
            $this->setMembershipAtt($groupid, 'reviewrequestedat', NULL);
3✔
6775
            $this->setMembershipAtt($groupid, 'reviewedat', $mysqltime);
3✔
6776
        }
6777
    }
6778

6779
    private function checkSupporterSettings($settings) {
6780
        $ret = TRUE;
78✔
6781

6782
        if ($settings) {
78✔
6783
            $s = json_decode($settings, TRUE);
15✔
6784

6785
            if ($s && array_key_exists('hidesupporter', $s)) {
15✔
6786
                $ret = !$s['hidesupporter'];
1✔
6787
            }
6788
        }
6789

6790
        return $ret;
78✔
6791
    }
6792

6793
    public function getSupporters(&$rets, $users) {
6794
        $idsleft = [];
294✔
6795

6796
        foreach ($rets as $userid => $ret) {
294✔
6797
            if (Utils::pres($userid, $users)) {
255✔
6798
                if (array_key_exists('supporter', $users[$userid])) {
11✔
6799
                    $rets[$userid]['supporter'] = $users[$userid]['supporter'];
3✔
6800
                    $rets[$userid]['donor'] = Utils::presdef('donor', $users[$userid], FALSE);
11✔
6801
                }
6802
            } else {
6803
                $idsleft[] = $userid;
250✔
6804
            }
6805
        }
6806

6807
        if (count($idsleft)) {
294✔
6808
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
250✔
6809
            $myid = $me ? $me->getId() : null;
250✔
6810

6811
            # A supporter is a mod, someone who has donated recently, or done microvolunteering recently.
6812
            if (count($idsleft)) {
250✔
6813
                $start = date('Y-m-d', strtotime("360 days ago"));
250✔
6814
                $info = $this->dbhr->preQuery(
250✔
6815
                    "SELECT DISTINCT users.id AS userid, settings, systemrole FROM users 
250✔
6816
    LEFT JOIN microactions ON users.id = microactions.userid
6817
    LEFT JOIN users_donations ON users_donations.userid = users.id 
6818
    WHERE users.id IN (" . implode(
250✔
6819
                        ',',
250✔
6820
                        $idsleft
250✔
6821
                    ) . ") AND 
250✔
6822
                    (systemrole IN (?, ?, ?) OR microactions.timestamp >= ? OR users_donations.timestamp >= ?);",
250✔
6823
                    [
250✔
6824
                        User::SYSTEMROLE_ADMIN,
250✔
6825
                        User::SYSTEMROLE_SUPPORT,
250✔
6826
                        User::SYSTEMROLE_MODERATOR,
250✔
6827
                        $start,
250✔
6828
                        $start
250✔
6829
                    ]
250✔
6830
                );
250✔
6831

6832
                $found = [];
250✔
6833

6834
                foreach ($info as $i) {
250✔
6835
                    $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
78✔
6836
                    $found[] = $i['userid'];
78✔
6837
                }
6838

6839
                $left = array_diff($idsleft, $found);
250✔
6840

6841
                # If we are one of the users, then we want to return whether we are a donor.
6842
                if (in_array($myid, $idsleft)) {
250✔
6843
                    $left[] = $myid;
143✔
6844
                    $left = array_filter(array_unique($left));
143✔
6845
                }
6846

6847
                if (count($left)) {
250✔
6848
                    $info = $this->dbhr->preQuery(
248✔
6849
                        "SELECT userid, settings, TransactionType FROM users_donations INNER JOIN users ON users_donations.userid = users.id WHERE users_donations.timestamp >= ? AND users_donations.userid IN (" . implode(
248✔
6850
                            ',',
248✔
6851
                            $left
248✔
6852
                        ) . ") GROUP BY TransactionType;",
248✔
6853
                        [
248✔
6854
                            $start
248✔
6855
                        ]
248✔
6856
                    );
248✔
6857

6858
                    foreach ($info as $i) {
248✔
6859
                        $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
3✔
6860

6861
                        if ($i['userid'] == $myid) {
3✔
6862
                            # Only return this info for ourselves, otherwise it's a privacy leak.
6863
                            $rets[$i['userid']]['donor'] = TRUE;
3✔
6864

6865
                            if ($i['TransactionType'] == 'subscr_payment' || $i['TransactionType'] == 'recurring_payment') {
3✔
6866
                                $rets[$i['userid']]['donorrecurring'] = TRUE;
1✔
6867
                            }
6868
                        }
6869
                    }
6870
                }
6871
            }
6872
        }
6873
    }
6874

6875
    public function obfuscateEmail($email) {
6876
        $p = strpos($email, '@');
2✔
6877
        $q = strpos($email, 'privaterelay.appleid.com');
2✔
6878

6879
        if ($q) {
2✔
6880
            $email = 'Your Apple ID';
1✔
6881
        } else {
6882
            # For very short emails, we just show the first character.
6883
            if ($p <= 3) {
2✔
6884
                $email = substr($email, 0, 1) . "***" . substr($email, $p);
1✔
6885
            } else if ($p < 10) {
2✔
6886
                $email = substr($email, 0, 1) . "***" . substr($email, $p - 1);
2✔
6887
            } else {
6888
                $email = substr($email, 0, 3) . "***" . substr($email, $p - 3);
1✔
6889
            }
6890
        }
6891

6892
        return $email;
2✔
6893
    }
6894

6895
    public function setSimpleMail($simplemail) {
6896
        $s = $this->getPrivate('settings');
2✔
6897

6898
        if ($s) {
2✔
6899
            $settings = json_decode($s, TRUE);
1✔
6900
        } else {
6901
            $settings = [];
2✔
6902
        }
6903

6904
        $this->dbhm->beginTransaction();
2✔
6905

6906
        switch ($simplemail) {
6907
            case User::SIMPLE_MAIL_NONE: {
6908
                # No digests, no events/volunteering.
6909
                # No relevant or newsletters.
6910
                # No email notifications.
6911
                # No enagement.
6912
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 0, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
2✔
6913
                    $this->id
2✔
6914
                ]);
2✔
6915

6916
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
2✔
6917
                    $this->id
2✔
6918
                ]);
2✔
6919

6920
                $settings['notifications']['email'] = FALSE;
2✔
6921
                $settings['notifications']['emailmine'] = FALSE;
2✔
6922
                $settings['notificationmails']= FALSE;
2✔
6923
                $settings['engagement'] = FALSE;
2✔
6924
                break;
2✔
6925
            }
6926
            case User::SIMPLE_MAIL_BASIC: {
6927
                # Daily digests, no events/volunteering.
6928
                # No relevant or newsletters.
6929
                # Chat email notifications.
6930
                # No enagement.
6931
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 24, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
1✔
6932
                    $this->id
1✔
6933
                ]);
1✔
6934

6935
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
1✔
6936
                    $this->id
1✔
6937
                ]);
1✔
6938

6939
                $settings['notifications']['email'] = TRUE;
1✔
6940
                $settings['notifications']['emailmine'] = FALSE;
1✔
6941
                $settings['notificationmails']= FALSE;
1✔
6942
                $settings['engagement']= FALSE;
1✔
6943
                break;
1✔
6944
            }
6945
            case User::SIMPLE_MAIL_FULL: {
6946
                # Immediate mails, events/volunteering.
6947
                # Relevant and newsletters.
6948
                # Email notifications.
6949
                # Enagement.
6950
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = -1, eventsallowed = 1, volunteeringallowed = 1 WHERE userid = ?;", [
1✔
6951
                    $this->id
1✔
6952
                ]);
1✔
6953

6954
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 1,  newslettersallowed = 1 WHERE id = ?;", [
1✔
6955
                    $this->id
1✔
6956
                ]);
1✔
6957

6958
                $settings['notifications']['email'] = TRUE;
1✔
6959
                $settings['notifications']['emailmine'] = FALSE;
1✔
6960
                $settings['notificationmails']= TRUE;
1✔
6961
                $settings['engagement']= TRUE;
1✔
6962
                break;
1✔
6963
            }
6964
        }
6965

6966
        $settings['simplemail'] = $simplemail;
2✔
6967

6968
        $this->setPrivate('settings', json_encode($settings));
2✔
6969

6970
        # Holiday no longer exposed so turn off.
6971
        $this->dbhm->preExec("UPDATE users SET onholidaytill = NULL, settings = ? WHERE id = ?;", [
2✔
6972
            json_encode($settings),
2✔
6973
            $this->id
2✔
6974
        ]);
2✔
6975

6976
        $this->dbhm->commit();
2✔
6977
    }
6978

6979
    public function processMemberships($g = NULL) {
6980
        $memberships = $this->dbhr->preQuery("SELECT id FROM `memberships_history` WHERE processingrequired = 1 ORDER BY id ASC;");
6✔
6981

6982
        foreach ($memberships as $membership) {
6✔
6983
            $this->processMembership($membership['id'], $g);
6✔
6984
        }
6985
    }
6986

6987
    public function processMembership($id, $g) {
6988
        $memberships = $this->dbhr->preQuery("SELECT * FROM memberships_history WHERE id = ?;",[
6✔
6989
            $id
6✔
6990
        ]);
6✔
6991

6992
        foreach ($memberships as $membership) {
6✔
6993
            $groupid = $membership['groupid'];
6✔
6994
            $userid = $membership['userid'];
6✔
6995
            $collection = $membership['collection'];
6✔
6996

6997
            $g = $g ? $g : Group::get($this->dbhr, $this->dbhm, $groupid);
6✔
6998

6999
            # The membership didn't already exist.  We might want to send a welcome mail.
7000
            if ($g->getPrivate('welcomemail') && $collection == MembershipCollection::APPROVED && $g->getPrivate('onhere')) {
6✔
7001
                # They are now approved.  We need to send a per-group welcome mail.
7002
                try {
7003
                    $g->sendWelcome($userid, FALSE);
2✔
7004
                } catch (Exception $e) {
×
7005
                    error_log("Welcome failed: " . $e->getMessage());
×
7006
                    \Sentry\captureException($e);
×
7007
                }
7008
            }
7009

7010
            # Check whether this user now counts as a possible spammer.
7011
            $s = new Spam($this->dbhr, $this->dbhm);
6✔
7012
            $s->checkUser($userid, $groupid);
6✔
7013

7014
            # We might have mod notes which require this member to be flagged up.
7015
            $comments = $this->dbhr->preQuery(
6✔
7016
                "SELECT COUNT(*) AS count FROM users_comments WHERE userid = ? AND flag = 1;", [
6✔
7017
                    $userid,
6✔
7018
                ]
6✔
7019
            );
6✔
7020

7021
            if ($comments[0]['count'] > 0) {
6✔
7022
                $this->memberReview($groupid, TRUE, 'Note flagged to other groups');
1✔
7023
            }
7024

7025
            $this->dbhm->preExec("UPDATE memberships_history SET processingrequired = 0 WHERE id = ?", [
6✔
7026
                $id
6✔
7027
            ]);
6✔
7028
        }
7029
    }
7030

7031
    public function getUserKey($id) {
7032
        $key = null;
105✔
7033
        $sql = "SELECT * FROM users_logins WHERE userid = ? AND type = ?;";
105✔
7034
        $logins = $this->dbhr->preQuery($sql, [$id, User::LOGIN_LINK]);
105✔
7035
        foreach ($logins as $login) {
105✔
7036
            $key = $login['credentials'];
31✔
7037
        }
7038

7039
        if (!$key) {
105✔
7040
            $key = Utils::randstr(32);
104✔
7041
            $rc = $this->dbhm->preExec("INSERT INTO users_logins (userid, type, credentials) VALUES (?,?,?);", [
104✔
7042
                $id,
104✔
7043
                User::LOGIN_LINK,
104✔
7044
                $key
104✔
7045
            ]);
104✔
7046
        }
7047

7048
        return $key;
104✔
7049
    }
7050

7051
    public function assignUserToToDonation($email, $userid) {
7052
        $email = trim($email);
528✔
7053

7054
        if (strlen($email)) {
528✔
7055
            # We might have donations made via PayPal using this email address which we can now link to this user.  Do
7056
            # SELECT first to avoid this having to replicate in the cluster.
7057
            $donations = $this->dbhr->preQuery("SELECT id FROM users_donations WHERE Payer = ? AND userid IS NULL;", [
528✔
7058
                $email
528✔
7059
            ]);
528✔
7060

7061
            foreach ($donations as $donation) {
528✔
7062
                // Check if user exists before updating to avoid foreign key constraint violations
7063
                $userExists = $this->dbhr->preQuery("SELECT id FROM users WHERE id = ?;", [$userid]);
156✔
7064
                if (count($userExists) > 0) {
156✔
7065
                    $this->dbhm->preExec("UPDATE users_donations SET userid = ? WHERE id = ?;", [
156✔
7066
                        $userid,
156✔
7067
                        $donation['id']
156✔
7068
                    ]);
156✔
7069
                }
7070
            }
7071
        }
7072
    }
7073
}
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

© 2026 Coveralls, Inc