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

Freegle / iznik-server / a4cc767e-c60d-43a5-a517-3235bba39197

pending completion
a4cc767e-c60d-43a5-a517-3235bba39197

push

circleci

edwh
Fix LoveJunk test on Circle.

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

19645 of 20679 relevant lines covered (95.0%)

32.41 hits per line

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

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

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

9
use Jenssegers\ImageHash\ImageHash;
10
use Twilio\Rest\Client;
11

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

20
    const OPEN_AGE = 90;
21

22
    const KUDOS_NEWBIE = 'Newbie';
23
    const KUDOS_OCCASIONAL = 'Occasional';
24
    const KUDOS_FREQUENT = 'Frequent';
25
    const KUDOS_AVID = 'Avid';
26

27
    const RATING_UP = 'Up';
28
    const RATING_DOWN = 'Down';
29
    const RATING_MINE = 'Mine';
30
    const RATING_UNKNOWN = 'Unknown';
31

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

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

44
    /** @var  $dbhm LoggedPDO */
45
    var $publicatts = array('id', 'firstname', 'lastname', 'fullname', 'systemrole', 'settings', 'yahooid', 'newslettersallowed', 'relevantallowed', 'bouncing', 'added', 'invitesleft', 'onholidaytill');
46

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

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

61
    const HAPPY = 'Happy';
62
    const FINE = 'Fine';
63
    const UNHAPPY = 'Unhappy';
64

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

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

77
    const NOTIFS_EMAIL = 'email';
78
    const NOTIFS_EMAIL_MINE = 'emailmine';
79
    const NOTIFS_PUSH = 'push';
80
    const NOTIFS_FACEBOOK = 'facebook';
81
    const NOTIFS_APP = 'app';
82

83
    const INVITE_PENDING = 'Pending';
84
    const INVITE_ACCEPTED = 'Accepted';
85
    const INVITE_DECLINED = 'Declined';
86

87
    # Traffic sources
88
    const SRC_DIGEST = 'digest';
89
    const SRC_RELEVANT = 'relevant';
90
    const SRC_CHASEUP = 'chaseup';
91
    const SRC_CHASEUP_IDLE = 'beenawhile';
92
    const SRC_CHATNOTIF = 'chatnotif';
93
    const SRC_REPOST_WARNING = 'repostwarn';
94
    const SRC_FORGOT_PASSWORD = 'forgotpass';
95
    const SRC_PUSHNOTIF = 'pushnotif'; // From JS
96
    const SRC_TWITTER = 'twitter';
97
    const SRC_EVENT_DIGEST = 'eventdigest';
98
    const SRC_VOLUNTEERING_DIGEST = 'voldigest';
99
    const SRC_VOLUNTEERING_RENEWAL = 'volrenew';
100
    const SRC_NEWSLETTER = 'newsletter';
101
    const SRC_NOTIFICATIONS_EMAIL = 'notifemail';
102
    const SRC_NEWSFEED_DIGEST = 'newsfeeddigest';
103
    const SRC_NOTICEBOARD = 'noticeboard';
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);
537✔
137
        $this->notif = new PushNotifications($dbhr, $dbhm);
537✔
138
        $this->dbhr = $dbhr;
537✔
139
        $this->dbhm = $dbhm;
537✔
140
        $this->name = 'user';
537✔
141
        $this->user = NULL;
537✔
142
        $this->id = NULL;
537✔
143
        $this->table = 'users';
537✔
144
        $this->spammer = [];
537✔
145

146
        if ($id) {
537✔
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 = ?;";
525✔
149

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

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

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

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

173
                if (!User::$cacheDeleted[$id]) {
488✔
174
                    # And it's not zapped - so we can use it.
175
                    #error_log("Not zapped");
176
                    return ($u);
432✔
177
                } else if (!$testonly) {
465✔
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);
465✔
183
                    #error_log("Fetched $id as " . $u->getId() . " mod " . $u->isModerator());
184

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

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

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

205
        return ($u);
535✔
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) {
671✔
213
            User::$cacheDeleted[$id] = TRUE;
484✔
214
        } else {
215
            User::$cache = [];
671✔
216
            User::$cacheDeleted = [];
671✔
217
        }
218
    }
219

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

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

231
            foreach ($logins as $login) {
250✔
232
                $pw = $this->hashPassword($suppliedpw, Utils::presdef('salt', $login, PASSWORD_SALT));
250✔
233

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

238
                    User::clearCache($this->id);
250✔
239

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

248
                    return (TRUE);
250✔
249
                }
250
            }
251
        }
252

253
        return (FALSE);
4✔
254
    }
255

256
    public function linkLogin($key)
257
    {
258
        $ret = TRUE;
2✔
259

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

264
            $sql = "SELECT users_logins.* FROM users_logins INNER JOIN users ON users.id = users_logins.userid WHERE userid = ? AND type = ? AND credentials = ? AND users.deleted IS NULL;";
2✔
265

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

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

280
                $this->dbhm->preExec("UPDATE users_logins SET lastaccess = NOW() WHERE userid = ? AND type = ?;", [
2✔
281
                    $this->id,
2✔
282
                    User::LOGIN_LINK
283
                ]);
284

285
                $ret = TRUE;
2✔
286
            }
287
        }
288

289
        return ($ret);
2✔
290
    }
291

292
    public function getBounce()
293
    {
294
        return ("bounce-{$this->id}-" . time() . "@" . USER_DOMAIN);
32✔
295
    }
296

297
    public function getName($default = TRUE, $atts = NULL, $forcetn = FALSE)
298
    {
299
        $atts = $atts ? $atts : $this->user;
525✔
300

301
        $name = NULL;
525✔
302

303
        # We may or may not have the knowledge about how the name is split out, depending
304
        # on the sign-in mechanism.
305
        if (Utils::pres('fullname', $atts)) {
525✔
306
            $name = $atts['fullname'];
464✔
307
        } else if (Utils::pres('firstname', $atts) || Utils::pres('lastname', $atts)) {
79✔
308
            $first = Utils::pres('firstname', $atts);
75✔
309
            $last = Utils::pres('lastname', $atts);
75✔
310

311
            $name = $first && $last ? "$first $last" : ($first ? $first : $last);
75✔
312
        }
313

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

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

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

340
        # Stop silly long names.
341
        $name = strlen($name) > 32 ? (substr($name, 0, 32) . '...') : $name;
525✔
342

343
        # Numeric names confuse the client.
344
        $name = is_numeric($name) ? "$name." : $name;
525✔
345

346
        if ($forcetn || !Session::modtools()) {
525✔
347
            $name = User::removeTNGroup($name);
23✔
348
        }
349

350
        return ($name);
525✔
351
    }
352

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

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

366
    public function create($firstname, $lastname, $fullname, $reason = '', $yahooid = NULL)
367
    {
368
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
526✔
369

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

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

390
            # Encourage them to introduce themselves.
391
            $n = new Notifications($this->dbhr, $this->dbhm);
525✔
392
            $n->add(NULL, $id, Notifications::TYPE_ABOUT_ME, NULL, NULL, NULL);
525✔
393

394
            return ($id);
525✔
395
        } else {
396
            return (NULL);
1✔
397
        }
398
    }
399

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

408
        $lengths = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/distinct_word_lengths.json'), true);
5✔
409
        $bigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/word_start_bigrams.json'), true);
5✔
410
        $trigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/trigrams.json'), true);
5✔
411

412
        $pw = '';
5✔
413

414
        do {
415
            $length = \Wordle\array_weighted_rand($lengths);
5✔
416
            $start = \Wordle\array_weighted_rand($bigrams);
5✔
417
            $word = \Wordle\fill_word($start, $length, $trigrams);
5✔
418

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

424
        $pw = strtolower($pw);
5✔
425
        return ($pw);
5✔
426
    }
427

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

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

441
            foreach ($this->emails as &$email) {
339✔
442
                $email['ourdomain'] = Mail::ourDomain($email['email']);
271✔
443
            }
444
        }
445

446
        return ($this->emails);
339✔
447
    }
448

449
    public function getEmailsById($uids) {
450
        $ret = [];
164✔
451

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

456
            foreach ($emails as $email) {
162✔
457
                $email['ourdomain'] = Mail::ourDomain($email['email']);
133✔
458
                $ret[$email['userid']][] = $email;
133✔
459
            }
460
        }
461

462
        return ($ret);
164✔
463
    }
464

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

475
        foreach ($emails as $email) {
330✔
476
            if (!Mail::ourDomain($email['email']) && strpos($email['email'], '@yahoogroups.') === FALSE && strpos($email['email'], GROUP_DOMAIN) === FALSE) {
262✔
477
                $ret = $email['email'];
249✔
478
                break;
249✔
479
            }
480
        }
481

482
        return ($ret);
330✔
483
    }
484

485
    public function getOurEmail($emails = NULL)
486
    {
487
        $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✔
488
            [$this->id]);
2✔
489
        $ret = NULL;
2✔
490

491
        foreach ($emails as $email) {
2✔
492
            if (Mail::ourDomain($email['email'])) {
2✔
493
                $ret = $email['email'];
1✔
494
                break;
1✔
495
            }
496
        }
497

498
        return ($ret);
2✔
499
    }
500

501
    public function getAnEmailId()
502
    {
503
        $emails = $this->dbhr->preQuery("SELECT id FROM users_emails WHERE userid = ? ORDER BY preferred DESC;",
7✔
504
            [$this->id]);
7✔
505
        return (count($emails) == 0 ? NULL : $emails[0]['id']);
7✔
506
    }
507

508
    public function isApprovedMember($groupid)
509
    {
510
        $membs = $this->dbhr->preQuery("SELECT id FROM memberships WHERE userid = ? AND groupid = ? AND collection = 'Approved';", [$this->id, $groupid]);
166✔
511
        return (count($membs) > 0 ? $membs[0]['id'] : NULL);
166✔
512
    }
513

514
    public function getEmailAge($email)
515
    {
516
        $emails = $this->dbhr->preQuery("SELECT TIMESTAMPDIFF(HOUR, added, NOW()) AS ago FROM users_emails WHERE email LIKE ?;", [
1✔
517
            $email
518
        ]);
519

520
        return (count($emails) > 0 ? $emails[0]['ago'] : NULL);
1✔
521
    }
522

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

531
        foreach ($ids as $id) {
19✔
532
            return ($id);
19✔
533
        }
534

535
        return (NULL);
1✔
536
    }
537

538
    public function getEmailById($id)
539
    {
540
        $emails = $this->dbhr->preQuery("SELECT email FROM users_emails WHERE id = ?;", [
1✔
541
            $id
542
        ]);
543

544
        $ret = NULL;
1✔
545

546
        foreach ($emails as $email) {
1✔
547
            $ret = $email['email'];
1✔
548
        }
549

550
        return ($ret);
1✔
551
    }
552

553
    public function findByTNId($id) {
554
        $ret = NULL;
4✔
555

556
        $users = $this->dbhr->preQuery("SELECT id FROM users WHERE tnuserid = ?;", [
4✔
557
            $id
558
        ]);
559

560
        foreach ($users as $user) {
4✔
561
            $ret = $user['id'];
×
562
        }
563

564
        return $ret;
4✔
565
    }
566

567
    public function findByEmail($email) {
568
        return $this->findByEmailIncludingUnvalidated($email)[0];
231✔
569
    }
570

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

581
            foreach ($users as $user) {
25✔
582
                return [ $user['id'], FALSE ];
4✔
583
            }
584
        }
585

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

595
        foreach ($users as $user) {
250✔
596
            if ($user['userid']) {
230✔
597
                return [ $user['userid'], FALSE ];
229✔
598
            } else {
599
                // This email is not yet validated.
600
                return [ NULL, TRUE ];
1✔
601
            }
602
        }
603

604
        return [ NULL, FALSE ];
163✔
605
    }
606

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

615
        foreach ($users as $user) {
1✔
616
            return ($user['userid']);
1✔
617
        }
618

619
        return (NULL);
1✔
620
    }
621

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

628
        foreach ($users as $user) {
9✔
629
            return ($user['id']);
6✔
630
        }
631

632
        return (NULL);
4✔
633
    }
634

635
    public static function canonMail($email)
636
    {
637
        # Googlemail is Gmail really in US and UK.
638
        $email = str_replace('@googlemail.', '@gmail.', $email);
421✔
639
        $email = str_replace('@googlemail.co.uk', '@gmail.co.uk', $email);
421✔
640

641
        # Canonicalise TN addresses.
642
        if (preg_match('/(.*)\-(.*)(@user.trashnothing.com)/', $email, $matches)) {
421✔
643
            $email = $matches[1] . $matches[3];
4✔
644
        }
645

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

654
        # Remove dots in LHS, which are ignored by gmail and can therefore be used to give the appearance of separate
655
        # emails.
656
        $p = strpos($email, '@');
421✔
657

658
        if ($p !== FALSE) {
421✔
659
            $lhs = substr($email, 0, $p);
420✔
660
            $rhs = substr($email, $p);
420✔
661

662
            if (stripos($rhs, '@gmail') !== FALSE || stripos($rhs, '@googlemail') !== FALSE) {
420✔
663
                $lhs = str_replace('.', '', $lhs);
3✔
664
            }
665

666
            # Remove dots from the RHS - saves a little space and is the format we have historically used.
667
            # Very unlikely to introduce ambiguity.
668
            $email = $lhs . str_replace('.', '', $rhs);
420✔
669
        }
670

671
        return ($email);
421✔
672
    }
673

674
    public function addEmail($email, $primary = 1, $changeprimary = TRUE)
675
    {
676
        $email = trim($email);
420✔
677

678
        # Invalidate cache.
679
        $this->emails = NULL;
420✔
680

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

697
            # Don't cache - lots of emails so don't want to flood the query cache.
698
            $sql = "SELECT SQL_NO_CACHE id, preferred FROM users_emails WHERE userid = ? AND email = ?;";
418✔
699
            $emails = $this->dbhm->preQuery($sql, [
418✔
700
                $this->id,
418✔
701
                $email
702
            ]);
703

704
            if (count($emails) == 0) {
418✔
705
                $sql = "INSERT IGNORE INTO users_emails (userid, email, preferred, canon, backwards) VALUES (?, ?, ?, ?, ?)";
418✔
706
                $rc = $this->dbhm->preExec($sql,
418✔
707
                    [$this->id, $email, $primary, $canon, strrev($canon)]);
418✔
708
                $rc = $this->dbhm->lastInsertId();
418✔
709

710
                if ($rc && $primary) {
418✔
711
                    # Make sure no other email is flagged as primary
712
                    $this->dbhm->preExec("UPDATE users_emails SET preferred = 0 WHERE userid = ? AND id != ?;", [
412✔
713
                        $this->id,
412✔
714
                        $rc
715
                    ]);
716
                }
717
            } else {
718
                $rc = $emails[0]['id'];
24✔
719

720
                if ($changeprimary && $primary != $emails[0]['preferred']) {
24✔
721
                    # Change in status.
722
                    $this->dbhm->preExec("UPDATE users_emails SET preferred = ? WHERE id = ?;", [
3✔
723
                        $primary,
724
                        $rc
725
                    ]);
726
                }
727

728
                if ($primary) {
24✔
729
                    # Make sure no other email is flagged as primary
730
                    $this->dbhm->preExec("UPDATE users_emails SET preferred = 0 WHERE userid = ? AND id != ?;", [
19✔
731
                        $this->id,
19✔
732
                        $rc
733
                    ]);
734

735
                    # If we've set an email we might no longer be bouncing.
736
                    $this->unbounce($rc, FALSE);
19✔
737
                }
738
            }
739
        }
740

741
        # We might have donations made via PayPal using this email address which we can now link to this user.  Do
742
        # SELECT first to avoid this having to replicate in the cluster.
743
        $donations = $this->dbhr->preQuery("SELECT id FROM users_donations WHERE Payer = ? AND userid IS NULL;", [
420✔
744
            $email
745
        ]);
746

747
        foreach ($donations as $donation) {
420✔
748
            $this->dbhm->preExec("UPDATE users_donations SET userid = ? WHERE id = ?;", [
141✔
749
                $this->id,
141✔
750
                $donation['id']
141✔
751
            ]);
752
        }
753

754
        return ($rc);
420✔
755
    }
756

757
    public function unbounce($emailid, $log)
758
    {
759
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
21✔
760
        $myid = $me ? $me->getId() : NULL;
21✔
761

762
        if ($log) {
21✔
763
            $l = new Log($this->dbhr, $this->dbhm);
2✔
764

765
            $l->log([
2✔
766
                'type' => Log::TYPE_USER,
767
                'subtype' => Log::SUBTYPE_UNBOUNCE,
768
                'user' => $this->id,
2✔
769
                'byuser' => $myid
770
            ]);
771
        }
772

773
        if ($emailid) {
21✔
774
            $this->dbhm->preExec("UPDATE bounces_emails SET reset = 1 WHERE emailid = ?;", [$emailid]);
21✔
775
        }
776

777
        $this->dbhm->preExec("UPDATE users SET bouncing = 0 WHERE id = ?;", [$this->id]);
21✔
778
    }
779

780
    public function removeEmail($email)
781
    {
782
        # Invalidate cache.
783
        $this->emails = NULL;
9✔
784

785
        $rc = $this->dbhm->preExec("DELETE FROM users_emails WHERE userid = ? AND email = ?;",
9✔
786
            [$this->id, $email]);
9✔
787
        return ($rc);
9✔
788
    }
789

790
    private function updateSystemRole($role)
791
    {
792
        #error_log("Update systemrole $role on {$this->id}");
793
        User::clearCache($this->id);
421✔
794

795
        if ($role == User::ROLE_MODERATOR || $role == User::ROLE_OWNER) {
421✔
796
            $sql = "UPDATE users SET systemrole = ? WHERE id = ? AND systemrole = ?;";
149✔
797
            $this->dbhm->preExec($sql, [User::SYSTEMROLE_MODERATOR, $this->id, User::SYSTEMROLE_USER]);
149✔
798
            $this->user['systemrole'] = $this->user['systemrole'] == User::SYSTEMROLE_USER ?
149✔
799
                User::SYSTEMROLE_MODERATOR : $this->user['systemrole'];
149✔
800
        } else if ($this->user['systemrole'] == User::SYSTEMROLE_MODERATOR) {
399✔
801
            # Check that we are still a mod on a group, otherwise we need to demote ourselves.
802
            $sql = "SELECT id FROM memberships WHERE userid = ? AND role IN (?,?);";
33✔
803
            $roles = $this->dbhr->preQuery($sql, [
33✔
804
                $this->id,
33✔
805
                User::ROLE_MODERATOR,
806
                User::ROLE_OWNER
807
            ]);
808

809
            if (count($roles) == 0) {
33✔
810
                $sql = "UPDATE users SET systemrole = ? WHERE id = ?;";
29✔
811
                $this->dbhm->preExec($sql, [User::SYSTEMROLE_USER, $this->id]);
29✔
812
                $this->user['systemrole'] = User::SYSTEMROLE_USER;
29✔
813
            }
814
        }
815
    }
816

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

830
    public function isBanned($groupid) {
831
        $sql = "SELECT * FROM users_banned WHERE userid = ? AND groupid = ?;";
421✔
832
        $banneds = $this->dbhr->preQuery($sql, [
421✔
833
            $this->id,
421✔
834
            $groupid
835
        ]);
836

837
        foreach ($banneds as $banned) {
421✔
838
            return TRUE;
3✔
839
        }
840

841
        return FALSE;
421✔
842
    }
843

844
    public function addMembership($groupid, $role = User::ROLE_MEMBER, $emailid = NULL, $collection = MembershipCollection::APPROVED, $message = NULL, $byemail = NULL, $addedhere = TRUE)
845
    {
846
        $this->memberships = NULL;
421✔
847
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
421✔
848
        $g = Group::get($this->dbhr, $this->dbhm, $groupid);
421✔
849

850
        Session::clearSessionCache();
421✔
851

852
        # Check if we're banned
853
        if ($this->isBanned($groupid)) {
421✔
854
            return FALSE;
3✔
855
        }
856

857
        $existing = $this->dbhm->preQuery("SELECT COUNT(*) AS count FROM memberships WHERE userid = ? AND groupid = ? AND collection = ?;", [
421✔
858
            $this->id,
421✔
859
            $groupid,
860
            $collection
861
        ]);
862

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

868
        $emailfrequency = 24;
421✔
869
        $eventsallowed = 1;
421✔
870
        $volunteeringallowed = 1;
421✔
871

872
        switch ($simplemail) {
873
            case User::SIMPLE_MAIL_NONE: {
874
                $emailfrequency = 0;
1✔
875
                $eventsallowed = 0;
1✔
876
                $volunteeringallowed = 0;
1✔
877
                break;
1✔
878
            }
879

880
            case User::SIMPLE_MAIL_BASIC: {
881
                $emailfrequency = 24;
×
882
                $eventsallowed = 0;
×
883
                $volunteeringallowed = 0;
×
884
                break;
×
885
            }
886

887
            case User::SIMPLE_MAIL_FULL: {
888
                $emailfrequency = -1;
×
889
                $eventsallowed = 1;
×
890
                $volunteeringallowed = 1;
×
891
                break;
×
892
            }
893
        }
894

895
        $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 = ?;", [
421✔
896
            $this->id,
421✔
897
            $groupid,
898
            $role,
899
            $collection,
900
            $emailfrequency,
901
            $eventsallowed,
902
            $volunteeringallowed,
903
            $role,
904
            $collection,
905
            $emailfrequency,
906
            $eventsallowed,
907
            $volunteeringallowed
908
        ]);
909

910
        $membershipid = $this->dbhm->lastInsertId();
421✔
911

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

915
        # Record the operation for abuse detection.
916
        $this->dbhm->preExec("INSERT INTO memberships_history (userid, groupid, collection) VALUES (?,?,?);", [
421✔
917
            $this->id,
421✔
918
            $groupid,
919
            $collection
920
        ]);
921

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

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

938
            Mail::addHeaders($message, Mail::WELCOME);
939
            $this->sendIt($mailer, $message);
940
        }
941
        // @codeCoverageIgnoreEnd
942

943
        if ($added) {
421✔
944
            # The membership didn't already exist.  We might want to send a welcome mail.
945
            $atts = $g->getPublic();
421✔
946

947
            if (($addedhere) && ($atts['welcomemail'] || $message) && $collection == MembershipCollection::APPROVED && $g->getPrivate('onhere')) {
421✔
948
                # They are now approved.  We need to send a per-group welcome mail.
949
                $this->sendWelcome($message ? $message : $atts['welcomemail'], $groupid, $g, $atts);
3✔
950
            }
951

952
            $l = new Log($this->dbhr, $this->dbhm);
421✔
953
            $l->log([
421✔
954
                'type' => Log::TYPE_GROUP,
955
                'subtype' => Log::SUBTYPE_JOINED,
956
                'user' => $this->id,
421✔
957
                'byuser' => $me ? $me->getId() : NULL,
421✔
958
                'groupid' => $groupid
959
            ]);
960
        }
961

962
        # Check whether this user now counts as a possible spammer.
963
        $s = new Spam($this->dbhr, $this->dbhm);
421✔
964
        $s->checkUser($this->id, $groupid);
421✔
965

966
        # We might have mod notes which require this member to be flagged up.
967
        $comments = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_comments WHERE userid = ? AND flag = 1;", [
421✔
968
            $this->id,
421✔
969
        ]);
970

971
        if ($comments[0]['count'] > 0) {
421✔
972
            $this->memberReview($groupid, TRUE, 'Note flagged to other groups');
1✔
973
        }
974

975
        return ($rc);
421✔
976
    }
977

978
    public function sendWelcome($welcome, $gid, $g = NULL, $atts = NULL, $review = FALSE) {
979
        $g = $g ? $g : Group::get($this->dbhr, $this->dbhm, $gid);
4✔
980
        $atts = $atts ? $atts : $g->getPublic();
4✔
981

982
        $to = $this->getEmailPreferred();
4✔
983

984
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/welcome');
4✔
985
        $twig = new \Twig_Environment($loader);
4✔
986

987
        $html = $twig->render('group.html', [
4✔
988
            'email' => $to,
989
            'message' => nl2br($welcome),
4✔
990
            'review' => $review,
991
            'groupname' => $g->getName()
4✔
992
        ]);
993

994
        if ($to) {
4✔
995
            list ($transport, $mailer) = Mail::getMailer();
4✔
996
            $message = \Swift_Message::newInstance()
4✔
997
                ->setSubject(($review ? "Please review: " : "") . "Welcome to " . $atts['namedisplay'])
4✔
998
                ->setFrom([$g->getAutoEmail() => $atts['namedisplay'] . ' Volunteers'])
4✔
999
                ->setReplyTo([$g->getModsEmail() => $atts['namedisplay'] . ' Volunteers'])
4✔
1000
                ->setTo($to)
4✔
1001
                ->setDate(time())
4✔
1002
                ->setBody($welcome);
4✔
1003

1004
            # Add HTML in base-64 as default quoted-printable encoding leads to problems on
1005
            # Outlook.
1006
            $htmlPart = \Swift_MimePart::newInstance();
4✔
1007
            $htmlPart->setCharset('utf-8');
4✔
1008
            $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
4✔
1009
            $htmlPart->setContentType('text/html');
4✔
1010
            $htmlPart->setBody($html);
4✔
1011
            $message->attach($htmlPart);
4✔
1012

1013
            Mail::addHeaders($message, Mail::WELCOME, $this->getId());
4✔
1014

1015
            $this->sendIt($mailer, $message);
4✔
1016
        }
1017
    }
1018

1019
    public function cacheMemberships($id = NULL)
1020
    {
1021
        $id = $id ? $id : $this->id;
325✔
1022

1023
        # We get all the memberships in a single call, because some members are on many groups and this can
1024
        # save hundreds of calls to the DB.
1025
        if (!$this->memberships) {
325✔
1026
            $this->memberships = [];
325✔
1027

1028
            $membs = $this->dbhr->preQuery("SELECT memberships.*, groups.type FROM memberships INNER JOIN `groups` ON groups.id = memberships.groupid WHERE userid = ?;", [ $id ]);
325✔
1029
            foreach ($membs as $memb) {
325✔
1030
                $this->memberships[$memb['groupid']] = $memb;
299✔
1031
            }
1032
        }
1033

1034
        return ($this->memberships);
325✔
1035
    }
1036

1037
    public function clearMembershipCache()
1038
    {
1039
        $this->memberships = NULL;
269✔
1040
    }
1041

1042
    public function getMembershipAtt($groupid, $att)
1043
    {
1044
        $this->cacheMemberships();
171✔
1045
        $val = NULL;
171✔
1046
        if (Utils::pres($groupid, $this->memberships)) {
171✔
1047
            $val = Utils::presdef($att, $this->memberships[$groupid], NULL);
164✔
1048
        }
1049

1050
        return ($val);
171✔
1051
    }
1052

1053
    public function setMembershipAtt($groupid, $att, $val)
1054
    {
1055
        $this->clearMembershipCache();
217✔
1056
        Session::clearSessionCache();
217✔
1057
        $sql = "UPDATE memberships SET $att = ? WHERE groupid = ? AND userid = ?;";
217✔
1058
        $rc = $this->dbhm->preExec($sql, [
217✔
1059
            $val,
1060
            $groupid,
1061
            $this->id
217✔
1062
        ]);
1063

1064
        return ($rc);
217✔
1065
    }
1066

1067
    public function removeMembership($groupid, $ban = FALSE, $spam = FALSE, $byemail = NULL)
1068
    {
1069
        $this->clearMembershipCache();
33✔
1070
        $g = Group::get($this->dbhr, $this->dbhm, $groupid);
33✔
1071
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
33✔
1072
        $meid = $me ? $me->getId() : NULL;
33✔
1073

1074
        // @codeCoverageIgnoreStart
1075
        //
1076
        // Let them know.  We always want to let TN know if a member is removed/banned so that they can't see
1077
        // the messages.
1078
        if ($byemail || $this->isTN()) {
1079
            list ($transport, $mailer) = Mail::getMailer();
1080
            $message = \Swift_Message::newInstance()
1081
                ->setSubject("Farewell from " . $g->getPrivate('nameshort'))
1082
                ->setFrom($g->getAutoEmail())
1083
                ->setReplyTo($g->getModsEmail())
1084
                ->setTo($this->getEmailPreferred())
1085
                ->setDate(time())
1086
                ->setBody("Parting is such sweet sorrow.");
1087

1088
            Mail::addHeaders($message, Mail::REMOVED);
1089

1090
            $this->sendIt($mailer, $message);
1091
        }
1092
        // @codeCoverageIgnoreEnd
1093

1094
        if ($ban) {
33✔
1095
            $sql = "INSERT IGNORE INTO users_banned (userid, groupid, byuser) VALUES (?,?,?);";
12✔
1096
            $this->dbhm->preExec($sql, [
12✔
1097
                $this->id,
12✔
1098
                $groupid,
1099
                $meid
1100
            ]);
1101

1102
            # Mark any messages on this group as withdrawn.  Not strictly true, but it will stop people replying
1103
            # while keeping the messages around for stats purposes and in case we want to look at them.
1104
            $msgs = $this->dbhr->preQuery("SELECT messages_groups.msgid FROM messages_groups 
12✔
1105
    INNER JOIN messages ON messages_groups.msgid = messages.id 
1106
    LEFT JOIN messages_outcomes ON messages_outcomes.msgid = messages_groups.msgid 
1107
    WHERE messages.fromuser = ? AND messages_groups.groupid = ? AND messages.type IN (?, ?);", [
1108
                $this->id,
12✔
1109
                $groupid,
1110
                Message::TYPE_OFFER,
1111
                Message::TYPE_WANTED
1112
            ]);
1113

1114
            foreach ($msgs as $msg) {
12✔
1115
                $m = new Message($this->dbhr, $this->dbhm, $msg['msgid']);
1✔
1116
                $m->mark(Message::OUTCOME_WITHDRAWN, "Marked as withdrawn by ban", NULL, NULL);
1✔
1117
            }
1118
        }
1119

1120
        # Now remove the membership.
1121
        $rc = $this->dbhm->preExec("DELETE FROM memberships WHERE userid = ? AND groupid = ?;",
33✔
1122
            [
1123
                $this->id,
33✔
1124
                $groupid
1125
            ]);
1126

1127
        if ($this->dbhm->rowsAffected() || $ban) {
33✔
1128
            $l = new Log($this->dbhr, $this->dbhm);
33✔
1129
            $l->log([
33✔
1130
                'type' => Log::TYPE_GROUP,
1131
                'subtype' => Log::SUBTYPE_LEFT,
1132
                'user' => $this->id,
33✔
1133
                'byuser' => $meid,
1134
                'groupid' => $groupid,
1135
                'text' => $spam ? "Autoremoved spammer" : ($ban ? "via ban" : NULL)
33✔
1136
            ]);
1137
        }
1138

1139
        return ($rc);
33✔
1140
    }
1141

1142
    public function getMembershipGroupIds($modonly = FALSE, $grouptype = NULL, $id = NULL) {
1143
        $id = $id ? $id : $this->id;
5✔
1144

1145
        $ret = [];
5✔
1146
        $modq = $modonly ? " AND role IN ('Owner', 'Moderator') " : "";
5✔
1147
        $typeq = $grouptype ? (" AND `type` = " . $this->dbhr->quote($grouptype)) : '';
5✔
1148
        $publishq = Session::modtools() ? "" : "AND groups.publish = 1";
5✔
1149
        $sql = "SELECT groupid FROM memberships INNER JOIN `groups` ON groups.id = memberships.groupid $publishq WHERE userid = ? $modq $typeq ORDER BY memberships.added DESC;";
5✔
1150
        $groups = $this->dbhr->preQuery($sql, [$id]);
5✔
1151
        #error_log("getMemberships $sql {$id} " . var_export($groups, TRUE));
1152
        $groupids = array_filter(array_column($groups, 'groupid'));
5✔
1153
        return $groupids;
5✔
1154
    }
1155

1156
    public function getMemberships($modonly = FALSE, $grouptype = NULL, $getwork = FALSE, $pernickety = FALSE, $id = NULL)
1157
    {
1158
        $id = $id ? $id : $this->id;
147✔
1159

1160
        $ret = [];
147✔
1161
        $modq = $modonly ? " AND role IN ('Owner', 'Moderator') " : "";
147✔
1162
        $typeq = $grouptype ? (" AND `type` = " . $this->dbhr->quote($grouptype)) : '';
147✔
1163
        $publishq = Session::modtools() ? "" : "AND groups.publish = 1";
147✔
1164
        $sql = "SELECT type, 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;";
147✔
1165
        $groups = $this->dbhr->preQuery($sql, [$id]);
147✔
1166
        #error_log("getMemberships $sql {$id} " . var_export($groups, TRUE));
1167

1168
        $c = new ModConfig($this->dbhr, $this->dbhm);
147✔
1169

1170
        # Get all the groups efficiently.
1171
        $groupids = array_filter(array_column($groups, 'groupid'));
147✔
1172
        $gc = new GroupCollection($this->dbhr, $this->dbhm, $groupids);
147✔
1173
        $groupobjs = $gc->get();
147✔
1174
        $getworkids = [];
147✔
1175
        $groupsettings = [];
147✔
1176

1177
        for ($i = 0; $i < count($groupids); $i++) {
147✔
1178
            $group = $groups[$i];
114✔
1179
            $g = $groupobjs[$i];
114✔
1180
            $one = $g->getPublic();
114✔
1181

1182
            $one['role'] = $group['role'];
114✔
1183
            $one['collection'] = $group['collection'];
114✔
1184
            $amod = ($one['role'] == User::ROLE_MODERATOR || $one['role'] == User::ROLE_OWNER);
114✔
1185
            $one['configid'] = Utils::presdef('configid', $group, NULL);
114✔
1186

1187
            if ($amod && !Utils::pres('configid', $one)) {
114✔
1188
                # Get a config using defaults.
1189
                $one['configid'] = $c->getForGroup($id, $group['groupid']);
28✔
1190
            }
1191

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

1194
            # If we don't have our own email on this group we won't be sending mails.  This is what affects what
1195
            # gets shown on the Settings page for the user, and we only want to check this here
1196
            # for performance reasons.
1197
            $one['mysettings']['emailfrequency'] = ($group['type'] ==  Group::GROUP_FREEGLE &&
114✔
1198
                ($pernickety || $this->sendOurMails($g, FALSE, FALSE))) ?
114✔
1199
                (array_key_exists('emailfrequency', $one['mysettings']) ? $one['mysettings']['emailfrequency'] :  24)
77✔
1200
                : 0;
40✔
1201

1202
            $groupsettings[$group['groupid']] = $one['mysettings'];
114✔
1203

1204
            if ($getwork) {
114✔
1205
                # We need to find out how much work there is whether or not we are an active mod because we need
1206
                # to be able to see that it is there.  The UI shows it less obviously.
1207
                if ($amod) {
20✔
1208
                    $getworkids[] = $group['groupid'];
18✔
1209
                }
1210
            }
1211

1212
            $ret[] = $one;
114✔
1213
        }
1214

1215
        if ($getwork) {
147✔
1216
            # Get all the work.  This is across all groups for performance.
1217
            $g = new Group($this->dbhr, $this->dbhm);
27✔
1218
            $work = $g->getWorkCounts($groupsettings, $groupids);
27✔
1219

1220
            foreach ($getworkids as $groupid) {
27✔
1221
                foreach ($ret as &$group) {
18✔
1222
                    if ($group['id'] == $groupid) {
18✔
1223
                        $group['work'] = $work[$groupid];
18✔
1224
                    }
1225
                }
1226
            }
1227
        }
1228

1229
        return ($ret);
147✔
1230
    }
1231

1232
    public function getConfigs($all)
1233
    {
1234
        $ret = [];
23✔
1235
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
23✔
1236

1237
        if ($all) {
23✔
1238
            # We can see configs which
1239
            # - we created
1240
            # - are used by mods on groups on which we are a mod
1241
            # - defaults
1242
            $modships = $me ? $this->getModeratorships() : [];
22✔
1243
            $modships = count($modships) > 0 ? $modships : [0];
22✔
1244

1245
            $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✔
1246
            $ids = $this->dbhr->preQuery($sql);
22✔
1247
        } else {
1248
            # We only want to see the configs that we are actively using.  This reduces the size of what we return
1249
            # for people on many groups.
1250
            $sql = "SELECT DISTINCT configid AS id FROM memberships WHERE userid = ? AND configid IS NOT NULL;";
3✔
1251
            $ids = $this->dbhr->preQuery($sql, [ $me->getId() ]);
3✔
1252
        }
1253

1254
        $configids = array_filter(array_column($ids, 'id'));
23✔
1255

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

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

1269
            foreach ($configs as $config) {
4✔
1270
                $c = new ModConfig($this->dbhr, $this->dbhm, $config['id'], $config, $stdmsgs, $bulkops);
4✔
1271
                $thisone = $c->getPublic(FALSE);
4✔
1272

1273
                if (Utils::pres('createdby', $config)) {
4✔
1274
                    $ctx = NULL;
3✔
1275
                    $thisone['createdby'] = [
3✔
1276
                        'id' => $config['createdby'],
3✔
1277
                        'displayname' => $config['createdname']
3✔
1278
                    ];
1279
                }
1280

1281
                $ret[] = $thisone;
4✔
1282
            }
1283
        }
1284

1285
        # Return in alphabetical order.
1286
        $rc = usort($ret, function ($a, $b) {
23✔
1287
            return strcasecmp($a['name'], $b['name']);
1✔
1288
        });
1289

1290
        return ($ret);
23✔
1291
    }
1292

1293
    public function getModeratorships($id = NULL, $activeonly = FALSE)
1294
    {
1295
        $this->cacheMemberships($id);
147✔
1296
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
147✔
1297

1298
        $ret = [];
147✔
1299
        foreach ($this->memberships AS $membership) {
147✔
1300
            if ($membership['role'] == 'Owner' || $membership['role'] == 'Moderator') {
123✔
1301
                if (!$activeonly || $me->activeModForGroup($membership['groupid'])) {
88✔
1302
                    $ret[] = $membership['groupid'];
88✔
1303
                }
1304
            }
1305
        }
1306

1307
        return ($ret);
147✔
1308
    }
1309

1310
    public function isModOrOwner($groupid)
1311
    {
1312
        # Very frequently used.  Cache in session.
1313
        #error_log("modOrOwner " . var_export($_SESSION['modorowner'], TRUE));
1314
        if ((session_status() !== PHP_SESSION_NONE || getenv('UT')) &&
169✔
1315
            array_key_exists('modorowner', $_SESSION) &&
169✔
1316
            array_key_exists($this->id, $_SESSION['modorowner']) &&
169✔
1317
            array_key_exists($groupid, $_SESSION['modorowner'][$this->id])) {
169✔
1318
            return ($_SESSION['modorowner'][$this->id][$groupid]);
27✔
1319
        } else {
1320
            $sql = "SELECT groupid FROM memberships WHERE userid = ? AND role IN ('Moderator', 'Owner') AND groupid = ?;";
169✔
1321
            #error_log("$sql {$this->id}, $groupid");
1322
            $groups = $this->dbhr->preQuery($sql, [
169✔
1323
                $this->id,
169✔
1324
                $groupid
1325
            ]);
1326

1327
            foreach ($groups as $group) {
169✔
1328
                $_SESSION['modorowner'][$this->id][$groupid] = TRUE;
42✔
1329
                return TRUE;
42✔
1330
            }
1331

1332
            $_SESSION['modorowner'][$this->id][$groupid] = FALSE;
152✔
1333
            return (FALSE);
152✔
1334
        }
1335
    }
1336

1337
    public function getLogins($credentials = TRUE, $id = NULL, $excludelink = FALSE)
1338
    {
1339
        $excludelinkq = $excludelink ? (" AND type != '" . User::LOGIN_LINK . "'") : '';
263✔
1340

1341
        $logins = $this->dbhr->preQuery("SELECT * FROM users_logins WHERE userid = ? $excludelinkq ORDER BY lastaccess DESC;",
263✔
1342
            [$id ? $id : $this->id]);
263✔
1343

1344
        foreach ($logins as &$login) {
263✔
1345
            if (!$credentials) {
255✔
1346
                unset($login['credentials']);
19✔
1347
            }
1348
            $login['added'] = Utils::ISODate($login['added']);
255✔
1349
            $login['lastaccess'] = Utils::ISODate($login['lastaccess']);
255✔
1350
            $login['uid'] = '' . $login['uid'];
255✔
1351
        }
1352

1353
        return ($logins);
263✔
1354
    }
1355

1356
    public function findByLogin($type, $uid)
1357
    {
1358
        $logins = $this->dbhr->preQuery("SELECT * FROM users_logins WHERE uid = ? AND type = ?;",
5✔
1359
            [$uid, $type]);
1360

1361
        foreach ($logins as $login) {
5✔
1362
            return ($login['userid']);
4✔
1363
        }
1364

1365
        return (NULL);
5✔
1366
    }
1367

1368
    public function addLogin($type, $uid, $creds = NULL, $salt = PASSWORD_SALT)
1369
    {
1370
        if ($type == User::LOGIN_NATIVE) {
395✔
1371
            # Native login - encrypt the password a bit.  The password salt is global in FD, but per-login for users
1372
            # migrated from Norfolk.
1373
            $creds = $this->hashPassword($creds, $salt);
394✔
1374
            $uid = $this->id;
394✔
1375
        }
1376

1377
        # If the login with this type already exists in the table, that's fine.
1378
        $sql = "INSERT INTO users_logins (userid, uid, type, credentials, salt) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE credentials = ?, salt = ?;";
395✔
1379
        $rc = $this->dbhm->preExec($sql,
395✔
1380
            [$this->id, $uid, $type, $creds, $salt, $creds, $salt]);
395✔
1381

1382
        # If we add a login, we might be about to log in.
1383
        # TODO This is a bit hacky.
1384
        global $sessionPrepared;
1385
        $sessionPrepared = FALSE;
395✔
1386

1387
        return ($rc);
395✔
1388
    }
1389

1390
    public function removeLogin($type, $uid)
1391
    {
1392
        $rc = $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ? AND type = ? AND uid = ?;",
4✔
1393
            [$this->id, $type, $uid]);
4✔
1394
        return ($rc);
4✔
1395
    }
1396

1397
    public function getRoleForGroup($groupid, $overrides = TRUE)
1398
    {
1399
        # We can have a number of roles on a group
1400
        # - none, we can only see what is member
1401
        # - member, we are a group member and can see some extra info
1402
        # - moderator, we can see most info on a group
1403
        # - owner, we can see everything
1404
        #
1405
        # If our system role is support then we get moderator status; if it's admin we get owner status.
1406
        $role = User::ROLE_NONMEMBER;
72✔
1407

1408
        if ($overrides) {
72✔
1409
            switch ($this->getPrivate('systemrole')) {
33✔
1410
                case User::SYSTEMROLE_SUPPORT:
1411
                    $role = User::ROLE_MODERATOR;
3✔
1412
                    break;
3✔
1413
                case User::SYSTEMROLE_ADMIN:
1414
                    $role = User::ROLE_OWNER;
1✔
1415
                    break;
1✔
1416
            }
1417
        }
1418

1419
        # Now find if we have any membership of the group which might also give us a role.
1420
        $membs = $this->dbhr->preQuery("SELECT role FROM memberships WHERE userid = ? AND groupid = ?;", [
72✔
1421
            $this->id,
72✔
1422
            $groupid
1423
        ]);
1424

1425
        foreach ($membs as $memb) {
72✔
1426
            switch ($memb['role']) {
69✔
1427
                case 'Moderator':
69✔
1428
                    # Don't downgrade from owner if we have that by virtue of an override.
1429
                    $role = $role == User::ROLE_OWNER ? $role : User::ROLE_MODERATOR;
33✔
1430
                    break;
33✔
1431
                case 'Owner':
61✔
1432
                    $role = User::ROLE_OWNER;
8✔
1433
                    break;
8✔
1434
                case 'Member':
59✔
1435
                    # Don't downgrade if we already have a role by virtue of an override.
1436
                    $role = $role == User::ROLE_NONMEMBER ? User::ROLE_MEMBER : $role;
59✔
1437
                    break;
59✔
1438
            }
1439
        }
1440

1441
        return ($role);
72✔
1442
    }
1443

1444
    public function moderatorForUser($userid, $allowmod = FALSE)
1445
    {
1446
        # There are times when we want to check whether we can administer a user, but when we are not immediately
1447
        # within the context of a known group.  We can administer a user when:
1448
        # - they're only a user themselves
1449
        # - we are a mod on one of the groups on which they are a member.
1450
        # - it's us
1451
        if ($userid != $this->getId()) {
13✔
1452
            $u = User::get($this->dbhr, $this->dbhm, $userid);
10✔
1453

1454
            $usermemberships = [];
10✔
1455
            $modq = $allowmod ? ", 'Moderator', 'Owner'" : '';
10✔
1456
            $groups = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ? AND role IN ('Member' $modq);", [$userid]);
10✔
1457
            foreach ($groups as $group) {
10✔
1458
                $usermemberships[] = $group['groupid'];
7✔
1459
            }
1460

1461
            $mymodships = $this->getModeratorships();
10✔
1462

1463
            # Is there any group which we mod and which they are a member of?
1464
            $canmod = count(array_intersect($usermemberships, $mymodships)) > 0;
10✔
1465
        } else {
1466
            $canmod = TRUE;
5✔
1467
        }
1468

1469
        return ($canmod);
13✔
1470
    }
1471

1472
    public function getSetting($setting, $default)
1473
    {
1474
        $ret = $default;
423✔
1475
        $s = $this->getPrivate('settings');
423✔
1476

1477
        if ($s) {
423✔
1478
            $settings = json_decode($s, TRUE);
17✔
1479
            $ret = array_key_exists($setting, $settings) ? $settings[$setting] : $default;
17✔
1480
        }
1481

1482
        return ($ret);
423✔
1483
    }
1484

1485
    public function setSetting($setting, $val)
1486
    {
1487
        $s = $this->getPrivate('settings');
28✔
1488

1489
        if ($s) {
28✔
1490
            $settings = json_decode($s, TRUE);
1✔
1491
        } else {
1492
            $settings = [];
28✔
1493
        }
1494

1495
        $settings[$setting] = $val;
28✔
1496
        $this->setPrivate('settings', json_encode($settings));
28✔
1497
    }
1498

1499
    public function setGroupSettings($groupid, $settings)
1500
    {
1501
        $this->clearMembershipCache();
5✔
1502
        $sql = "UPDATE memberships SET settings = ? WHERE userid = ? AND groupid = ?;";
5✔
1503
        return ($this->dbhm->preExec($sql, [
5✔
1504
            json_encode($settings),
5✔
1505
            $this->id,
5✔
1506
            $groupid
1507
        ]));
1508
    }
1509

1510
    public function activeModForGroup($groupid, $mysettings = NULL)
1511
    {
1512
        $mysettings = $mysettings ? $mysettings : $this->getGroupSettings($groupid);
38✔
1513

1514
        # If we have the active flag use that; otherwise assume that the legacy showmessages flag tells us.  Default
1515
        # to active.
1516
        # TODO Retire showmessages entirely and remove from user configs.
1517
        $active = array_key_exists('active', $mysettings) ? $mysettings['active'] : (!array_key_exists('showmessages', $mysettings) || $mysettings['showmessages']);
38✔
1518
        return ($active);
38✔
1519
    }
1520

1521
    public function getGroupSettings($groupid, $configid = NULL, $id = NULL)
1522
    {
1523
        $id = $id ? $id : $this->id;
144✔
1524

1525
        # We have some parameters which may give us some info which saves queries
1526
        $this->cacheMemberships($id);
144✔
1527

1528
        # Defaults match memberships ones in Group.php.
1529
        $defaults = [
144✔
1530
            'active' => 1,
1531
            'showchat' => 1,
1532
            'pushnotify' => 1,
1533
            'eventsallowed' => 1,
1534
            'volunteeringallowed' => 1
1535
        ];
1536

1537
        $settings = $defaults;
144✔
1538

1539
        if (Utils::pres($groupid, $this->memberships)) {
144✔
1540
            $set = $this->memberships[$groupid];
144✔
1541

1542
            if ($set['settings']) {
144✔
1543
                $settings = json_decode($set['settings'], TRUE);
4✔
1544

1545
                if (!$configid && ($set['role'] == User::ROLE_OWNER || $set['role'] == User::ROLE_MODERATOR)) {
4✔
1546
                    $c = new ModConfig($this->dbhr, $this->dbhm);
4✔
1547

1548
                    # We might have an explicit configid - if so, use it to save on DB calls.
1549
                    $settings['configid'] = $set['configid'] ? $set['configid'] : $c->getForGroup($this->id, $groupid);
4✔
1550
                }
1551
            }
1552

1553
            # Base active setting on legacy showmessages setting if not present.
1554
            $settings['active'] = array_key_exists('active', $settings) ? $settings['active'] : (!array_key_exists('showmessages', $settings) || $settings['showmessages']);
144✔
1555
            $settings['active'] = $settings['active'] ? 1 : 0;
144✔
1556

1557
            foreach ($defaults as $key => $val) {
144✔
1558
                if (!array_key_exists($key, $settings)) {
144✔
1559
                    $settings[$key] = $val;
4✔
1560
                }
1561
            }
1562

1563
            $settings['emailfrequency'] = $set['emailfrequency'];
144✔
1564
            $settings['eventsallowed'] = $set['eventsallowed'];
144✔
1565
            $settings['volunteeringallowed'] = $set['volunteeringallowed'];
144✔
1566
        }
1567

1568
        return ($settings);
144✔
1569
    }
1570

1571
    public function setRole($role, $groupid)
1572
    {
1573
        $rc = TRUE;
45✔
1574
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
45✔
1575

1576
        Session::clearSessionCache();
45✔
1577

1578
        $currentRole = $this->getRoleForGroup($groupid, FALSE);
45✔
1579

1580
        if ($currentRole != $role) {
45✔
1581
            $l = new Log($this->dbhr, $this->dbhm);
45✔
1582
            $l->log([
45✔
1583
                        'type' => Log::TYPE_USER,
1584
                        'byuser' => $me ? $me->getId() : NULL,
45✔
1585
                        'subtype' => Log::SUBTYPE_ROLE_CHANGE,
1586
                        'groupid' => $groupid,
1587
                        'user' => $this->id,
45✔
1588
                        'text' => $role
1589
                    ]);
1590

1591
            $this->clearMembershipCache();
45✔
1592
            $sql = "UPDATE memberships SET role = ? WHERE userid = ? AND groupid = ?;";
45✔
1593
            $rc = $this->dbhm->preExec($sql, [
45✔
1594
                $role,
1595
                $this->id,
45✔
1596
                $groupid
1597
            ]);
1598

1599
            # We might need to update the systemrole.
1600
            #
1601
            # Not the end of the world if this fails.
1602
            $this->updateSystemRole($role);
45✔
1603

1604
            if ($currentRole == User::ROLE_MEMBER) {
45✔
1605
                # We have promoted this member.  We want to ensure that they have no unread old chats.
1606
                $r = new ChatRoom($this->dbhr, $this->dbhm);
44✔
1607
                $r->upToDateAll($this->getId(),[
44✔
1608
                    ChatRoom::TYPE_USER2MOD
1609
                ]);
1610
            }
1611

1612
            $this->memberships = NULL;
45✔
1613
        }
1614

1615
        return ($rc);
45✔
1616
    }
1617

1618
    public function getActiveCounts() {
1619
        $users = [
1✔
1620
            $this->id => [
1✔
1621
                'id' => $this->id
1✔
1622
            ]];
1623

1624
        $this->getActiveCountss($users);
1✔
1625
        return($users[$this->id]['activecounts']);
1✔
1626
    }
1627

1628
    public function getActiveCountss(&$users) {
1629
        $start = date('Y-m-d', strtotime(User::OPEN_AGE . " days ago"));
17✔
1630
        $uids = array_filter(array_column($users, 'id'));
17✔
1631

1632
        if (count($uids)) {
17✔
1633
            $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;", [
16✔
1634
                $start,
1635
                MessageCollection::APPROVED
1636
            ]);
1637

1638
            foreach ($users as $user) {
16✔
1639
                $offers = 0;
16✔
1640
                $wanteds = 0;
16✔
1641

1642
                foreach ($counts as $count) {
16✔
1643
                    if ($count['userid'] == $user['id']) {
1✔
1644
                        if ($count['type'] == Message::TYPE_OFFER) {
1✔
1645
                            $offers += $count['count'];
1✔
1646
                        } else if ($count['type'] == Message::TYPE_WANTED) {
1✔
1647
                            $wanteds += $count['count'];
1✔
1648
                        }
1649
                    }
1650
                }
1651

1652
                $users[$user['id']]['activecounts'] = [
16✔
1653
                    'offers' => $offers,
1654
                    'wanteds' => $wanteds
1655
                ];
1656
            }
1657
        }
1658
    }
1659

1660
    public function getInfos(&$users, $grace = 30) {
1661
        $uids = array_filter(array_column($users, 'id'));
119✔
1662

1663
        $start = date('Y-m-d', strtotime(User::OPEN_AGE . " days ago"));
119✔
1664
        $days90 = date("Y-m-d", strtotime("90 days ago"));
119✔
1665
        $userq = "userid IN (" . implode(',', $uids) . ")";
119✔
1666

1667
        foreach ($uids as $uid) {
119✔
1668
            $users[$uid]['info']['replies'] = 0;
119✔
1669
            $users[$uid]['info']['taken'] = 0;
119✔
1670
            $users[$uid]['info']['reneged'] = 0;
119✔
1671
            $users[$uid]['info']['collected'] = 0;
119✔
1672
            $users[$uid]['info']['openage'] = User::OPEN_AGE;
119✔
1673
        }
1674

1675
        // We can combine some queries into a single one.  This is better for performance because it saves on
1676
        // the round trip (seriously, I've measured it, and it's worth doing).
1677
        //
1678
        // No need to check on the chat room type as we can only get messages of type Interested in a User2User chat.
1679
        $tq = Session::modtools() ? ", t6.*, t7.*" : '';
119✔
1680
        $sql = "SELECT t0.id AS theuserid, t0.lastaccess AS lastaccess, t1.*, t3.*, t4.*, t5.* $tq FROM
119✔
1681
(SELECT id, lastaccess FROM users WHERE id in (" . implode(',', $uids) . ")) t0 LEFT JOIN                                                                
119✔
1682
(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";
1683

1684
        if (Session::modtools()) {
119✔
1685
            $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 ";
117✔
1686
            $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 ";
117✔
1687
        }
1688

1689
        $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
119✔
1690
(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
1691
(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
1692
;";
1693
        $counts = $this->dbhr->preQuery($sql, [
119✔
1694
            $start,
1695
            ChatMessage::TYPE_INTERESTED,
1696
            $start,
1697
            Message::TYPE_OFFER,
1698
            ChatMessage::TYPE_INTERESTED
1699
        ]);
1700

1701
        foreach ($users as $uid => $user) {
119✔
1702
            foreach ($counts as $count) {
119✔
1703
                if ($count['theuserid'] == $users[$uid]['id']) {
119✔
1704
                    $users[$uid]['info']['replies'] = $count['replycount'] ? $count['replycount'] : 0;
119✔
1705

1706
                    if (Session::modtools()) {
119✔
1707
                        $users[$uid]['info']['repliesoffer'] = $count['replycountoffer'] ? $count['replycountoffer'] : 0;
117✔
1708
                        $users[$uid]['info']['replieswanted'] = $count['replycountwanted'] ? $count['replycountwanted'] : 0;
117✔
1709
                    }
1710

1711
                    $users[$uid]['info']['reneged'] = $count['reneged'] ? $count['reneged'] : 0;
119✔
1712
                    $users[$uid]['info']['collected'] = $count['collected'] ? $count['collected'] : 0;
119✔
1713
                    $users[$uid]['info']['lastaccess'] = $count['lastaccess'] ? Utils::ISODate($count['lastaccess']) : NULL;
119✔
1714
                    $users[$uid]['info']['count'] = $count;
119✔
1715

1716
                    if (Utils::pres('abouttime', $count)) {
119✔
1717
                        $users[$uid]['info']['aboutme'] = [
2✔
1718
                            'timestamp' => Utils::ISODate($count['abouttime']),
2✔
1719
                            'text' => $count['abouttext']
2✔
1720
                        ];
1721
                    }
1722
                }
1723
            }
1724
        }
1725

1726
        $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;";
119✔
1727
        $counts = $this->dbhr->preQuery($sql, [
119✔
1728
            $start,
1729
            MessageCollection::APPROVED
1730
        ]);
1731

1732
        foreach ($users as $uid => $user) {
119✔
1733
            $users[$uid]['info']['offers'] = 0;
119✔
1734
            $users[$uid]['info']['wanteds'] = 0;
119✔
1735
            $users[$uid]['info']['openoffers'] = 0;
119✔
1736
            $users[$uid]['info']['openwanteds'] = 0;
119✔
1737
            $users[$uid]['info']['expectedreply'] = 0;
119✔
1738

1739
            foreach ($counts as $count) {
119✔
1740
                if ($count['userid'] == $users[$uid]['id']) {
59✔
1741
                    if ($count['type'] == Message::TYPE_OFFER) {
59✔
1742
                        $users[$uid]['info']['offers'] += $count['count'];
41✔
1743

1744
                        if (!Utils::pres('outcome', $count)) {
41✔
1745
                            $users[$uid]['info']['openoffers'] += $count['count'];
41✔
1746
                        }
1747
                    } else if ($count['type'] == Message::TYPE_WANTED) {
20✔
1748
                        $users[$uid]['info']['wanteds'] += $count['count'];
7✔
1749

1750
                        if (!Utils::pres('outcome', $count)) {
7✔
1751
                            $users[$uid]['info']['openwanteds'] += $count['count'];
3✔
1752
                        }
1753
                    }
1754
                }
1755
            }
1756
        }
1757

1758
        # Distance away.
1759
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
119✔
1760

1761
        if ($me) {
119✔
1762
            list ($mylat, $mylng, $myloc) = $me->getLatLng();
79✔
1763

1764
            if (!is_null($myloc)) {
79✔
1765
                $latlngs = $this->getLatLngs($users, FALSE, TRUE);
11✔
1766

1767
                foreach ($latlngs as $userid => $latlng) {
11✔
1768
                    if ($latlng) {
11✔
1769
                        $users[$userid]['info']['milesaway'] = $this->getDistanceBetween($mylat, $mylng, $latlng['lat'], $latlng['lng']);
11✔
1770
                    }
1771
                }
1772
            }
1773

1774
            $this->getPublicLocations($users);
79✔
1775
        }
1776

1777
        $r = new ChatRoom($this->dbhr, $this->dbhm);
119✔
1778
        $replytimes = $r->replyTimes($uids);
119✔
1779

1780
        foreach ($replytimes as $uid => $replytime) {
119✔
1781
            $users[$uid]['info']['replytime'] = $replytime;
119✔
1782
        }
1783

1784
        $nudges = $r->nudgeCounts($uids);
119✔
1785

1786
        foreach ($nudges as $uid => $nudgecount) {
119✔
1787
            $users[$uid]['info']['nudges'] = $nudgecount;
119✔
1788
        }
1789

1790
        $ratings = $this->getRatings($uids);
119✔
1791

1792
        foreach ($ratings as $uid => $rating) {
119✔
1793
            $users[$uid]['info']['ratings'] = $rating;
119✔
1794
        }
1795

1796
        $replies = $this->getExpectedReplies($uids, ChatRoom::ACTIVELIM, $grace);
119✔
1797

1798
        foreach ($replies as $reply) {
119✔
1799
            if ($reply['expectee']) {
119✔
1800
                $users[$reply['expectee']]['info']['expectedreply'] = $reply['count'];
1✔
1801
            }
1802
        }
1803
    }
1804
    
1805
    public function getInfo($grace = 30)
1806
    {
1807
        $users = [
14✔
1808
            $this->id => [
14✔
1809
                'id' => $this->id
14✔
1810
            ]
1811
        ];
1812

1813
        $this->getInfos($users, $grace);
14✔
1814

1815
        return ($users[$this->id]['info']);
14✔
1816
    }
1817

1818
    public function getAboutMe() {
1819
        $ret = NULL;
41✔
1820

1821
        $aboutmes = $this->dbhr->preQuery("SELECT * FROM users_aboutme WHERE userid = ? ORDER BY timestamp DESC LIMIT 1;", [
41✔
1822
            $this->id
41✔
1823
        ]);
1824

1825
        foreach ($aboutmes as $aboutme) {
41✔
1826
            $ret = [
1✔
1827
                'timestamp' => Utils::ISODate($aboutme['timestamp']),
1✔
1828
                'text' => $aboutme['text']
1✔
1829
            ];
1830
        }
1831

1832
        return($ret);
41✔
1833
    }
1834

1835
    private function md5_hex_to_dec($hex_str)
1836
    {
1837
        $arr = str_split($hex_str, 4);
18✔
1838
        foreach ($arr as $grp) {
18✔
1839
            $dec[] = str_pad(hexdec($grp), 5, '0', STR_PAD_LEFT);
18✔
1840
        }
1841
        return floatval("0." . implode('', $dec));
18✔
1842
    }
1843

1844
    public function getDistance($mylat, $mylng) {
1845
        list ($tlat, $tlng, $tloc) = $this->getLatLng();
1✔
1846
        #error_log("Get distance $mylat, $mylng, $tlat, $tlng = " . $this->getDistanceBetween($mylat, $mylng, $tlat, $tlng));
1847
        return($this->getDistanceBetween($mylat, $mylng, $tlat, $tlng));
1✔
1848
    }
1849

1850
    public function getDistanceBetween($mylat, $mylng, $tlat, $tlng)
1851
    {
1852
        $p1 = new POI($mylat, $mylng);
18✔
1853

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

1860
        # Now randomise the distance a bit each time we get it, so that anyone attempting repeated measurements
1861
        # will get conflicting results around the precise location that isn't actually theirs.  But still close
1862
        # enough to be useful for our purposes.
1863
        $tlat += mt_rand(-100, 100) / 20000;
18✔
1864
        $tlng += mt_rand(-100, 100) / 20000;
18✔
1865

1866
        $p2 = new POI($tlat, $tlng);
18✔
1867
        $metres = $p1->getDistanceInMetersTo($p2);
18✔
1868
        $miles = $metres / 1609.344;
18✔
1869
        $miles = $miles > 2 ? round($miles) : round($miles, 1);
18✔
1870
        return ($miles);
18✔
1871
    }
1872

1873
    public function gravatar($email, $s = 80, $d = 'mm', $r = 'g')
1874
    {
1875
        $url = 'https://www.gravatar.com/avatar/';
19✔
1876
        $url .= md5(strtolower(trim($email)));
19✔
1877
        $url .= "?s=$s&d=$d&r=$r";
19✔
1878
        return $url;
19✔
1879
    }
1880

1881
    public function getPublicLocation()
1882
    {
1883
        $users = [
29✔
1884
            $this->id => [
29✔
1885
                'id' => $this->id
29✔
1886
            ]
1887
        ];
1888

1889
        $this->getLatLngs($users);
29✔
1890
        $this->getPublicLocations($users);
29✔
1891

1892
        return($users[$this->id]['info']['publiclocation']);
29✔
1893
    }
1894

1895
    public function ensureAvatar(&$atts)
1896
    {
1897
        # This involves querying external sites, so we need to use it with care, otherwise we can hang our
1898
        # system.  It can also cause updates, so if we call it lots of times, it can result in cluster issues.
1899
        $forcedefault = FALSE;
18✔
1900
        $settings = Utils::presdef('settings', $atts, NULL);
18✔
1901

1902
        if ($settings) {
18✔
1903
            if (array_key_exists('useprofile', $settings) && !$settings['useprofile']) {
18✔
1904
                $forcedefault = TRUE;
1✔
1905
            }
1906
        }
1907

1908
        if (!$forcedefault && $atts['profile']['default']) {
18✔
1909
            # See if we can do better than a default.
1910
            $emails = $this->getEmails($atts['id']);
18✔
1911

1912
            try {
1913
                foreach ($emails as $email) {
18✔
1914
                    if (preg_match('/(.*)-g.*@user.trashnothing.com/', $email['email'], $matches)) {
5✔
1915
                        # TrashNothing has an API we can use.
1916
                        $url = "https://trashnothing.com/api/users/{$matches[1]}/profile-image?default=" . urlencode('https://' . IMAGE_DOMAIN . '/defaultprofile.png');
1✔
1917
                        $atts['profile'] = [
1✔
1918
                            'url' => $url,
1919
                            'turl' => $url,
1920
                            'default' => FALSE,
1921
                            'TN' => TRUE
1922
                        ];
1923
                    } else if (!Mail::ourDomain($email['email'])) {
5✔
1924
                        # Try for gravatar
1925
                        $gurl = $this->gravatar($email['email'], 200, 404);
5✔
1926
                        $g = @file_get_contents($gurl);
5✔
1927

1928
                        if ($g) {
5✔
1929
                            $atts['profile'] = [
1✔
1930
                                'url' => $gurl,
1931
                                'turl' => $this->gravatar($email['email'], 100, 404),
1✔
1932
                                'default' => FALSE,
1933
                                'gravatar' => TRUE
1934
                            ];
1935

1936
                            break;
1✔
1937
                        }
1938
                    }
1939
                }
1940

1941
                if ($atts['profile']['default']) {
18✔
1942
                    # Try for Facebook.
1943
                    $logins = $this->getLogins(TRUE);
18✔
1944
                    foreach ($logins as $login) {
18✔
1945
                        if ($login['type'] == User::LOGIN_FACEBOOK) {
5✔
1946
                            if (Utils::presdef('useprofile', $atts['settings'], TRUE)) {
×
1947
                                // As of October 2020 we can no longer just access the profile picture via the UID, we need to make a
1948
                                // call to the Graph API to fetch it.
1949
                                $f = new Facebook($this->dbhr, $this->dbhm);
×
1950
                                $atts['profile'] = $f->getProfilePicture($login['uid']);
×
1951
                            }
1952
                        }
1953
                    }
1954
                }
1955
            } catch (Throwable $e) {}
×
1956

1957
            $hash = NULL;
18✔
1958

1959
            if (!Utils::pres('default', $atts['profile'])) {
18✔
1960
                # We think we have a profile.  Make sure we can fetch it and filter out other people's
1961
                # default images.
1962
                $atts['profile']['default'] = TRUE;
1✔
1963
                $this->filterDefault($atts['profile'], $hash);
1✔
1964
            }
1965

1966
            if (Utils::pres('default', $atts['profile'])) {
18✔
1967
                # Nothing - so get gravatar to generate a default for us.
1968
                $atts['profile'] = [
18✔
1969
                    'url' => $this->gravatar($this->getEmailPreferred(), 200, 'identicon'),
18✔
1970
                    'turl' => $this->gravatar($this->getEmailPreferred(), 100, 'identicon'),
18✔
1971
                    'default' => FALSE,
1972
                    'gravatar' => TRUE
1973
                ];
1974
            }
1975

1976
            # Save for next time.
1977
            $this->dbhm->preExec("INSERT INTO users_images (userid, url, `default`, hash) VALUES (?, ?, ?, ?);", [
18✔
1978
                $atts['id'],
18✔
1979
                $atts['profile']['default'] ? NULL : $atts['profile']['url'],
18✔
1980
                $atts['profile']['default'],
18✔
1981
                $hash
1982
            ]);
1983
        }
1984
    }
1985

1986
    public function filterDefault(&$profile, &$hash) {
1987
        $hasher = new ImageHash;
1✔
1988
        $data = Utils::pres('url', $profile) && strlen($profile['url']) ? @file_get_contents($profile['url']) : NULL;
1✔
1989
        $hash = NULL;
1✔
1990

1991
        if ($data) {
1✔
1992
            $img = @imagecreatefromstring($data);
1✔
1993

1994
            if ($img) {
1✔
1995
                $hash = $hasher->hash($img);
1✔
1996
                $profile['default'] = FALSE;
1✔
1997
            }
1998
        }
1999

2000
        if ($hash == 'e070716060607120' || $hash == 'd0f0323171707030' || $hash == '13130f4e0e0e4e52' ||
2001
            $hash == '1f0fcf9f9f9fcfff' || $hash == '23230f0c0e0e0c24' || $hash == 'c0c0e070e0603100' ||
2002
            $hash == 'f0f0316870f07130' || $hash == '242e070e060b0d24' || $hash == '69aa49558e4da88e') {
1✔
2003
            # This is a default profile - replace it with ours.
2004
            $profile['url'] = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
2005
            $profile['turl'] = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
2006
            $profile['default'] = TRUE;
×
2007
            $hash = NULL;
×
2008
        }
2009
    }
2010

2011
    public function getPublic($groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [ MessageCollection::APPROVED ], $historyfull = FALSE)
2012
    {
2013
        $atts = [];
232✔
2014

2015
        if ($this->id) {
232✔
2016
            $users = [ $this->user ];
232✔
2017
            $rets = $this->getPublics($users, $groupids, $history, $comments, $memberof, $applied, $modmailsonly, $emailhistory, $msgcoll, $historyfull);
232✔
2018
            $atts = $rets[$this->id];
232✔
2019
        }
2020

2021
        return($atts);
232✔
2022
    }
2023

2024
    public function getPublicAtts(&$rets, $users, $me) {
2025
        foreach ($users as &$user) {
236✔
2026
            if (!array_key_exists($user['id'], $rets)) {
236✔
2027
                $rets[$user['id']] = [];
236✔
2028
            }
2029

2030
            $atts = $this->publicatts;
236✔
2031

2032
            if (Session::modtools()) {
236✔
2033
                # We have some extra attributes.
2034
                $atts[] = 'deleted';
223✔
2035
                $atts[] = 'lastaccess';
223✔
2036
                $atts[] = 'trustlevel';
223✔
2037
            }
2038

2039
            foreach ($atts as $att) {
236✔
2040
                $rets[$user['id']][$att] = Utils::presdef($att, $user, NULL);
236✔
2041
            }
2042

2043
            $rets[$user['id']]['settings'] = ['dummy' => TRUE];
236✔
2044

2045
            if (Utils::presdef('settings', $user, NULL)) {
236✔
2046
                # This is a bit of a type guddle.
2047
                if (gettype($user['settings']) == 'string') {
34✔
2048
                    $rets[$user['id']]['settings'] = json_decode($user['settings'], TRUE);
33✔
2049
                } else {
2050
                    $rets[$user['id']]['settings'] = $user['settings'];
1✔
2051
                }
2052
            }
2053

2054
            if (Utils::pres('mylocation', $rets[$user['id']]['settings']) && Utils::pres('groupsnear', $rets[$user['id']]['settings']['mylocation'])) {
236✔
2055
                # This is large - no need for it.
2056
                $rets[$user['id']]['settings']['mylocation']['groupsnear'] = NULL;
×
2057
            }
2058

2059
            $rets[$user['id']]['settings']['notificationmails'] = array_key_exists('notificationmails', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['notificationmails'] : TRUE;
236✔
2060
            $rets[$user['id']]['settings']['engagement'] = array_key_exists('engagement', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['engagement'] : TRUE;
236✔
2061
            $rets[$user['id']]['settings']['modnotifs'] = array_key_exists('modnotifs', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['modnotifs'] : 4;
236✔
2062
            $rets[$user['id']]['settings']['backupmodnotifs'] = array_key_exists('backupmodnotifs', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['backupmodnotifs'] : 12;
236✔
2063

2064
            $rets[$user['id']]['displayname'] = $this->getName(TRUE, $user);
236✔
2065

2066
            $rets[$user['id']]['added'] = array_key_exists('added', $user) ? Utils::ISODate($user['added']) : NULL;
236✔
2067

2068
            foreach (['fullname', 'firstname', 'lastname'] as $att) {
236✔
2069
                # Make sure we don't return an email if somehow one has snuck in.
2070
                $rets[$user['id']][$att] = strpos($rets[$user['id']][$att], '@') !== FALSE ? substr($rets[$user['id']][$att], 0, strpos($rets[$user['id']][$att], '@')) : $rets[$user['id']][$att];
236✔
2071
            }
2072

2073
            if ($me && $rets[$user['id']]['id'] == $me->getId()) {
236✔
2074
                # Add in private attributes for our own entry.
2075
                $rets[$user['id']]['emails'] = $me->getEmails();
135✔
2076
                $rets[$user['id']]['email'] = $me->getEmailPreferred();
135✔
2077
                $rets[$user['id']]['relevantallowed'] = $me->getPrivate('relevantallowed');
135✔
2078
                $rets[$user['id']]['permissions'] = $me->getPrivate('permissions');
135✔
2079
            }
2080

2081
            if ($me && ($me->isModerator() || $user['id'] == $me->getId())) {
236✔
2082
                # Mods can see email settings, no matter which group.
2083
                $rets[$user['id']]['onholidaytill'] = (Utils::pres('onholidaytill', $rets[$user['id']]) && (time() < strtotime($rets[$user['id']]['onholidaytill']))) ? Utils::ISODate($rets[$user['id']]['onholidaytill']) : NULL;
154✔
2084
            } else {
2085
                # Don't show some attributes unless they're a mod or ourselves.
2086
                $ismod = $rets[$user['id']]['systemrole'] == User::SYSTEMROLE_ADMIN ||
121✔
2087
                    $rets[$user['id']]['systemrole'] == User::SYSTEMROLE_SUPPORT ||
121✔
2088
                    $rets[$user['id']]['systemrole'] == User::SYSTEMROLE_MODERATOR;
121✔
2089
                $showmod = $ismod && Utils::presdef('showmod', $rets[$user['id']]['settings'], FALSE);
121✔
2090
                $rets[$user['id']]['settings']['showmod'] = $showmod;
121✔
2091
                $rets[$user['id']]['yahooid'] = NULL;
121✔
2092
            }
2093

2094
            if (Utils::pres('deleted', $rets[$user['id']])) {
236✔
2095
                $rets[$user['id']]['deleted'] = Utils::ISODate($rets[$user['id']]['deleted']);
2✔
2096
            }
2097

2098
            if (Utils::pres('lastaccess', $rets[$user['id']])) {
236✔
2099
                $rets[$user['id']]['lastaccess'] = Utils::ISODate($rets[$user['id']]['lastaccess']);
223✔
2100
            }
2101
        }
2102
    }
2103
    
2104
    public function getPublicProfiles(&$rets, $users) {
2105
        $idsleft = [];
237✔
2106

2107
        foreach ($rets as $userid => $ret) {
237✔
2108
            if (Utils::pres($userid, $users)) {
237✔
2109
                if (Utils::pres('profile', $users[$userid])) {
7✔
2110
                    $rets[$userid]['profile'] = $users[$userid]['profile'];
1✔
2111
                } else {
2112
                    $idsleft[] = $userid;
7✔
2113
                }
2114
            } else {
2115
                $idsleft[] = $userid;
233✔
2116
            }
2117
        }
2118

2119
        if (count($idsleft)) {
237✔
2120
            foreach ($idsleft as $id) {
237✔
2121
                $rets[$id]['profile'] = [
237✔
2122
                    'url' => 'https://' . IMAGE_DOMAIN . '/defaultprofile.png',
2123
                    'turl' => 'https://' . IMAGE_DOMAIN . '/defaultprofile.png',
2124
                    'default' => TRUE
2125
                ];
2126
            }
2127

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

2131
            $profiles = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
237✔
2132

2133
            foreach ($profiles as $profile) {
237✔
2134
                # Get a profile.  This function is called so frequently that we can't afford to query external sites
2135
                # within it, so if we don't find one, we default to none.
2136
                if (Utils::pres('settings', $rets[$profile['userid']]) &&
12✔
2137
                    gettype($rets[$profile['userid']]['settings']) == 'array' &&
12✔
2138
                    (!array_key_exists('useprofile', $rets[$profile['userid']]['settings']) || $rets[$profile['userid']]['settings']['useprofile'])) {
12✔
2139
                    # We found a profile that we can use.
2140
                    if (!$profile['default']) {
12✔
2141
                        # If it's a gravatar image we can return a thumbnail url that specifies a different size.
2142
                        $turl = Utils::pres('url', $profile) ? $profile['url'] : ('https://' . IMAGE_DOMAIN . "/tuimg_{$profile['id']}.jpg");
12✔
2143
                        $turl = strpos($turl, 'https://www.gravatar.com') === 0 ? str_replace('?s=200', '?s=100', $turl) : $turl;
12✔
2144
                        $rets[$profile['userid']]['profile'] = [
12✔
2145
                            'id' => $profile['id'],
12✔
2146
                            'url' => Utils::pres('url', $profile) ? $profile['url'] : ('https://' . IMAGE_DOMAIN . "/uimg_{$profile['id']}.jpg"),
12✔
2147
                            'turl' => $turl,
2148
                            'default' => FALSE
2149
                        ];
2150
                    }
2151
                }
2152
            }
2153
        }
2154
    }
2155

2156
    public function getPublicHistory($me, &$rets, $users, $historyfull, $systemrole, $msgcoll = [ MessageCollection::APPROVED ]) {
2157
        $idsleft = [];
112✔
2158

2159
        foreach ($rets as $userid => $ret) {
112✔
2160
            if (Utils::pres($userid, $users)) {
112✔
2161
                if (array_key_exists('messagehistory', $users[$userid])) {
5✔
2162
                    $rets[$userid]['messagehistory'] = $users[$userid]['messagehistory'];
1✔
2163
                    $rets[$userid]['modmails'] = $users[$userid]['modmails'];
1✔
2164
                } else {
2165
                    $idsleft[] = $userid;
5✔
2166
                }
2167
            } else {
2168
                $idsleft[] = $userid;
108✔
2169
            }
2170
        }
2171

2172
        if (count($idsleft)) {
112✔
2173
            foreach ($rets as &$atts) {
112✔
2174
                $atts['messagehistory'] = [];
112✔
2175
            }
2176

2177
            # Add in the message history - from any of the emails associated with this user.
2178
            #
2179
            # We want one entry in here for each repost, so we LEFT JOIN with the reposts table.
2180
            $sql = null;
112✔
2181

2182
            if (count($idsleft)) {
112✔
2183
                $collq = " AND messages_groups.collection IN ('" . implode("','", $msgcoll) . "') ";
112✔
2184
                $earliest = $historyfull ? '1970-01-01' : date('Y-m-d', strtotime("midnight 30 days ago"));
112✔
2185
                $delq = $historyfull ? '' : ' AND messages_groups.deleted = 0';
112✔
2186

2187
                $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(
112✔
2188
                        ',',
2189
                        $idsleft
2190
                    ) . ") $delq LEFT JOIN messages_postings ON messages.id = messages_postings.msgid HAVING postdate > ? ORDER BY postdate DESC;";
112✔
2191
            }
2192

2193
            if ($sql) {
112✔
2194
                $histories = $this->dbhr->preQuery(
112✔
2195
                    $sql,
2196
                    [
2197
                        $earliest
2198
                    ]
2199
                );
2200

2201
                foreach ($rets as $userid => $ret) {
112✔
2202
                    foreach ($histories as $history) {
112✔
2203
                        if ($history['fromuser'] == $ret['id']) {
35✔
2204
                            $history['arrival'] = Utils::pres('repostdate', $history) ? Utils::ISODate(
35✔
2205
                                $history['repostdate']
27✔
2206
                            ) : Utils::ISODate($history['arrival']);
8✔
2207
                            $history['date'] = Utils::ISODate($history['date']);
35✔
2208
                            $rets[$userid]['messagehistory'][] = $history;
35✔
2209
                        }
2210
                    }
2211
                }
2212
            }
2213

2214
            # Add in a count of recent "modmail" type logs which a mod might care about.
2215
            $modships = $me ? $me->getModeratorships() : [];
112✔
2216
            $modships = count($modships) == 0 ? [0] : $modships;
112✔
2217
            $sql = "SELECT COUNT(*) AS count, userid FROM `users_modmails` WHERE userid IN (" . implode(
112✔
2218
                    ',',
2219
                    $idsleft
2220
                ) . ") AND groupid IN (" . implode(',', $modships) . ") GROUP BY userid;";
112✔
2221
            $modmails = $this->dbhr->preQuery($sql, null, false, false);
112✔
2222

2223
            foreach ($idsleft as $userid) {
112✔
2224
                $rets[$userid]['modmails'] = 0;
112✔
2225
            }
2226

2227
            foreach ($rets as $userid => $ret) {
112✔
2228
                foreach ($modmails as $modmail) {
112✔
2229
                    if ($modmail['userid'] == $ret['id']) {
1✔
2230
                        $rets[$userid]['modmails'] = $modmail['count'] ? $modmail['count'] : 0;
1✔
2231
                    }
2232
                }
2233
            }
2234
        }
2235
    }
2236

2237
    public function getPublicMemberOf(&$rets, $me, $freeglemod, $memberof, $systemrole) {
2238
        $userids = [];
223✔
2239

2240
        foreach ($rets as $ret) {
223✔
2241
            $ret['activearea'] = NULL;
223✔
2242

2243
            if (!Utils::pres('memberof', $ret)) {
223✔
2244
                # We haven't provided the complete list already, e.g. because the user is suspect.
2245
                $userids[] = $ret['id'];
223✔
2246
            }
2247
        }
2248

2249
        if ($memberof &&
2250
            count($userids) &&
223✔
2251
            ($systemrole == User::ROLE_MODERATOR || $systemrole == User::SYSTEMROLE_ADMIN || $systemrole == User::SYSTEMROLE_SUPPORT)
223✔
2252
        ) {
2253
            # Gt the recent ones (which preserves some privacy for the user but allows us to spot abuse) and any which
2254
            # are on our groups.
2255
            $addmax = ($systemrole == User::SYSTEMROLE_ADMIN || $systemrole == User::SYSTEMROLE_SUPPORT) ? PHP_INT_MAX : 31;
74✔
2256
            $modids = array_merge([0], $me->getModeratorships());
74✔
2257
            $freegleq = $freeglemod ? " OR groups.type = 'Freegle' " : '';
74✔
2258
            $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);";
74✔
2259
            $groups = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
74✔
2260
            #error_log("Get groups $sql, {$this->id}");
2261

2262
            foreach ($rets as &$ret) {
74✔
2263
                $ret['memberof'] = [];
74✔
2264
                $ourEmailId = NULL;
74✔
2265

2266
                if (Utils::pres('emails', $ret)) {
74✔
2267
                    foreach ($ret['emails'] as $email) {
57✔
2268
                        if (Mail::ourDomain($email['email'])) {
57✔
2269
                            $ourEmailId = $email['id'];
6✔
2270
                        }
2271
                    }
2272
                }
2273

2274
                foreach ($groups as $group) {
74✔
2275
                    if ($ret['id'] ==  $group['userid']) {
62✔
2276
                        $name = $group['namefull'] ? $group['namefull'] : $group['nameshort'];
62✔
2277
                        $added = Utils::ISODate(Utils::pres('yadded', $group) ? $group['yadded'] : $group['added']);
62✔
2278
                        $addedago = floor((time() - strtotime($added)) / 86400);
62✔
2279

2280
                        $ret['memberof'][] = [
62✔
2281
                            'id' => $group['groupid'],
62✔
2282
                            'membershipid' => $group['id'],
62✔
2283
                            'namedisplay' => $name,
2284
                            'nameshort' => $group['nameshort'],
62✔
2285
                            'added' => $added,
2286
                            'collection' => $group['coll'],
62✔
2287
                            'role' => $group['role'],
62✔
2288
                            'emailfrequency' => $group['emailfrequency'],
62✔
2289
                            'eventsallowed' => $group['eventsallowed'],
62✔
2290
                            'volunteeringallowed' => $group['volunteeringallowed'],
62✔
2291
                            'ourpostingstatus' => $group['ourPostingStatus'],
62✔
2292
                            'type' => $group['type'],
62✔
2293
                            'onhere' => $group['onhere'],
62✔
2294
                            'reviewrequestedat' => $group['reviewrequestedat'] ? Utils::ISODate($group['reviewrequestedat']) : NULL,
62✔
2295
                            'reviewreason' => $group['reviewreason'],
62✔
2296
                            'reviewedat' => $group['reviewedat'] ? Utils::ISODate($group['reviewedat']) : NULL,
62✔
2297
                        ];
2298

2299

2300
                        // Counts as active if recently joined.
2301
                        if ($group['lat'] && $group['lng'] && $addedago <= 31) {
62✔
2302
                            $box = Utils::presdef('activearea', $ret, NULL);
2✔
2303

2304
                            $ret['activearea'] = [
2✔
2305
                                'swlat' => is_null($box)? $group['lat'] : min($group['lat'], $box['swlat']),
2✔
2306
                                'swlng' => is_null($box)? $group['lng'] : min($group['lng'], $box['swlng']),
2✔
2307
                                'nelng' => is_null($box)? $group['lng'] : max($group['lng'], $box['nelng']),
2✔
2308
                                'nelat' => is_null($box)? $group['lat'] : max($group['lat'], $box['nelat'])
2✔
2309
                            ];
2310
                        }
2311
                    }
2312
                }
2313
            }
2314
        }
2315
    }
2316

2317
    public function getPublicApplied(&$rets, $users, $applied, $systemrole) {
2318
        if ($applied &&
2319
            $systemrole == User::ROLE_MODERATOR ||
2320
            $systemrole == User::SYSTEMROLE_ADMIN ||
2321
            $systemrole == User::SYSTEMROLE_SUPPORT
223✔
2322
        ) {
2323
            $idsleft = [];
74✔
2324

2325
            foreach ($rets as $userid => $ret) {
74✔
2326
                if (Utils::pres($userid, $users)) {
74✔
2327
                    if (array_key_exists('applied', $users[$userid])) {
1✔
2328
                        $rets[$userid]['applied'] = $users[$userid]['applied'];
1✔
2329
                        $rets[$userid]['activedistance'] = $users[$userid]['activedistance'];
1✔
2330
                    } else {
2331
                        $idsleft[] = $userid;
1✔
2332
                    }
2333
                } else {
2334
                    $idsleft[] = $userid;
74✔
2335
                }
2336
            }
2337

2338
            if (count($idsleft)) {
74✔
2339
                # As well as being a member of a group, they might have joined and left, or applied and been rejected.
2340
                # This is useful info for moderators.
2341
                $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(
74✔
2342
                        ',',
2343
                        $idsleft
2344
                    ) . ") AND DATEDIFF(NOW(), added) <= 31 AND groups.publish = 1 AND groups.onmap = 1 ORDER BY added DESC;";
2345
                $membs = $this->dbhr->preQuery($sql);
74✔
2346

2347
                foreach ($rets as &$ret) {
74✔
2348
                    $ret['applied'] = [];
74✔
2349
                    $ret['activedistance'] = null;
74✔
2350

2351
                    foreach ($membs as $memb) {
74✔
2352
                        if ($ret['id'] == $memb['userid']) {
63✔
2353
                            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
63✔
2354
                            $memb['namedisplay'] = $name;
63✔
2355
                            $memb['added'] = Utils::ISODate($memb['added']);
63✔
2356
                            $memb['id'] = $memb['groupid'];
63✔
2357
                            unset($memb['groupid']);
63✔
2358

2359
                            if ($memb['lat'] && $memb['lng']) {
63✔
2360
                                $box = Utils::presdef('activearea', $ret, null);
2✔
2361

2362
                                $box = [
2✔
2363
                                    'swlat' => is_null($box)? $memb['lat'] : min($memb['lat'], $box['swlat']),
2✔
2364
                                    'swlng' => is_null($box)? $memb['lng'] : min($memb['lng'], $box['swlng']),
2✔
2365
                                    'nelng' => is_null($box)? $memb['lng'] : max($memb['lng'], $box['nelng']),
2✔
2366
                                    'nelat' => is_null($box)? $memb['lat'] : max($memb['lat'], $box['nelat'])
2✔
2367
                                ];
2368

2369
                                $ret['activearea'] = $box;
2✔
2370

2371
                                if ($box) {
2✔
2372
                                    $ret['activedistance'] = round(
2✔
2373
                                        Location::getDistance(
2✔
2374
                                            $box['swlat'],
2✔
2375
                                            $box['swlng'],
2✔
2376
                                            $box['nelat'],
2✔
2377
                                            $box['nelng']
2✔
2378
                                        )
2379
                                    );
2380
                                }
2381
                            }
2382

2383
                            $ret['applied'][] = $memb;
63✔
2384
                        }
2385
                    }
2386
                }
2387
            }
2388
        }
2389
    }
2390

2391
    public function getPublicSpammer(&$rets, $me, $systemrole) {
2392
        # We want to check for spammers.  If we have suitable rights then we can
2393
        # return detailed info; otherwise just that they are on the list.
2394
        #
2395
        # We don't do this for our own logged in user, otherwise we recurse to death.
2396
        $myid = $me ? $me->getId() : NULL;
223✔
2397
        $userids = array_filter(array_keys($rets), function($val) use ($myid) {
223✔
2398
            return($val != $myid);
223✔
2399
        });
2400

2401
        if (count($userids)) {
223✔
2402
            # Fetch the users.  There are so many users that there is no point trying to use the query cache.
2403
            $sql = "SELECT * FROM spam_users WHERE userid IN (" . implode(',', $userids) . ");";
144✔
2404

2405
            $users = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
144✔
2406

2407
            foreach ($rets as &$ret) {
144✔
2408
                foreach ($users as &$user) {
144✔
2409
                    if ($user['userid'] == $ret['id']) {
2✔
2410
                        if (Session::modtools() && ($systemrole == User::ROLE_MODERATOR ||
2✔
2411
                                $systemrole == User::SYSTEMROLE_ADMIN ||
2412
                                $systemrole == User::SYSTEMROLE_SUPPORT)) {
2✔
2413
                            $ret['spammer'] = [];
2✔
2414
                            foreach (['id', 'userid', 'byuserid', 'added', 'collection', 'reason'] as $att) {
2✔
2415
                                $ret['spammer'][$att]= $user[$att];
2✔
2416
                            }
2417

2418
                            $ret['spammer']['added'] = Utils::ISODate($ret['spammer']['added']);
2✔
2419
                        } else if ($user['collection'] == Spam::TYPE_SPAMMER) {
1✔
2420
                            # Only return to members that they are a spammer once approved.
2421
                            $ret['spammer'] = TRUE;
1✔
2422
                        }
2423
                    }
2424
                }
2425
            }
2426
        }
2427
    }
2428

2429
    public function getEmailHistory(&$rets) {
2430
        $userids = array_keys($rets);
4✔
2431

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

2434
        foreach ($rets as $retind => $ret) {
4✔
2435
            $rets[$retind]['emailhistory'] = [];
4✔
2436

2437
            foreach ($emails as $email) {
4✔
2438
                if ($rets[$retind]['id'] == $email['userid']) {
1✔
2439
                    $email['timestamp'] = Utils::ISODate($email['timestamp']);
1✔
2440
                    unset($email['userid']);
1✔
2441
                    $rets[$retind]['emailhistory'][] = $email;
1✔
2442
                }
2443
            }
2444
        }
2445
    }
2446

2447
    public function getPublicsById($uids, $groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [MessageCollection::APPROVED], $historyfull = FALSE) {
2448
        $rets = [];
126✔
2449

2450
        # We might have some of these in cache, especially ourselves.
2451
        $uidsleft = [];
126✔
2452

2453
        foreach ($uids as $uid) {
126✔
2454
            $u = User::get($this->dbhr, $this->dbhm, $uid, TRUE, TRUE);
123✔
2455

2456
            if ($u) {
123✔
2457
                $rets[$uid] = $u->getPublic($groupids, $history, $comments, $memberof, $applied, $modmailsonly, $emailhistory, $msgcoll, $historyfull);
118✔
2458
            } else {
2459
                $uidsleft[] = $uid;
8✔
2460
            }
2461
        }
2462

2463
        $uidsleft = array_filter($uidsleft);
126✔
2464

2465
        if (count($uidsleft)) {
126✔
2466
            $us = $this->dbhr->preQuery("SELECT * FROM users WHERE id IN (" . implode(',', $uidsleft) . ");", NULL, FALSE, FALSE);
7✔
2467
            $users = [];
7✔
2468
            foreach ($us as $u) {
7✔
2469
                $users[$u['id']] = $u;
6✔
2470
            }
2471

2472
            if (count($users)) {
7✔
2473
                $users = $this->getPublics($users, $groupids, $history, $comments, $memberof, $applied, $modmailsonly, $emailhistory, $msgcoll, $historyfull);
6✔
2474

2475
                foreach ($users as $user) {
6✔
2476
                    $rets[$user['id']] = $user;
6✔
2477
                }
2478
            }
2479
        }
2480

2481
        return($rets);
126✔
2482
    }
2483

2484
    public function isTN() {
2485
        return strpos($this->getEmailPreferred(), '@user.trashnothing.com') !== FALSE;
60✔
2486
    }
2487

2488
    public function getPublicEmails(&$rets) {
2489
        $userids = array_keys($rets);
92✔
2490
        $emails = $this->getEmailsById($userids);
92✔
2491

2492
        foreach ($rets as &$ret) {
92✔
2493
            if (Utils::pres($ret['id'], $emails)) {
91✔
2494
                $ret['emails'] = $emails[$ret['id']];
64✔
2495
            }
2496
        }
2497
    }
2498

2499
    public static function purgedUser($id) {
2500
        return [
2501
            'id' => $id,
1✔
2502
            'displayname' => 'Purged user #' . $id,
2503
            'systemrole' => User::SYSTEMROLE_USER
2504
        ];
2505
    }
2506

2507
    public function getPublicLogs($me, &$rets, $modmailsonly, &$ctx, $suppress = TRUE, $seeall = FALSE) {
2508
        # Add in the log entries we have for this user.  We exclude some logs of little interest to mods.
2509
        # - creation - either of ourselves or others during syncing.
2510
        # - deletion of users due to syncing
2511
        # Don't cache as there might be a lot, they're rarely used, and it can cause UT issues.
2512
        $myid = $me ? $me->getId() : NULL;
16✔
2513
        $uids = array_keys($rets);
16✔
2514
        $startq = $ctx ? (" AND id < " . intval($ctx['id']) . " ") : '';
16✔
2515
        $modships = $me ? $me->getModeratorships() : [];
16✔
2516
        $groupq = count($modships) ? (" AND groupid IN (" . implode(',', $modships) . ")") : '';
16✔
2517
        $modmailq = " AND ((type = 'Message' AND subtype IN ('Rejected', 'Deleted', 'Replied')) OR (type = 'User' AND subtype IN ('Mailed', 'Rejected', 'Deleted'))) $groupq";
16✔
2518
        $modq = $modmailsonly ? $modmailq : '';
16✔
2519
        $suppq = $suppress ? " AND NOT (type = 'User' AND subtype IN('Created', 'Merged', 'YahooConfirmed')) " : '';
16✔
2520
        $sql = "SELECT DISTINCT * FROM logs WHERE (user IN (" . implode(',', $uids) . ") OR byuser IN (" . implode(',', $uids) . ")) $startq $suppq $modq ORDER BY id DESC LIMIT 50;";
16✔
2521
        $logs = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
16✔
2522
        $groups = [];
16✔
2523
        $users = [];
16✔
2524
        $configs = [];
16✔
2525

2526
        # Get all log users in a single go.
2527
        $loguids = array_filter(array_merge(array_column($rets, 'user'), array_column($rets, 'byuser')));
16✔
2528

2529
        if (count($loguids)) {
16✔
2530
            $u = new User($this->dbhr, $this->dbhm);
×
2531
            $users = $u->getPublicsById($loguids, NULL, FALSE, FALSE, FALSE, FALSE);
×
2532
        }
2533

2534
        if (!$ctx) {
16✔
2535
            $ctx = ['id' => 0];
16✔
2536
        }
2537

2538
        foreach ($rets as $uid => $ret) {
16✔
2539
            $rets[$uid]['logs'] = [];
16✔
2540

2541
            foreach ($logs as $log) {
16✔
2542
                if ($log['user'] == $ret['id'] || $log['byuser'] == $ret['id']) {
15✔
2543
                    $ctx['id'] = $ctx['id'] == 0 ? $log['id'] : intval(min($ctx['id'], $log['id']));
15✔
2544

2545
                    if (Utils::pres('byuser', $log)) {
15✔
2546
                        if (!Utils::pres($log['byuser'], $users)) {
11✔
2547
                            $u = User::get($this->dbhr, $this->dbhm, $log['byuser']);
8✔
2548

2549
                            if ($u->getId() == $log['byuser']) {
8✔
2550
                                $users[$log['byuser']] = $u->getPublic(NULL, FALSE);
8✔
2551
                            } else {
2552
                                $users[$log['byuser']] = User::purgedUser($log['byuser']);
×
2553
                            }
2554
                        }
2555

2556
                        $log['byuser'] = $users[$log['byuser']];
11✔
2557
                    }
2558

2559
                    if (Utils::pres('user', $log)) {
15✔
2560
                        if (!Utils::pres($log['user'], $users)) {
14✔
2561
                            $u = User::get($this->dbhr, $this->dbhm, $log['user']);
12✔
2562

2563
                            if ($u->getId() == $log['user']) {
12✔
2564
                                $users[$log['user']] = $u->getPublic(NULL, FALSE);
12✔
2565
                            } else {
2566
                                $users[$log['user']] = User::purgedUser($log['user']);
×
2567
                            }
2568
                        }
2569

2570
                        $log['user'] = $users[$log['user']];
14✔
2571
                    }
2572

2573
                    if (Utils::pres('groupid', $log)) {
15✔
2574
                        if (!Utils::pres($log['groupid'], $groups)) {
11✔
2575
                            $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
11✔
2576

2577
                            if ($g->getId()) {
11✔
2578
                                $groups[$log['groupid']] = $g->getPublic();
11✔
2579
                                $groups[$log['groupid']]['myrole'] = $me ? $me->getRoleForGroup($log['groupid']) : User::ROLE_NONMEMBER;
11✔
2580
                            }
2581
                        }
2582

2583
                        # We can see logs for ourselves.
2584
                        if (!($myid != NULL && Utils::pres('user', $log) && Utils::presdef('id', $log['user'], NULL) == $myid) &&
11✔
2585
                            $g->getId() &&
11✔
2586
                            $groups[$log['groupid']]['myrole'] != User::ROLE_OWNER &&
11✔
2587
                            $groups[$log['groupid']]['myrole'] != User::ROLE_MODERATOR &&
11✔
2588
                            !$seeall
11✔
2589
                        ) {
2590
                            # We can only see logs for this group if we have a mod role, or if we have appropriate system
2591
                            # rights.  Skip this log.
2592
                            continue;
2✔
2593
                        }
2594

2595
                        $log['group'] = Utils::presdef($log['groupid'], $groups, NULL);
11✔
2596
                    }
2597

2598
                    if (Utils::pres('configid', $log)) {
15✔
2599
                        if (!Utils::pres($log['configid'], $configs)) {
2✔
2600
                            $c = new ModConfig($this->dbhr, $this->dbhm, $log['configid']);
2✔
2601

2602
                            if ($c->getId()) {
2✔
2603
                                $configs[$log['configid']] = $c->getPublic();
2✔
2604
                            }
2605
                        }
2606

2607
                        if (Utils::pres($log['configid'], $configs)) {
2✔
2608
                            $log['config'] = $configs[$log['configid']];
2✔
2609
                        }
2610
                    }
2611

2612
                    if (Utils::pres('stdmsgid', $log)) {
15✔
2613
                        $s = new StdMessage($this->dbhr, $this->dbhm, $log['stdmsgid']);
2✔
2614
                        $log['stdmsg'] = $s->getPublic();
2✔
2615
                    }
2616

2617
                    if (Utils::pres('msgid', $log)) {
15✔
2618
                        $m = new Message($this->dbhr, $this->dbhm, $log['msgid']);
8✔
2619

2620
                        if ($m->getID()) {
8✔
2621
                            $log['message'] = $m->getPublic(FALSE);
8✔
2622

2623
                            # If we're a mod (which we must be because we're accessing logs) we need to see the
2624
                            # envelopeto, because this is displayed in MT.  No real privacy issues in that.
2625
                            $log['message']['envelopeto'] = $m->getPrivate('envelopeto');
8✔
2626
                        } else {
2627
                            # The message has been deleted.
2628
                            $log['message'] = [
1✔
2629
                                'id' => $log['msgid'],
1✔
2630
                                'deleted' => true
2631
                            ];
2632

2633
                            # See if we can find out why.
2634
                            $sql = "SELECT * FROM logs WHERE msgid = ? AND type = 'Message' AND subtype = 'Deleted' ORDER BY id DESC LIMIT 1;";
1✔
2635
                            $deletelogs = $this->dbhr->preQuery($sql, [$log['msgid']]);
1✔
2636
                            foreach ($deletelogs as $deletelog) {
1✔
2637
                                $log['message']['deletereason'] = $deletelog['text'];
1✔
2638
                            }
2639
                        }
2640

2641
                        # Prune large attributes.
2642
                        unset($log['message']['textbody']);
8✔
2643
                        unset($log['message']['htmlbody']);
8✔
2644
                        unset($log['message']['message']);
8✔
2645
                    }
2646

2647
                    $log['timestamp'] = Utils::ISODate($log['timestamp']);
15✔
2648

2649
                    $rets[$uid]['logs'][] = $log;
15✔
2650
                }
2651
            }
2652
        }
2653

2654
        # Get merge history
2655
        $merges = [];
16✔
2656
        do {
2657
            $added = FALSE;
16✔
2658
            $sql = "SELECT * FROM logs WHERE type = 'User' AND subtype = 'Merged' AND user IN (" . implode(',', $uids) . ");";
16✔
2659
            $logs = $this->dbhr->preQuery($sql);
16✔
2660
            foreach ($logs as $log) {
16✔
2661
                #error_log("Consider merge log {$log['text']}");
2662
                if (preg_match('/Merged (.*) into (.*?) \((.*)\)/', $log['text'], $matches)) {
1✔
2663
                    #error_log("Matched " . var_export($matches, TRUE));
2664
                    #error_log("Check ids {$matches[1]} and {$matches[2]}");
2665
                    foreach ([$matches[1], $matches[2]] as $id) {
1✔
2666
                        if (!in_array($id, $uids, TRUE)) {
1✔
2667
                            $added = TRUE;
1✔
2668
                            $uids[] = $id;
1✔
2669
                            $merges[] = ['timestamp' => Utils::ISODate($log['timestamp']), 'from' => $matches[1], 'to' => $matches[2], 'reason' => $matches[3]];
1✔
2670
                        }
2671
                    }
2672
                }
2673
            }
2674
        } while ($added);
16✔
2675

2676
        $merges = array_unique($merges, SORT_REGULAR);
16✔
2677

2678
        foreach ($rets as $uid => $ret) {
16✔
2679
            $rets[$uid]['merges'] = [];
16✔
2680

2681
            foreach ($merges as $merge) {
16✔
2682
                if ($merge['from'] == $ret['id'] || $merge['to'] == $ret['id']) {
1✔
2683
                    $rets[$uid]['merges'][] = $merge;
1✔
2684
                }
2685
            }
2686
        }
2687
    }
2688

2689
    public function getPublics($users, $groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [MessageCollection::APPROVED], $historyfull = FALSE)
2690
    {
2691
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
236✔
2692
        $systemrole = $me ? $me->getPrivate('systemrole') : User::SYSTEMROLE_USER;
236✔
2693
        $freeglemod = $me && $me->isFreegleMod();
236✔
2694

2695
        $rets = [];
236✔
2696

2697
        $this->getPublicAtts($rets, $users, $me);
236✔
2698
        $this->getPublicProfiles($rets, $users);
236✔
2699
        $this->getSupporters($rets, $users);
236✔
2700

2701
        if ($systemrole == User::ROLE_MODERATOR || $systemrole == User::SYSTEMROLE_ADMIN || $systemrole == User::SYSTEMROLE_SUPPORT) {
236✔
2702
            $this->getPublicEmails($rets);
91✔
2703
        }
2704

2705
        if ($history) {
236✔
2706
            $this->getPublicHistory($me, $rets, $users, $historyfull, $systemrole, $msgcoll);
112✔
2707
        }
2708

2709
        if (Session::modtools()) {
236✔
2710
            $this->getPublicMemberOf($rets, $me, $freeglemod, $memberof, $systemrole);
223✔
2711
            $this->getPublicApplied($rets, $users, $applied, $systemrole);
223✔
2712
            $this->getPublicSpammer($rets, $me, $systemrole);
223✔
2713

2714
            if ($comments) {
223✔
2715
                $this->getComments($me, $rets);
181✔
2716
            }
2717

2718
            if ($emailhistory) {
223✔
2719
                $this->getEmailHistory($rets);
4✔
2720
            }
2721
        }
2722

2723
        return ($rets);
236✔
2724
    }
2725

2726
    public function isAdmin()
2727
    {
2728
        return ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN);
13✔
2729
    }
2730

2731
    public function isAdminOrSupport()
2732
    {
2733
        return ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN || $this->user['systemrole'] == User::SYSTEMROLE_SUPPORT);
90✔
2734
    }
2735

2736
    public function isModerator()
2737
    {
2738
        return ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN ||
444✔
2739
            $this->user['systemrole'] == User::SYSTEMROLE_SUPPORT ||
443✔
2740
            $this->user['systemrole'] == User::SYSTEMROLE_MODERATOR);
444✔
2741
    }
2742

2743
    public function systemRoleMax($role1, $role2)
2744
    {
2745
        $role = User::SYSTEMROLE_USER;
12✔
2746

2747
        if ($role1 == User::SYSTEMROLE_MODERATOR || $role2 == User::SYSTEMROLE_MODERATOR) {
12✔
2748
            $role = User::SYSTEMROLE_MODERATOR;
3✔
2749
        }
2750

2751
        if ($role1 == User::SYSTEMROLE_SUPPORT || $role2 == User::SYSTEMROLE_SUPPORT) {
12✔
2752
            $role = User::SYSTEMROLE_SUPPORT;
1✔
2753
        }
2754

2755
        if ($role1 == User::SYSTEMROLE_ADMIN || $role2 == User::SYSTEMROLE_ADMIN) {
12✔
2756
            $role = User::SYSTEMROLE_ADMIN;
1✔
2757
        }
2758

2759
        return ($role);
12✔
2760
    }
2761

2762
    public function roleMax($role1, $role2)
2763
    {
2764
        $role = User::ROLE_NONMEMBER;
14✔
2765

2766
        if ($role1 == User::ROLE_MEMBER || $role2 == User::ROLE_MEMBER) {
14✔
2767
            $role = User::ROLE_MEMBER;
13✔
2768
        }
2769

2770
        if ($role1 == User::ROLE_MODERATOR || $role2 == User::ROLE_MODERATOR) {
14✔
2771
            $role = User::ROLE_MODERATOR;
7✔
2772
        }
2773

2774
        if ($role1 == User::ROLE_OWNER || $role2 == User::ROLE_OWNER) {
14✔
2775
            $role = User::ROLE_OWNER;
2✔
2776
        }
2777

2778
        return ($role);
14✔
2779
    }
2780

2781
    public function roleMin($role1, $role2)
2782
    {
2783
        $role = User::ROLE_OWNER;
4✔
2784

2785
        if ($role1 == User::ROLE_MODERATOR || $role2 == User::ROLE_MODERATOR) {
4✔
2786
            $role = User::ROLE_MODERATOR;
4✔
2787
        }
2788

2789
        if ($role1 == User::ROLE_MEMBER || $role2 == User::ROLE_MEMBER) {
4✔
2790
            $role = User::ROLE_MEMBER;
4✔
2791
        }
2792

2793
        if ($role1 == User::ROLE_NONMEMBER || $role2 == User::ROLE_NONMEMBER) {
4✔
2794
            $role = User::ROLE_NONMEMBER;
1✔
2795
        }
2796

2797
        return ($role);
4✔
2798
    }
2799

2800
    public function merge($id1, $id2, $reason, $forcemerge = FALSE)
2801
    {
2802
        error_log("Merge $id2 into $id1, $reason");
14✔
2803

2804
        # We might not be able to merge them, if one or the other has the setting to prevent that.
2805
        $u1 = User::get($this->dbhr, $this->dbhm, $id1);
14✔
2806
        $u2 = User::get($this->dbhr, $this->dbhm, $id2);
14✔
2807
        $ret = FALSE;
14✔
2808

2809
        if ($id1 != $id2 && (($u1->canMerge() && $u2->canMerge()) || ($forcemerge))) {
14✔
2810
            #
2811
            # We want to merge two users.  At present we just merge the memberships, comments, emails and logs; we don't try to
2812
            # merge any conflicting settings.
2813
            #
2814
            # Both users might have membership of the same group, including at different levels.
2815
            #
2816
            # A useful query to find foreign key references is of this form:
2817
            #
2818
            # USE information_schema; SELECT * FROM KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'iznik' AND REFERENCED_TABLE_NAME = 'users';
2819
            #
2820
            # We avoid too much use of quoting in preQuery/preExec because quoted numbers can't use a numeric index and therefore
2821
            # perform slowly.
2822
            #error_log("Merge $id2 into $id1");
2823
            $l = new Log($this->dbhr, $this->dbhm);
12✔
2824
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
12✔
2825

2826
            $rc = $this->dbhm->beginTransaction();
12✔
2827
            $rollback = FALSE;
12✔
2828

2829
            if ($rc) {
12✔
2830
                try {
2831
                    #error_log("Started transaction");
2832
                    $rollback = TRUE;
12✔
2833

2834
                    # Merge the top-level memberships
2835
                    $id2membs = $this->dbhr->preQuery("SELECT * FROM memberships WHERE userid = $id2;");
12✔
2836
                    foreach ($id2membs as $id2memb) {
12✔
2837
                        $rc2 = $rc;
7✔
2838
                        # Jiggery-pokery with $rc for UT purposes.
2839
                        #error_log("$id2 member of {$id2memb['groupid']} ");
2840
                        $id1membs = $this->dbhr->preQuery("SELECT * FROM memberships WHERE userid = $id1 AND groupid = {$id2memb['groupid']};");
7✔
2841

2842
                        if (count($id1membs) == 0) {
7✔
2843
                            # id1 is not already a member.  Just change our id2 membership to id1.
2844
                            #error_log("...$id1 not a member, UPDATE");
2845
                            $rc2 = $this->dbhm->preExec("UPDATE IGNORE memberships SET userid = $id1 WHERE userid = $id2 AND groupid = {$id2memb['groupid']};");
2✔
2846

2847
                            #error_log("Membership UPDATE merge returned $rc2");
2848
                        } else {
2849
                            # id1 is already a member, so we really have to merge.
2850
                            #
2851
                            # Our new membership has the highest role.
2852
                            $id1memb = $id1membs[0];
6✔
2853
                            $role = User::roleMax($id1memb['role'], $id2memb['role']);
6✔
2854
                            #error_log("...as is $id1, roles {$id1memb['role']} vs {$id2memb['role']} => $role");
2855

2856
                            if ($role != $id1memb['role']) {
6✔
2857
                                $rc2 = $this->dbhm->preExec("UPDATE memberships SET role = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
1✔
2858
                                    $role
2859
                                ]);
2860
                                #error_log("Set role $rc2");
2861
                            }
2862

2863
                            if ($rc2) {
6✔
2864
                                #  Our added date should be the older of the two.
2865
                                $date = min(strtotime($id1memb['added']), strtotime($id2memb['added']));
6✔
2866
                                $mysqltime = date("Y-m-d H:i:s", $date);
6✔
2867
                                $rc2 = $this->dbhm->preExec("UPDATE memberships SET added = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
6✔
2868
                                    $mysqltime
2869
                                ]);
2870
                                #error_log("Added $rc2");
2871
                            }
2872

2873
                            # There are several attributes we want to take the non-NULL version.
2874
                            foreach (['configid', 'settings', 'heldby'] as $key) {
5✔
2875
                                #error_log("Check {$id2memb['groupid']} memb $id2 $key = " . Utils::presdef($key, $id2memb, NULL));
2876
                                if ($id2memb[$key]) {
5✔
2877
                                    if ($rc2) {
1✔
2878
                                        $rc2 = $this->dbhm->preExec("UPDATE memberships SET $key = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
1✔
2879
                                            $id2memb[$key]
1✔
2880
                                        ]);
2881
                                        #error_log("Set att $key = {$id2memb[$key]} $rc2");
2882
                                    }
2883
                                }
2884
                            }
2885
                        }
2886

2887
                        $rc = $rc2 && $rc ? $rc2 : 0;
6✔
2888
                    }
2889

2890
                    # Merge the emails.  Both might have a primary address; if so then id1 wins.
2891
                    # There is a unique index, so there can't be a conflict on email.
2892
                    if ($rc) {
11✔
2893
                        $primary = NULL;
11✔
2894
                        $foundprim = FALSE;
11✔
2895
                        $sql = "SELECT * FROM users_emails WHERE userid = $id2 AND preferred = 1;";
11✔
2896
                        $emails = $this->dbhr->preQuery($sql);
11✔
2897
                        foreach ($emails as $email) {
11✔
2898
                            $primary = $email['id'];
4✔
2899
                            $foundprim = TRUE;
4✔
2900
                        }
2901

2902
                        $sql = "SELECT * FROM users_emails WHERE userid = $id1 AND preferred = 1;";
11✔
2903
                        $emails = $this->dbhr->preQuery($sql);
11✔
2904
                        foreach ($emails as $email) {
11✔
2905
                            $primary = $email['id'];
6✔
2906
                            $foundprim = TRUE;
6✔
2907
                        }
2908

2909
                        if (!$foundprim) {
11✔
2910
                            # No primary.  Whatever we would choose for id1 should become the new one.
2911
                            $pemail = $u1->getEmailPreferred();
4✔
2912
                            $emails = $this->dbhr->preQuery("SELECT * FROM users_emails WHERE email LIKE ?;", [
4✔
2913
                                $pemail
2914
                            ]);
2915

2916
                            foreach ($emails as $email) {
4✔
2917
                                $primary = $email['id'];
4✔
2918
                            }
2919
                        }
2920

2921
                        #error_log("Merge emails");
2922
                        $sql = "UPDATE users_emails SET userid = $id1, preferred = 0 WHERE userid = $id2;";
11✔
2923
                        $rc = $this->dbhm->preExec($sql);
11✔
2924

2925
                        if ($primary) {
11✔
2926
                            $sql = "UPDATE users_emails SET preferred = 1 WHERE id = $primary;";
11✔
2927
                            $rc = $this->dbhm->preExec($sql);
11✔
2928
                        }
2929

2930
                        #error_log("Emails now " . var_export($this->dbhm->preQuery("SELECT * FROM users_emails WHERE userid = $id1;"), true));
2931
                        #error_log("Email merge returned $rc");
2932
                    }
2933

2934
                    if ($rc) {
11✔
2935
                        # Merge other foreign keys where success is less important.  For some of these there might already
2936
                        # be entries, so we do an IGNORE.
2937
                        $this->dbhm->preExec("UPDATE locations_excluded SET userid = $id1 WHERE userid = $id2;");
11✔
2938
                        $this->dbhm->preExec("UPDATE IGNORE chat_roster SET userid = $id1 WHERE userid = $id2;");
11✔
2939
                        $this->dbhm->preExec("UPDATE IGNORE sessions SET userid = $id1 WHERE userid = $id2;");
11✔
2940
                        $this->dbhm->preExec("UPDATE IGNORE spam_users SET userid = $id1 WHERE userid = $id2;");
11✔
2941
                        $this->dbhm->preExec("UPDATE IGNORE spam_users SET byuserid = $id1 WHERE byuserid = $id2;");
11✔
2942
                        $this->dbhm->preExec("UPDATE IGNORE users_addresses SET userid = $id1 WHERE userid = $id2;");
11✔
2943
                        $this->dbhm->preExec("UPDATE IGNORE users_banned SET userid = $id1 WHERE userid = $id2;");
11✔
2944
                        $this->dbhm->preExec("UPDATE IGNORE users_banned SET byuser = $id1 WHERE byuser = $id2;");
11✔
2945
                        $this->dbhm->preExec("UPDATE users_comments SET userid = $id1 WHERE userid = $id2;");
11✔
2946
                        $this->dbhm->preExec("UPDATE users_comments SET byuserid = $id1 WHERE byuserid = $id2;");
11✔
2947
                        $this->dbhm->preExec("UPDATE IGNORE users_donations SET userid = $id1 WHERE userid = $id2;");
11✔
2948
                        $this->dbhm->preExec("UPDATE IGNORE users_images SET userid = $id1 WHERE userid = $id2;");
11✔
2949
                        $this->dbhm->preExec("UPDATE IGNORE users_invitations SET userid = $id1 WHERE userid = $id2;");
11✔
2950
                        $this->dbhm->preExec("UPDATE users_logins SET userid = $id1 WHERE userid = $id2;");
11✔
2951
                        $this->dbhm->preExec("UPDATE IGNORE users_logins SET uid = $id1 WHERE userid = $id1 AND `type` = ?;", [
11✔
2952
                            User::LOGIN_NATIVE
2953
                        ]);
2954
                        $this->dbhm->preExec("UPDATE IGNORE users_nearby SET userid = $id1 WHERE userid = $id2;");
11✔
2955
                        $this->dbhm->preExec("UPDATE IGNORE users_notifications SET fromuser = $id1 WHERE fromuser = $id2;");
11✔
2956
                        $this->dbhm->preExec("UPDATE IGNORE users_notifications SET touser = $id1 WHERE touser = $id2;");
11✔
2957
                        $this->dbhm->preExec("UPDATE IGNORE users_nudges SET fromuser = $id1 WHERE fromuser = $id2;");
11✔
2958
                        $this->dbhm->preExec("UPDATE IGNORE users_nudges SET touser = $id1 WHERE touser = $id2;");
11✔
2959
                        $this->dbhm->preExec("UPDATE IGNORE users_phones SET userid = $id1 WHERE userid = $id2;");
11✔
2960
                        $this->dbhm->preExec("UPDATE IGNORE users_push_notifications SET userid = $id1 WHERE userid = $id2;");
11✔
2961
                        $this->dbhm->preExec("UPDATE IGNORE users_requests SET userid = $id1 WHERE userid = $id2;");
11✔
2962
                        $this->dbhm->preExec("UPDATE IGNORE users_requests SET completedby = $id1 WHERE completedby = $id2;");
11✔
2963
                        $this->dbhm->preExec("UPDATE IGNORE users_searches SET userid = $id1 WHERE userid = $id2;");
11✔
2964
                        $this->dbhm->preExec("UPDATE IGNORE newsfeed SET userid = $id1 WHERE userid = $id2;");
11✔
2965
                        $this->dbhm->preExec("UPDATE IGNORE messages_reneged SET userid = $id1 WHERE userid = $id2;");
11✔
2966
                        $this->dbhm->preExec("UPDATE IGNORE users_stories SET userid = $id1 WHERE userid = $id2;");
11✔
2967
                        $this->dbhm->preExec("UPDATE IGNORE users_stories_likes SET userid = $id1 WHERE userid = $id2;");
11✔
2968
                        $this->dbhm->preExec("UPDATE IGNORE users_stories_requested SET userid = $id1 WHERE userid = $id2;");
11✔
2969
                        $this->dbhm->preExec("UPDATE IGNORE users_thanks SET userid = $id1 WHERE userid = $id2;");
11✔
2970
                        $this->dbhm->preExec("UPDATE IGNORE modnotifs SET userid = $id1 WHERE userid = $id2;");
11✔
2971
                        $this->dbhm->preExec("UPDATE IGNORE teams_members SET userid = $id1 WHERE userid = $id2;");
11✔
2972
                        $this->dbhm->preExec("UPDATE IGNORE users_aboutme SET userid = $id1 WHERE userid = $id2;");
11✔
2973
                        $this->dbhm->preExec("UPDATE IGNORE ratings SET rater = $id1 WHERE rater = $id2;");
11✔
2974
                        $this->dbhm->preExec("UPDATE IGNORE ratings SET ratee = $id1 WHERE ratee = $id2;");
11✔
2975
                        $this->dbhm->preExec("UPDATE IGNORE users_replytime SET userid = $id1 WHERE userid = $id2;");
11✔
2976
                        $this->dbhm->preExec("UPDATE IGNORE messages_promises SET userid = $id1 WHERE userid = $id2;");
11✔
2977
                        $this->dbhm->preExec("UPDATE IGNORE messages_by SET userid = $id1 WHERE userid = $id2;");
11✔
2978
                        $this->dbhm->preExec("UPDATE IGNORE trysts SET user1 = $id1 WHERE user1 = $id2;");
11✔
2979
                        $this->dbhm->preExec("UPDATE IGNORE trysts SET user2 = $id1 WHERE user2 = $id2;");
11✔
2980
                        $this->dbhm->preExec("UPDATE IGNORE isochrones_users SET userid = $id1 WHERE userid = $id2;");
11✔
2981

2982
                        # Merge chat rooms.  There might have be two separate rooms already, which means that we need
2983
                        # to make sure that messages from both end up in the same one.
2984
                        $rooms = $this->dbhr->preQuery("SELECT * FROM chat_rooms WHERE (user1 = $id2 OR user2 = $id2) AND chattype IN (?,?);", [
11✔
2985
                            ChatRoom::TYPE_USER2MOD,
2986
                            ChatRoom::TYPE_USER2USER
2987
                        ]);
2988

2989
                        foreach ($rooms as $room) {
11✔
2990
                            # Now see if there is already a chat room between the destination user and whatever this
2991
                            # one is.
2992
                            switch ($room['chattype']) {
1✔
2993
                                case ChatRoom::TYPE_USER2MOD;
1✔
2994
                                    $sql = "SELECT id FROM chat_rooms WHERE user1 = $id1 AND groupid = {$room['groupid']};";
1✔
2995
                                    break;
1✔
2996
                                case ChatRoom::TYPE_USER2USER;
1✔
2997
                                    $other = $room['user1'] == $id2 ? $room['user2'] : $room['user1'];
1✔
2998
                                    $sql = "SELECT id FROM chat_rooms WHERE (user1 = $id1 AND user2 = $other) OR (user2 = $id1 AND user1 = $other);";
1✔
2999
                                    break;
1✔
3000
                            }
3001

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

3005
                            if (count($alreadys) > 0) {
1✔
3006
                                # Yes, there already is one.
3007
                                $this->dbhm->preExec("UPDATE chat_messages SET chatid = {$alreadys[0]['id']} WHERE chatid = {$room['id']}");
1✔
3008
                            } else {
3009
                                # No, there isn't, so we can update our old one.
3010
                                $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✔
3011
                                $this->dbhm->preExec($sql);
1✔
3012
                            }
3013
                        }
3014

3015
                        $this->dbhm->preExec("UPDATE chat_messages SET userid = $id1 WHERE userid = $id2;");
11✔
3016
                    }
3017

3018
                    # Merge attributes we want to keep if we have them in id2 but not id1.  Some will have unique
3019
                    # keys, so update to delete them.
3020
                    foreach (['fullname', 'firstname', 'lastname', 'yahooid'] as $att) {
11✔
3021
                        $users = $this->dbhm->preQuery("SELECT $att FROM users WHERE id = $id2;");
11✔
3022
                        foreach ($users as $user) {
11✔
3023
                            $this->dbhm->preExec("UPDATE users SET $att = NULL WHERE id = $id2;");
11✔
3024
                            User::clearCache($id1);
11✔
3025
                            User::clearCache($id2);
11✔
3026

3027
                            if (!$u1->getPrivate($att)) {
11✔
3028
                                if ($att != 'fullname') {
11✔
3029
                                    $this->dbhm->preExec("UPDATE users SET $att = ? WHERE id = $id1 AND $att IS NULL;", [$user[$att]]);
11✔
3030
                                } else if (stripos($user[$att], 'fbuser') === FALSE && stripos($user[$att], '-owner') === FALSE) {
4✔
3031
                                    # We don't want to overwrite a name with FBUser or a -owner address.
3032
                                    $this->dbhm->preExec("UPDATE users SET $att = ? WHERE id = $id1;", [$user[$att]]);
4✔
3033
                                }
3034
                            }
3035
                        }
3036
                    }
3037

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

3042
                        #error_log("Log merge 1 returned $rc");
3043
                    }
3044

3045
                    if ($rc) {
11✔
3046
                        $rc = $this->dbhm->preExec("UPDATE logs SET byuser = $id1 WHERE byuser = $id2;");
11✔
3047

3048
                        #error_log("Log merge 2 returned $rc");
3049
                    }
3050

3051
                    # Merge the fromuser in messages.  There might not be any, and it's not the end of the world
3052
                    # if this info isn't correct, so ignore the rc.
3053
                    #error_log("Merge messages, current rc $rc");
3054
                    if ($rc) {
11✔
3055
                        $this->dbhm->preExec("UPDATE messages SET fromuser = $id1 WHERE fromuser = $id2;");
11✔
3056
                    }
3057

3058
                    # Merge the history
3059
                    #error_log("Merge history, current rc $rc");
3060
                    if ($rc) {
11✔
3061
                        $this->dbhm->preExec("UPDATE messages_history SET fromuser = $id1 WHERE fromuser = $id2;");
11✔
3062
                        $this->dbhm->preExec("UPDATE memberships_history SET userid = $id1 WHERE userid = $id2;");
11✔
3063
                    }
3064

3065
                    # Merge the systemrole.
3066
                    $u1s = $this->dbhr->preQuery("SELECT systemrole FROM users WHERE id = $id1;");
11✔
3067
                    foreach ($u1s as $u1) {
11✔
3068
                        $u2s = $this->dbhr->preQuery("SELECT systemrole FROM users WHERE id = $id2;");
11✔
3069
                        foreach ($u2s as $u2) {
11✔
3070
                            $rc = $this->dbhm->preExec("UPDATE users SET systemrole = ? WHERE id = $id1;", [
11✔
3071
                                $this->systemRoleMax($u1['systemrole'], $u2['systemrole'])
11✔
3072
                            ]);
3073
                        }
3074
                        User::clearCache($id1);
11✔
3075
                    }
3076

3077
                    # Merge the add date.
3078
                    $u1 = User::get($this->dbhr, $this->dbhm, $id1);
11✔
3079
                    $u2 = User::get($this->dbhr, $this->dbhm, $id2);
11✔
3080
                    $this->dbhm->preExec("UPDATE users SET added = ? WHERE id = $id1;", [
11✔
3081
                        strtotime($u1->getPrivate('added')) < strtotime($u2->getPrivate('added')) ? $u1->getPrivate('added') : $u2->getPrivate('added')
11✔
3082
                    ]);
3083

3084
                    $this->dbhm->preExec("UPDATE users SET lastupdated = NOW() WHERE id = ?;", [
11✔
3085
                        $id1
3086
                    ]);
3087

3088
                    $tnid1 = $u2->getPrivate('tnuserid');
11✔
3089
                    $tnid2 = $u2->getPrivate('tnuserid');
11✔
3090

3091
                    if (!$tnid1 && $tnid2) {
11✔
3092
                        $u2->setPrivate('tnuserid', NULL);
×
3093
                        $u1->setPrivate('tnuserid', $tnid2);
×
3094
                    }
3095

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

3100
                    if (count($giftaids)) {
11✔
3101
                        $weights = [
1✔
3102
                            Donations::PERIOD_PAST_4_YEARS_AND_FUTURE => 0,
3103
                            Donations::PERIOD_SINCE => 1,
3104
                            Donations::PERIOD_FUTURE=> 2,
3105
                            Donations::PERIOD_THIS => 3,
3106
                            Donations::PERIOD_DECLINED => 4
3107
                        ];
3108

3109
                        $best = NULL;
1✔
3110
                        foreach ($giftaids as $giftaid) {
1✔
3111
                            if ($best == NULL ||
3112
                                $weights[$giftaid['period']] < $weights[$best['period']]) {
1✔
3113
                                $best = $giftaid;
1✔
3114
                            }
3115
                        }
3116

3117
                        foreach ($giftaids as $giftaid) {
1✔
3118
                            if ($giftaid['id'] != $best['id']) {
1✔
3119
                                $this->dbhm->preExec("DELETE FROM giftaid WHERE id = ?;", [
1✔
3120
                                    $giftaid['id']
1✔
3121
                                ]);
3122
                            }
3123
                        }
3124

3125
                        $this->dbhm->preExec("UPDATE giftaid SET userid = ? WHERE id = ?;", [
1✔
3126
                            $id1,
3127
                            $best['id']
1✔
3128
                        ]);
3129
                    }
3130

3131
                    if ($rc) {
11✔
3132
                        # Log the merge - before the delete otherwise we will fail to log it.
3133
                        $l->log([
11✔
3134
                            'type' => Log::TYPE_USER,
3135
                            'subtype' => Log::SUBTYPE_MERGED,
3136
                            'user' => $id2,
3137
                            'byuser' => $me ? $me->getId() : NULL,
11✔
3138
                            'text' => "Merged $id2 into $id1 ($reason)"
11✔
3139
                        ]);
3140

3141
                        # Log under both users to make sure we can trace it.
3142
                        $l->log([
11✔
3143
                            'type' => Log::TYPE_USER,
3144
                            'subtype' => Log::SUBTYPE_MERGED,
3145
                            'user' => $id1,
3146
                            'byuser' => $me ? $me->getId() : NULL,
11✔
3147
                            'text' => "Merged $id2 into $id1 ($reason)"
11✔
3148
                        ]);
3149
                    }
3150

3151
                    if ($rc) {
11✔
3152
                        # Everything worked.
3153
                        $rollback = FALSE;
11✔
3154

3155
                        # We might have merged ourself!
3156
                        if (Utils::pres('id', $_SESSION) == $id2) {
11✔
3157
                            $_SESSION['id'] = $id1;
11✔
3158
                        }
3159
                    }
3160
                } catch (\Exception $e) {
1✔
3161
                    error_log("Merge exception " . $e->getMessage());
1✔
3162
                    $rollback = TRUE;
1✔
3163
                }
3164
            }
3165

3166
            if ($rollback) {
12✔
3167
                # Something went wrong.
3168
                #error_log("Merge failed, rollback");
3169
                $this->dbhm->rollBack();
1✔
3170
                $ret = FALSE;
1✔
3171
            } else {
3172
                #error_log("Merge worked, commit");
3173
                $ret = $this->dbhm->commit();
11✔
3174

3175
                if ($ret) {
11✔
3176
                    # Finally, delete id2.  We used to this inside the transaction, but the result was that
3177
                    # fromuser sometimes got set to NULL on messages owned by id2, despite them having been set to
3178
                    # id1 earlier on.  Either we're dumb, or there's a subtle interaction between transactions,
3179
                    # foreign keys and Percona clusters.  This is safer and proves to be more reliable.
3180
                    #
3181
                    # Make sure we don't pick up an old cached version, as we've just changed it quite a bit.
3182
                    error_log("Merged $id1 < $id2, $reason");
11✔
3183
                    $deleteme = new User($this->dbhm, $this->dbhm, $id2);
11✔
3184
                    $rc = $deleteme->delete(NULL, NULL, NULL, FALSE);
11✔
3185
                }
3186
            }
3187
        }
3188

3189
        return ($ret);
14✔
3190
    }
3191

3192
    public function mailer($user, $modmail, $toname, $to, $bcc, $fromname, $from, $subject, $text) {
3193
        # These mails don't need tracking, so we don't call addHeaders.
3194
        try {
3195
            #error_log(session_id() . " mail " . microtime(true));
3196

3197
            list ($transport, $mailer) = Mail::getMailer();
4✔
3198

3199
            $message = \Swift_Message::newInstance()
4✔
3200
                ->setSubject($subject)
4✔
3201
                ->setFrom([$from => $fromname])
4✔
3202
                ->setTo([$to => $toname])
4✔
3203
                ->setBody($text);
3✔
3204

3205
            # We add some headers so that if we receive this back, we can identify it as a mod mail.
3206
            $headers = $message->getHeaders();
3✔
3207

3208
            if ($user) {
3✔
3209
                $headers->addTextHeader('X-Iznik-From-User', $user->getId());
3✔
3210
            }
3211

3212
            $headers->addTextHeader('X-Iznik-ModMail', $modmail);
3✔
3213

3214
            if ($bcc) {
3✔
3215
                $message->setBcc(explode(',', $bcc));
1✔
3216
            }
3217

3218
            $this->sendIt($mailer, $message);
3✔
3219

3220
            # Stop the transport, otherwise the message doesn't get sent until the UT script finishes.
3221
            $transport->stop();
3✔
3222

3223
            #error_log(session_id() . " mailed " . microtime(true));
3224
        } catch (\Exception $e) {
4✔
3225
            # Not much we can do - shouldn't really happen given the failover transport.
3226
            // @codeCoverageIgnoreStart
3227
            error_log("Send failed with " . $e->getMessage());
3228
            // @codeCoverageIgnoreEnd
3229
        }
3230
    }
3231

3232
    private function maybeMail($groupid, $subject, $body, $action)
3233
    {
3234
        if ($body) {
4✔
3235
            # We have a mail to send.
3236
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3237
            $myid = $me->getId();
4✔
3238

3239
            $g = Group::get($this->dbhr, $this->dbhm, $groupid);
4✔
3240
            $atts = $g->getPublic();
4✔
3241

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

3244
            # Find who to send it from.  If we have a config to use for this group then it will tell us.
3245
            $name = $me->getName();
4✔
3246
            $c = new ModConfig($this->dbhr, $this->dbhm);
4✔
3247
            $cid = $c->getForGroup($me->getId(), $groupid);
4✔
3248
            $c = new ModConfig($this->dbhr, $this->dbhm, $cid);
4✔
3249
            $fromname = $c->getPrivate('fromname');
4✔
3250
            $name = ($fromname == 'Groupname Moderator') ? '$groupname Moderator' : $name;
4✔
3251

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

3255
            $bcc = $c->getBcc($action);
4✔
3256

3257
            if ($bcc) {
4✔
3258
                $bcc = str_replace('$groupname', $atts['nameshort'], $bcc);
1✔
3259
            }
3260

3261
            # We add the message into chat.
3262
            $r = new ChatRoom($this->dbhr, $this->dbhm);
4✔
3263
            $rid = $r->createUser2Mod($this->id, $groupid);
4✔
3264
            $m = NULL;
4✔
3265

3266
            $to = $this->getEmailPreferred();
4✔
3267

3268
            if ($rid) {
4✔
3269
                # Create the message.  Mark it as needing review to prevent timing window.
3270
                $m = new ChatMessage($this->dbhr, $this->dbhm);
4✔
3271
                list ($mid, $banned) = $m->create($rid,
4✔
3272
                    $myid,
3273
                    "$subject\r\n\r\n$body",
4✔
3274
                    ChatMessage::TYPE_MODMAIL,
3275
                    NULL,
3276
                    TRUE,
3277
                    NULL,
3278
                    NULL,
3279
                    NULL,
3280
                    NULL,
3281
                    NULL,
3282
                    TRUE,
3283
                    TRUE);
3284

3285
                $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✔
3286
            }
3287

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

3294
                # We've mailed the message out so they are up to date with this chat.
3295
                $r->upToDate($this->id);
3✔
3296
            }
3297

3298
            if ($m) {
4✔
3299
                # We, as a mod, have seen this message - update the roster to show that.  This avoids this message
3300
                # appearing as unread to us.
3301
                $r->updateRoster($myid, $mid);
4✔
3302

3303
                # Ensure that the other mods are present in the roster with the message seen/unseen depending on
3304
                # whether that's what we want.
3305
                $mods = $g->getMods();
4✔
3306
                foreach ($mods as $mod) {
4✔
3307
                    if ($mod != $myid) {
3✔
3308
                        if ($c->getPrivate('chatread')) {
2✔
3309
                            # We want to mark it as seen for all mods.
3310
                            $r->updateRoster($mod, $mid, ChatRoom::STATUS_AWAY);
1✔
3311
                        } else {
3312
                            # Leave it unseen, but make sure they're in the roster.
3313
                            $r->updateRoster($mod, NULL, ChatRoom::STATUS_AWAY);
1✔
3314
                        }
3315
                    }
3316
                }
3317

3318
                if ($c->getPrivate('chatread')) {
4✔
3319
                    $m->setPrivate('mailedtoall', 1);
1✔
3320
                    $m->setPrivate('seenbyall', 1);
1✔
3321
                }
3322

3323
                # Allow mailing to happen.
3324
                $m->setPrivate('reviewrequired', 0);
4✔
3325
            }
3326
        }
3327
    }
3328

3329
    public function mail($groupid, $subject, $body, $stdmsgid, $action = NULL)
3330
    {
3331
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3332

3333
        $this->log->log([
4✔
3334
            'type' => Log::TYPE_USER,
3335
            'subtype' => Log::SUBTYPE_MAILED,
3336
            'user' => $this->id,
4✔
3337
            'byuser' => $me ? $me->getId() : NULL,
4✔
3338
            'text' => $subject,
3339
            'groupid' => $groupid,
3340
            'stdmsgid' => $stdmsgid
3341
        ]);
3342

3343
        $this->maybeMail($groupid, $subject, $body, $action);
4✔
3344
    }
3345

3346
    public function happinessReviewed($happinessid) {
3347
        $this->dbhm->preExec("UPDATE messages_outcomes SET reviewed = 1 WHERE id = ?", [
1✔
3348
            $happinessid
3349
        ]);
3350
    }
3351

3352
    public function getCommentsForSingleUser($userid) {
3353
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
5✔
3354
        $rets = [
5✔
3355
            $userid => [
3356
                'id' => $userid
3357
            ]
3358
        ];
3359

3360
        $this->getComments($me, $rets);
5✔
3361

3362
        return Utils::presdef('comments', $rets[$userid], NULL);
5✔
3363
    }
3364

3365
    public function getComments($me, &$rets)
3366
    {
3367
        $userids = array_keys($rets);
183✔
3368

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

3377
            $commentuids = [];
74✔
3378
            foreach ($comments as $comment) {
74✔
3379
                if (Utils::pres('byuserid', $comment)) {
2✔
3380
                    $commentuids[] = $comment['byuserid'];
2✔
3381
                }
3382
            }
3383

3384
            $commentusers = [];
74✔
3385

3386
            if ($commentuids && count($commentuids)) {
74✔
3387
                $commentusers = $this->getPublicsById($commentuids, NULL, FALSE, FALSE);
2✔
3388

3389
                foreach ($commentusers as &$commentuser) {
2✔
3390
                    $commentuser['settings'] = NULL;
2✔
3391
                }
3392
            }
3393

3394
            foreach ($rets as $retind => $ret) {
74✔
3395
                $rets[$retind]['comments'] = [];
74✔
3396

3397
                for ($commentind = 0; $commentind < count($comments); $commentind++) {
74✔
3398
                    if ($comments[$commentind]['userid'] == $rets[$retind]['id']) {
2✔
3399
                        $comments[$commentind]['date'] = Utils::ISODate($comments[$commentind]['date']);
2✔
3400
                        $comments[$commentind]['reviewed'] = Utils::ISODate($comments[$commentind]['reviewed']);
2✔
3401

3402
                        if (Utils::pres('byuserid', $comments[$commentind])) {
2✔
3403
                            $comments[$commentind]['byuser'] = $commentusers[$comments[$commentind]['byuserid']];
2✔
3404
                        }
3405

3406
                        $rets[$retind]['comments'][] = $comments[$commentind];
2✔
3407
                    }
3408
                }
3409
            }
3410
        }
3411
    }
3412

3413
    public function listComments(&$ctx, $groupid = NULL) {
3414
        $comments = [];
1✔
3415
        $ctxq = '';
1✔
3416

3417
        if ($ctx && Utils::pres('reviewed', $ctx)) {
1✔
3418
            $ctxq = "users_comments.reviewed < " . $this->dbhr->quote($ctx['reviewed']) . " AND ";
1✔
3419
        }
3420

3421
        $groupq = $groupid ? " groupid = $groupid AND " : '';
1✔
3422

3423
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
3424
        $groupids = $me->getModeratorships();
1✔
3425

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

3431
            $uids = array_unique(array_merge(array_column($comments, 'byuserid'), array_column($comments, 'userid')));
1✔
3432
            $u = new User($this->dbhr, $this->dbhm);
1✔
3433
            $users = $u->getPublicsById($uids, NULL, FALSE, FALSE, FALSE, FALSE);
1✔
3434

3435
            foreach ($comments as &$comment) {
1✔
3436
                $comment['date'] = Utils::ISODate($comment['date']);
1✔
3437
                $comment['reviewed'] = Utils::ISODate($comment['reviewed']);
1✔
3438

3439
                if (Utils::pres('userid', $comment)) {
1✔
3440
                    $comment['user'] = $users[$comment['userid']];
1✔
3441
                    unset($comment['userid']);
1✔
3442
                }
3443

3444
                if (Utils::pres('byuserid', $comment)) {
1✔
3445
                    $comment['byuser'] = $users[$comment['byuserid']];
1✔
3446
                    unset($comment['byuserid']);
1✔
3447
                }
3448

3449
                $ctx['reviewed'] = $comment['reviewed'];
1✔
3450
            }
3451
        }
3452

3453
        return $comments;
1✔
3454
    }
3455

3456
    public function getComment($id)
3457
    {
3458
        # We can only see comments on groups on which we have mod status.
3459
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3460
        $groupids = $me ? $me->getModeratorships() : [];
2✔
3461
        $groupids = count($groupids) == 0 ? [0] : $groupids;
2✔
3462

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

3466
        foreach ($comments as &$comment) {
2✔
3467
            $comment['date'] = Utils::ISODate($comment['date']);
2✔
3468
            $comment['reviewed'] = Utils::ISODate($comment['reviewed']);
2✔
3469

3470
            if (Utils::pres('byuserid', $comment)) {
2✔
3471
                $u = User::get($this->dbhr, $this->dbhm, $comment['byuserid']);
2✔
3472
                $comment['byuser'] = $u->getPublic();
2✔
3473
            }
3474

3475
            return ($comment);
2✔
3476
        }
3477

3478
        return (NULL);
1✔
3479
    }
3480

3481
    public function addComment($groupid, $user1 = NULL, $user2 = NULL, $user3 = NULL, $user4 = NULL, $user5 = NULL,
3482
                               $user6 = NULL, $user7 = NULL, $user8 = NULL, $user9 = NULL, $user10 = NULL,
3483
                               $user11 = NULL, $byuserid = NULL, $checkperms = TRUE, $flag = FALSE)
3484
    {
3485
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
7✔
3486

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

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

3495
        foreach ($groups as $modgroupid) {
7✔
3496
            if ($groupid == $modgroupid) {
7✔
3497
                $sql = "INSERT INTO users_comments (userid, groupid, byuserid, user1, user2, user3, user4, user5, user6, user7, user8, user9, user10, user11, flag) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
6✔
3498
                $this->dbhm->preExec($sql, [
6✔
3499
                    $this->id,
6✔
3500
                    $groupid,
3501
                    $byuserid,
3502
                    $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
3503
                    $flag ? 1 : 0
6✔
3504
                ]);
3505

3506
                $rc = $this->dbhm->lastInsertId();
6✔
3507

3508
                $added = TRUE;
6✔
3509
            }
3510
        }
3511

3512
        if (!$added && $me && $me->isAdminOrSupport()) {
7✔
3513
            $sql = "INSERT INTO users_comments (userid, groupid, byuserid, user1, user2, user3, user4, user5, user6, user7, user8, user9, user10, user11, flag) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
1✔
3514
            $this->dbhm->preExec($sql, [
1✔
3515
                $this->id,
1✔
3516
                NULL,
3517
                $byuserid,
3518
                $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
3519
                $flag ? 1 : 0
1✔
3520
            ]);
3521

3522
            $rc = $this->dbhm->lastInsertId();
1✔
3523
        }
3524

3525
        if ($rc && $flag) {
7✔
3526
            $this->flagOthers($groupid);
1✔
3527
        }
3528

3529
        return ($rc);
7✔
3530
    }
3531

3532
    private function flagOthers($groupid) {
3533
        # We want to flag this to any other groups that the member is on.
3534
        $membs = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ? AND groupid != ?;", [
2✔
3535
            $this->id,
2✔
3536
            $groupid
3537
        ]);
3538

3539
        foreach ($membs as $memb) {
2✔
3540
            $this->memberReview($memb['groupid'], TRUE, 'Note flagged to other groups');
2✔
3541
        }
3542
    }
3543

3544
    public function editComment($id, $user1 = NULL, $user2 = NULL, $user3 = NULL, $user4 = NULL, $user5 = NULL,
3545
                                $user6 = NULL, $user7 = NULL, $user8 = NULL, $user9 = NULL, $user10 = NULL,
3546
                                $user11 = NULL, $flag = FALSE)
3547
    {
3548
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
3549

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

3553
        # Can only edit comments for a group on which we're a mod.  This code isn't that efficient but it doesn't
3554
        # happen often.
3555
        $rc = NULL;
3✔
3556
        $comments = $this->dbhr->preQuery("SELECT id, groupid FROM users_comments WHERE id = ?;", [
3✔
3557
            $id
3558
        ]);
3559

3560
        foreach ($comments as $comment) {
3✔
3561
            if ($me && ($me->isAdminOrSupport() || $me->isModOrOwner($comment['groupid']))) {
3✔
3562
                $sql = "UPDATE users_comments SET byuserid = ?, user1 = ?, user2 = ?, user3 = ?, user4 = ?, user5 = ?, user6 = ?, user7 = ?, user8 = ?, user9 = ?, user10 = ?, user11 = ?, reviewed = NOW(), flag = ? WHERE id = ?;";
3✔
3563
                $rc = $this->dbhm->preExec($sql, [
3✔
3564
                    $byuserid,
3565
                    $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
3566
                    $flag,
3567
                    $comment['id']
3✔
3568
                ]);
3569

3570
                if ($rc && $flag) {
3✔
3571
                    $this->flagOthers($comment['groupid']);
1✔
3572
                }
3573
            }
3574
        }
3575

3576
        return ($rc);
3✔
3577
    }
3578

3579
    public function deleteComment($id)
3580
    {
3581
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3582

3583
        # Can only delete comments for a group on which we're a mod.
3584
        $rc = FALSE;
2✔
3585

3586
        $comments = $this->dbhr->preQuery("SELECT id, groupid FROM users_comments WHERE id = ?;", [
2✔
3587
            $id
3588
        ]);
3589

3590
        foreach ($comments as $comment) {
2✔
3591
            if ($me && ($me->isAdminOrSupport() || $me->isModOrOwner($comment['groupid']))) {
2✔
3592
                $rc = $this->dbhm->preExec("DELETE FROM users_comments WHERE id = ?;", [$id]);
2✔
3593
            }
3594
        }
3595

3596
        return ($rc);
2✔
3597
    }
3598

3599
    public function deleteComments()
3600
    {
3601
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
3602

3603
        # Can only delete comments for a group on which we're a mod.
3604
        $rc = FALSE;
1✔
3605
        $groups = $me ? $me->getModeratorships() : [];
1✔
3606
        foreach ($groups as $modgroupid) {
1✔
3607
            $rc = $this->dbhm->preExec("DELETE FROM users_comments WHERE userid = ? AND groupid = ?;", [$this->id, $modgroupid]);
1✔
3608
        }
3609

3610
        return ($rc);
1✔
3611
    }
3612

3613
    public function split($email, $name = NULL)
3614
    {
3615
        # We want to ensure that the current user has no reference to these values.
3616
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3617
        $l = new Log($this->dbhr, $this->dbhm);
2✔
3618
        if ($email) {
2✔
3619
            $this->removeEmail($email);
2✔
3620
        }
3621

3622
        $l->log([
2✔
3623
            'type' => Log::TYPE_USER,
3624
            'subtype' => Log::SUBTYPE_SPLIT,
3625
            'user' => $this->id,
2✔
3626
            'byuser' => $me ? $me->getId() : NULL,
2✔
3627
            'text' => "Split out $email"
2✔
3628
        ]);
3629

3630
        $u = new User($this->dbhr, $this->dbhm);
2✔
3631
        $uid2 = $u->create(NULL, NULL, $name);
2✔
3632
        $u->addEmail($email);
2✔
3633

3634
        # We might be able to move some messages over.
3635
        $this->dbhm->preExec("UPDATE messages SET fromuser = ? WHERE fromaddr = ?;", [
2✔
3636
            $uid2,
3637
            $email
3638
        ]);
3639
        $this->dbhm->preExec("UPDATE messages_history SET fromuser = ? WHERE fromaddr = ?;", [
2✔
3640
            $uid2,
3641
            $email
3642
        ]);
3643

3644
        # Chats which reference the messages sent from that email must also be intended for the split user.
3645
        $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✔
3646
            $email
3647
        ]);
3648

3649
        foreach ($chats as $chat) {
2✔
3650
            if ($chat['user1'] == $this->id) {
1✔
3651
                $this->dbhm->preExec("UPDATE chat_rooms SET user1 = ? WHERE id = ?;", [
1✔
3652
                    $uid2,
3653
                    $chat['id']
1✔
3654
                ]);
3655
            }
3656

3657
            if ($chat['user2'] == $this->id) {
1✔
3658
                $this->dbhm->preExec("UPDATE chat_rooms SET user2 = ? WHERE id = ?;", [
1✔
3659
                    $uid2,
3660
                    $chat['id']
1✔
3661
                ]);
3662
            }
3663
        }
3664

3665
        # We might have a name.
3666
        $this->dbhm->preExec("UPDATE users SET fullname = (SELECT fromname FROM messages WHERE fromaddr = ? LIMIT 1) WHERE id = ?;", [
2✔
3667
            $email,
3668
            $uid2
3669
        ]);
3670

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

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

3679
        return ($uid2);
2✔
3680
    }
3681

3682
    public function welcome($email, $password)
3683
    {
3684
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
5✔
3685
        $twig = new \Twig_Environment($loader);
5✔
3686

3687
        $html = $twig->render('welcome/welcome.html', [
5✔
3688
            'email' => $email,
3689
            'password' => $password
3690
        ]);
3691

3692
        $message = \Swift_Message::newInstance()
5✔
3693
            ->setSubject("Welcome to " . SITE_NAME . "!")
5✔
3694
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
5✔
3695
            ->setTo($email)
5✔
3696
            ->setBody("Thanks for joining" . SITE_NAME . "!" . ($password ? "  Here's your password: $password." : ''));
5✔
3697

3698
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3699
        # Outlook.
3700
        $htmlPart = \Swift_MimePart::newInstance();
5✔
3701
        $htmlPart->setCharset('utf-8');
5✔
3702
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
5✔
3703
        $htmlPart->setContentType('text/html');
5✔
3704
        $htmlPart->setBody($html);
5✔
3705
        $message->attach($htmlPart);
5✔
3706

3707
        Mail::addHeaders($message, Mail::WELCOME, $this->getId());
5✔
3708

3709
        list ($transport, $mailer) = Mail::getMailer();
5✔
3710
        $this->sendIt($mailer, $message);
5✔
3711
    }
3712

3713
    public function forgotPassword($email)
3714
    {
3715
        $link = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FORGOT_PASSWORD, TRUE);
1✔
3716

3717
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/welcome');
1✔
3718
        $twig = new \Twig_Environment($loader);
1✔
3719

3720
        $html = $twig->render('forgotpassword.html', [
1✔
3721
            'email' => $this->getEmailPreferred(),
1✔
3722
            'url' => $link,
3723
        ]);
3724

3725
        $message = \Swift_Message::newInstance()
1✔
3726
            ->setSubject("Forgot your password?")
1✔
3727
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
1✔
3728
            ->setTo($email)
1✔
3729
            ->setBody("To set a new password, just log in here: $link");
1✔
3730

3731
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3732
        # Outlook.
3733
        $htmlPart = \Swift_MimePart::newInstance();
1✔
3734
        $htmlPart->setCharset('utf-8');
1✔
3735
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
3736
        $htmlPart->setContentType('text/html');
1✔
3737
        $htmlPart->setBody($html);
1✔
3738
        $message->attach($htmlPart);
1✔
3739

3740
        Mail::addHeaders($message, Mail::FORGOT_PASSWORD, $this->getId());
1✔
3741

3742
        list ($transport, $mailer) = Mail::getMailer();
1✔
3743
        $this->sendIt($mailer, $message);
1✔
3744
    }
3745

3746
    public function verifyEmail($email, $force = false)
3747
    {
3748
        # If this is one of our current emails, then we can just make it the primary.
3749
        $emails = $this->getEmails();
4✔
3750
        $handled = FALSE;
4✔
3751

3752
        if (!$force) {
4✔
3753
            foreach ($emails as $anemail) {
4✔
3754
                if ($anemail['email'] == $email) {
4✔
3755
                    # It's one of ours already; make sure it's flagged as primary.
3756
                    $this->addEmail($email, 1);
2✔
3757
                    $handled = TRUE;
2✔
3758
                }
3759
            }
3760
        }
3761

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

3769
            do {
3770
                # Loop in case of clash on the key we happen to invent.
3771
                $key = uniqid();
4✔
3772
                $sql = "INSERT INTO users_emails (email, canon, validatekey, backwards) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE validatekey = ?;";
4✔
3773
                $this->dbhm->preExec($sql,
4✔
3774
                    [$email, $canon, $key, strrev($canon), $key]);
4✔
3775
            } while (!$this->dbhm->rowsAffected());
4✔
3776

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

3779
            list ($transport, $mailer) = Mail::getMailer();
4✔
3780
            $html = verify_email($email, $confirm, $usersite ? USERLOGO : MODLOGO);
4✔
3781

3782
            $message = \Swift_Message::newInstance()
4✔
3783
                ->setSubject("Please verify your email")
4✔
3784
                ->setFrom([NOREPLY_ADDR => SITE_NAME])
4✔
3785
                ->setReturnPath($this->getBounce())
4✔
3786
                ->setTo([$email => $this->getName()])
4✔
3787
                ->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");
4✔
3788

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

3798
            Mail::addHeaders($message, Mail::VERIFY_EMAIL, $this->getId());
4✔
3799

3800
            $this->sendIt($mailer, $message);
4✔
3801
        }
3802

3803
        return ($handled);
4✔
3804
    }
3805

3806
    public function confirmEmail($key)
3807
    {
3808
        $rc = FALSE;
2✔
3809
        $sql = "SELECT * FROM users_emails WHERE validatekey = ?;";
2✔
3810
        $mails = $this->dbhr->preQuery($sql, [$key]);
2✔
3811
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3812

3813
        foreach ($mails as $mail) {
2✔
3814
            if ($mail['userid'] && $mail['userid'] != $me->getId()) {
2✔
3815
                # This email belongs to another user.  But we've confirmed that it is ours.  So merge.
3816
                $this->merge($this->id, $mail['userid'], "Verified ownership of email {$mail['email']}");
1✔
3817
            }
3818

3819
            $this->dbhm->preExec("UPDATE users_emails SET preferred = 0 WHERE id = ?;", [$this->id]);
2✔
3820
            $this->dbhm->preExec("UPDATE users_emails SET userid = ?, preferred = 1, validated = NOW(), validatekey = NULL WHERE id = ?;", [$this->id, $mail['id']]);
2✔
3821
            $this->addEmail($mail['email'], 1);
2✔
3822
            $rc = TRUE;
2✔
3823
        }
3824

3825
        return ($rc);
2✔
3826
    }
3827

3828
    public function confirmUnsubscribe()
3829
    {
3830
        list ($transport, $mailer) = Mail::getMailer();
2✔
3831

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

3834
        $message = \Swift_Message::newInstance()
2✔
3835
            ->setSubject("Please confirm you want to leave Freegle")
2✔
3836
            ->setFrom(NOREPLY_ADDR)
2✔
3837
            ->setReplyTo(SUPPORT_ADDR)
2✔
3838
            ->setTo($this->getEmailPreferred())
2✔
3839
            ->setDate(time())
2✔
3840
            ->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✔
3841

3842
        Mail::addHeaders($message, Mail::UNSUBSCRIBE);
2✔
3843
        $this->sendIt($mailer, $message);
2✔
3844
    }
3845

3846
    public function inventEmail($force = FALSE)
3847
    {
3848
        # An invented email is one on our domain that doesn't give away too much detail, but isn't just a string of
3849
        # numbers (ideally).  We may already have one.
3850
        $email = NULL;
41✔
3851

3852
        if (!$force) {
41✔
3853
            # We want the most recent of our own emails.
3854
            $emails = $this->getEmails(TRUE);
41✔
3855
            foreach ($emails as $thisemail) {
41✔
3856
                if (strpos($thisemail['email'], USER_DOMAIN) !== FALSE) {
24✔
3857
                    $email = $thisemail['email'];
11✔
3858
                    break;
11✔
3859
                }
3860
            }
3861
        }
3862

3863
        if (!$email) {
41✔
3864
            # If they have a Yahoo ID, that'll do nicely - it's public info.  But some Yahoo IDs are actually
3865
            # email addresses (don't ask) and we don't want those.  And some are stupidly long.
3866
            $yahooid = $this->getPrivate('yahooid');
31✔
3867

3868
            if (!$force && strlen(str_replace(' ', '', $yahooid)) && strpos($yahooid, '@') === FALSE && strlen($yahooid) <= 16) {
31✔
3869
                $email = str_replace(' ', '', $yahooid) . '-' . $this->id . '@' . USER_DOMAIN;
1✔
3870
            } else {
3871
                # Their own email might already be of that nature, which would be lovely.
3872
                if (!$force) {
31✔
3873
                    $email = $this->getEmailPreferred();
31✔
3874

3875
                    if ($email) {
31✔
3876
                        foreach (['firstname', 'lastname', 'fullname'] as $att) {
14✔
3877
                            $words = explode(' ', $this->user[$att]);
14✔
3878
                            foreach ($words as $word) {
14✔
3879
                                if (strlen($word) && stripos($email, $word) !== FALSE) {
14✔
3880
                                    # Unfortunately not - it has some personal info in it.
3881
                                    $email = NULL;
14✔
3882
                                }
3883
                            }
3884
                        }
3885

3886
                        if (stripos($email, '%') !== FALSE) {
14✔
3887
                            # This may indicate a case where the real email is encoded on the LHS, eg gtempaccount.com
3888
                            $email = NULL;
1✔
3889
                        }
3890
                    }
3891
                }
3892

3893
                if ($email) {
31✔
3894
                    # We have an email which is fairly anonymous.  Use the LHS.
3895
                    $p = strpos($email, '@');
2✔
3896
                    $email = str_replace(' ', '', $p > 0 ? substr($email, 0, $p) : $email) . '-' . $this->id . '@' . USER_DOMAIN;
2✔
3897
                } else {
3898
                    # We can't make up something similar to their existing email address so invent from scratch.
3899
                    $lengths = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/distinct_word_lengths.json'), true);
31✔
3900
                    $bigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/word_start_bigrams.json'), true);
31✔
3901
                    $trigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/trigrams.json'), true);
31✔
3902

3903
                    do {
3904
                        $length = \Wordle\array_weighted_rand($lengths);
31✔
3905
                        $start = \Wordle\array_weighted_rand($bigrams);
31✔
3906
                        $email = strtolower(\Wordle\fill_word($start, $length, $trigrams)) . '-' . $this->id . '@' . USER_DOMAIN;
31✔
3907

3908
                        # We might just happen to have invented an email with their personal information in it.  This
3909
                        # actually happened in the UT with "test".
3910
                        foreach (['firstname', 'lastname', 'fullname'] as $att) {
31✔
3911
                            $words = explode(' ', $this->user[$att]);
31✔
3912
                            foreach ($words as $word) {
31✔
3913
                                $word = trim($word);
31✔
3914
                                if (strlen($word)) {
31✔
3915
                                    $p = stripos($email, $word);
26✔
3916
                                    $q = strpos($email, '@');
26✔
3917

3918
                                    if ($word !== '-') {
26✔
3919
                                        # Dash is always present, which is fine.
3920
                                        $email = ($p !== FALSE && $p < $q) ? NULL : $email;
26✔
3921
                                    }
3922
                                }
3923
                            }
3924
                        }
3925
                    } while (!$email);
31✔
3926
                }
3927
            }
3928
        }
3929

3930
        return ($email);
41✔
3931
    }
3932

3933
    public function delete($groupid = NULL, $subject = NULL, $body = NULL, $log = TRUE)
3934
    {
3935
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
16✔
3936

3937
        # Delete memberships.  This will remove any Yahoo memberships.
3938
        $membs = $this->getMemberships();
16✔
3939
        #error_log("Members in delete " . var_export($membs, TRUE));
3940
        foreach ($membs as $memb) {
16✔
3941
            $this->removeMembership($memb['id']);
7✔
3942
        }
3943

3944
        $rc = $this->dbhm->preExec("DELETE FROM users WHERE id = ?;", [$this->id]);
16✔
3945

3946
        if ($rc && $log) {
16✔
3947
            $this->log->log([
5✔
3948
                'type' => Log::TYPE_USER,
3949
                'subtype' => Log::SUBTYPE_DELETED,
3950
                'user' => $this->id,
5✔
3951
                'byuser' => $me ? $me->getId() : NULL,
5✔
3952
                'text' => $this->getName()
5✔
3953
            ]);
3954
        }
3955

3956
        return ($rc);
16✔
3957
    }
3958

3959
    public function getUnsubLink($domain, $id, $type = NULL, $auto = FALSE)
3960
    {
3961
        return (User::loginLink($domain, $id, "/unsubscribe/$id", $type, $auto));
21✔
3962
    }
3963

3964
    public function listUnsubscribe($domain, $id, $type = NULL)
3965
    {
3966
        # Generates the value for the List-Unsubscribe header field.
3967
        $ret = "<mailto:unsubscribe-$id@" . USER_SITE . ">, <" . $this->getUnsubLink($domain, $id, $type) . ">";
19✔
3968
        return ($ret);
19✔
3969
    }
3970

3971
    public function loginLink($domain, $id, $url = '/', $type = NULL, $auto = FALSE)
3972
    {
3973
        $p = strpos($url, '?');
52✔
3974
        $ret = $p === FALSE ? "https://$domain$url?u=$id&src=$type" : "https://$domain$url&u=$id&src=$type";
52✔
3975

3976
        if ($auto) {
52✔
3977
            # Get a per-user link we can use to log in without a password.
3978
            $key = NULL;
10✔
3979
            $sql = "SELECT * FROM users_logins WHERE userid = ? AND type = ?;";
10✔
3980
            $logins = $this->dbhr->preQuery($sql, [$id, User::LOGIN_LINK]);
10✔
3981
            foreach ($logins as $login) {
10✔
3982
                $key = $login['credentials'];
1✔
3983
            }
3984

3985
            if (!$key) {
10✔
3986
                $key = Utils::randstr(32);
10✔
3987
                $rc = $this->dbhm->preExec("INSERT INTO users_logins (userid, type, credentials) VALUES (?,?,?);", [
10✔
3988
                    $id,
3989
                    User::LOGIN_LINK,
3990
                    $key
3991
                ]);
3992

3993
                # If this didn't work, we still return an URL - worst case they'll have to sign in.
3994
                $key = $rc ? $key : NULL;
10✔
3995
            }
3996

3997
            $p = strpos($url, '?');
10✔
3998
            $src = $type ? "&src=$type" : "";
10✔
3999
            $ret = $p === FALSE ? ("https://$domain$url?u=$id&k=$key$src") : ("https://$domain$url&u=$id&k=$key$src");
10✔
4000
        }
4001

4002
        return ($ret);
52✔
4003
    }
4004

4005
    public function sendOurMails($g = NULL, $checkholiday = TRUE, $checkbouncing = TRUE)
4006
    {
4007
        # We don't want to send emails to people who haven't been active for more than six months.  This improves
4008
        # our spam reputation, by avoiding honeytraps.
4009
        $sendit = FALSE;
88✔
4010
        $lastaccess = strtotime($this->getPrivate('lastaccess'));
88✔
4011

4012
        // This time is also present on the client in ModMember, and in Engage.
4013
        if (time() - $lastaccess <= Engage::USER_INACTIVE) {
88✔
4014
            $sendit = TRUE;
88✔
4015

4016
            if ($sendit && $checkholiday) {
88✔
4017
                # We might be on holiday.
4018
                $hol = $this->getPrivate('onholidaytill');
17✔
4019
                $till = $hol ? strtotime($hol) : 0;
17✔
4020
                #error_log("Holiday $till vs " . time());
4021

4022
                $sendit = time() > $till;
17✔
4023
            }
4024

4025
            if ($sendit && $checkbouncing) {
88✔
4026
                # And don't send if we're bouncing.
4027
                $sendit = !$this->getPrivate('bouncing');
17✔
4028
                #error_log("After bouncing $sendit");
4029
            }
4030
        }
4031

4032
        #error_log("Sendit? $sendit");
4033
        return ($sendit);
88✔
4034
    }
4035

4036
    public function getMembershipHistory()
4037
    {
4038
        # We get this from our logs.
4039
        $sql = "SELECT * FROM logs WHERE user = ? AND `type` = ? ORDER BY id DESC;";
6✔
4040
        $logs = $this->dbhr->preQuery($sql, [$this->id, Log::TYPE_GROUP]);
6✔
4041

4042
        $ret = [];
6✔
4043
        foreach ($logs as $log) {
6✔
4044
            $thisone = NULL;
3✔
4045
            switch ($log['subtype']) {
3✔
4046
                case Log::SUBTYPE_JOINED:
3✔
4047
                case Log::SUBTYPE_APPROVED:
1✔
4048
                case Log::SUBTYPE_REJECTED:
1✔
4049
                case Log::SUBTYPE_APPLIED:
1✔
4050
                case Log::SUBTYPE_LEFT:
1✔
4051
                    {
4052
                        $thisone = $log['subtype'];
3✔
4053
                        break;
3✔
4054
                    }
4055
            }
4056

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

4061
                if ($g->getId() ==  $log['groupid']) {
3✔
4062
                    $ret[] = [
3✔
4063
                        'timestamp' => Utils::ISODate($log['timestamp']),
3✔
4064
                        'type' => $thisone,
4065
                        'group' => [
4066
                            'id' => $log['groupid'],
3✔
4067
                            'nameshort' => $g->getPrivate('nameshort'),
3✔
4068
                            'namedisplay' => $g->getName()
3✔
4069
                        ],
4070
                        'text' => $log['text']
3✔
4071
                    ];
4072
                }
4073
            }
4074
        }
4075

4076
        return ($ret);
6✔
4077
    }
4078

4079
    public function search($search, $ctx)
4080
    {
4081
        if (preg_replace('/\-|\~/', '', $search) ==  '') {
6✔
4082
            # Most likely an encoded id.
4083
            $search = User::decodeId($search);
1✔
4084
        }
4085

4086
        if (preg_match('/story-(.*)/', $search, $matches)) {
6✔
4087
            # Story.
4088
            $s = new Story($this->dbhr, $this->dbhm, $matches[1]);
×
4089
            $search = $s->getPrivate('userid');
×
4090
        }
4091

4092
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
6✔
4093
        $id = intval(Utils::presdef('id', $ctx, 0));
6✔
4094
        $ctx = $ctx ? $ctx : [];
6✔
4095
        $q = $this->dbhr->quote("$search%");
6✔
4096
        $backwards = strrev($search);
6✔
4097
        $qb = $this->dbhr->quote("$backwards%");
6✔
4098

4099
        $canon = $this->dbhr->quote(User::canonMail($search) . "%");
6✔
4100

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

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

4108
        $sql = "SELECT DISTINCT userid FROM
6✔
4109
                ((SELECT userid FROM users_emails WHERE canon LIKE $canon OR backwards LIKE $qb) UNION
4110
                (SELECT userid FROM users_emails WHERE canon LIKE $canon2) UNION
4111
                (SELECT id AS userid FROM users WHERE fullname LIKE $q) UNION
4112
                (SELECT id AS userid FROM users WHERE yahooid LIKE $q) UNION
4113
                (SELECT id AS userid FROM users WHERE id = ?) UNION
4114
                (SELECT userid FROM users_logins WHERE uid LIKE $q)) t WHERE userid > ? ORDER BY userid ASC";
4115
        $users = $this->dbhr->preQuery($sql, [$search, $id]);
6✔
4116

4117
        $ret = [];
6✔
4118

4119
        foreach ($users as $user) {
6✔
4120
            $ctx['id'] = $user['userid'];
5✔
4121

4122
            $u = User::get($this->dbhr, $this->dbhm, $user['userid']);
5✔
4123

4124
            $thisone = $u->getPublic(NULL, TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, [
5✔
4125
                MessageCollection::PENDING,
4126
                MessageCollection::APPROVED
4127
            ], TRUE);
4128

4129
            # We might not have the emails.
4130
            $thisone['email'] = $u->getEmailPreferred();
5✔
4131
            $thisone['emails'] = $u->getEmails();
5✔
4132

4133
            $thisone['membershiphistory'] = $u->getMembershipHistory();
5✔
4134

4135
            # Make sure there's a link login as admin/support can use that to impersonate.
4136
            if ($me && ($me->isAdmin() || ($me->isAdminOrSupport() && !$u->isModerator()))) {
5✔
4137
                $thisone['loginlink'] = $u->loginLink(USER_SITE, $user['userid'], '/', NULL, TRUE);
1✔
4138
            }
4139

4140
            $thisone['logins'] = $u->getLogins($me && $me->isAdmin());
5✔
4141

4142
            # Also return the chats for this user.  Can't use ChatRooms::listForUser because that would exclude any
4143
            # chats on groups where we were no longer a member.
4144
            $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 = ?;", [
5✔
4145
                $user['userid'],
5✔
4146
                ChatRoom::TYPE_USER2USER,
4147
                $user['userid'],
5✔
4148
            ]), 'id'));
4149

4150
            $thisone['chatrooms'] = [];
5✔
4151

4152
            if ($rooms) {
5✔
4153
                $r = new ChatRoom($this->dbhr, $this->dbhm);
1✔
4154
                $thisone['chatrooms'] = $r->fetchRooms($rooms, $user['userid'], FALSE);
1✔
4155
            }
4156

4157
            # Add the public location and best guess lat/lng
4158
            $thisone['info']['publiclocation'] = $u->getPublicLocation();
5✔
4159
            $latlng = $u->getLatLng(FALSE, TRUE);
5✔
4160
            $thisone['privateposition'] = [
5✔
4161
                'lat' => $latlng[0],
5✔
4162
                'lng' => $latlng[1],
5✔
4163
                'name' => $latlng[2]
5✔
4164
            ];
4165

4166
            $thisone['comments'] = $this->getCommentsForSingleUser($user['userid']);
5✔
4167
            $thisone['tnuserid'] = $u->getPrivate('tnuserid');
5✔
4168

4169
            $push = $this->dbhr->preQuery("SELECT MAX(lastsent) AS lastpush FROM users_push_notifications WHERE userid = ?;", [
5✔
4170
                $user['userid']
5✔
4171
            ]);
4172

4173
            foreach ($push as $p) {
5✔
4174
                $thisone['lastpush'] = Utils::ISODate($p['lastpush']);
5✔
4175
            }
4176

4177
            $thisone['info'] = $u->getInfo();
5✔
4178
            $thisone['trustlevel'] = $u->getPrivate('trustlevel');
5✔
4179

4180
            $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE userid = ?;", [
5✔
4181
                $u->getId()
5✔
4182
            ]);
4183

4184
            $thisone['bans'] = [];
5✔
4185

4186
            foreach ($bans as $ban) {
5✔
4187
                $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
4188
                $banner = User::get($this->dbhr, $this->dbhm, $ban['byuser']);
1✔
4189
                $thisone['bans'][] = [
1✔
4190
                    'date' => Utils::ISODate($ban['date']),
1✔
4191
                    'group' => $g->getName(),
1✔
4192
                    'byemail' => $banner->getEmailPreferred(),
1✔
4193
                    'byuserid' => $ban['byuser']
1✔
4194
                ];
4195
            }
4196

4197
            $d = new Donations($this->dbhr, $this->dbhm);
5✔
4198
            $thisone['giftaid'] = $d->getGiftAid($user['userid']);
5✔
4199

4200
            if ($me->hasPermission(User::PERM_GIFTAID)) {
5✔
4201
                $thisone['donations'] = $d->listByUser($user['userid']);
2✔
4202
            }
4203

4204
            $ret[] = $thisone;
5✔
4205
        }
4206

4207
        return ($ret);
6✔
4208
    }
4209

4210
    private function safeGetPostcode($val) {
4211
        $ret = [ NULL, NULL ];
52✔
4212

4213
        $settings = json_decode($val, TRUE);
52✔
4214

4215
        if (Utils::pres('mylocation', $settings) &&
52✔
4216
            Utils::presdef('type', $settings['mylocation'], NULL) == 'Postcode') {
52✔
4217
            $ret = [
13✔
4218
                Utils::presdef('id', $settings['mylocation'], NULL),
13✔
4219
                Utils::presdef('name', $settings['mylocation'], NULL)
13✔
4220
            ];
4221
        }
4222

4223
        return $ret;
52✔
4224
    }
4225

4226
    public function setPrivate($att, $val)
4227
    {
4228
        if (!strcmp($att, 'settings') && $val) {
161✔
4229
            # Possible location change.
4230
            list ($oldid, $oldloc) = $this->safeGetPostcode($this->getPrivate('settings'));
52✔
4231
            list ($newid, $newloc) = $this->safeGetPostcode($val);
52✔
4232

4233
            if ($oldloc !== $newloc) {
52✔
4234
                # We have changed our location.
4235
                parent::setPrivate('lastlocation', $newid);
13✔
4236
                $i = new Isochrone($this->dbhr, $this->dbhm);
13✔
4237
                $i->deleteForUser($this->id);
13✔
4238

4239
                $this->log->log([
13✔
4240
                            'type' => Log::TYPE_USER,
4241
                            'subtype' => Log::SUBTYPE_POSTCODECHANGE,
4242
                            'user' => $this->id,
13✔
4243
                            'text' => $newloc
4244
                        ]);
4245
            }
4246

4247
            // Prune the info in the settings to remove any groupsnear info, which would use space and is not needed.
4248
            $val = User::pruneSettings($val);
52✔
4249
        }
4250

4251
        User::clearCache($this->id);
161✔
4252
        parent::setPrivate($att, $val);
161✔
4253
    }
4254

4255
    public static function pruneSettings($val) {
4256
        // Prune info from what we store in the user table to keep it smaller.
4257
        if (strpos($val, 'groupsnear') !== FALSE) {
52✔
4258
            $decoded = json_decode($val, TRUE);
×
4259
            if (Utils::pres('mylocation', $decoded) && Utils::pres('groupsnear', $decoded['mylocation'])) {
×
4260
                unset($decoded['mylocation']['groupsnear']);
×
4261
                $val = json_encode($decoded);
×
4262
            }
4263
        }
4264

4265
        return $val;
52✔
4266
    }
4267

4268
    public function canMerge()
4269
    {
4270
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
14✔
4271
        return (array_key_exists('canmerge', $settings) ? $settings['canmerge'] : TRUE);
14✔
4272
    }
4273

4274
    public function notifsOn($type, $groupid = NULL)
4275
    {
4276
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
525✔
4277
        $notifs = Utils::pres('notifications', $settings);
525✔
4278

4279
        $defs = [
525✔
4280
            self::NOTIFS_EMAIL => TRUE,
4281
            self::NOTIFS_EMAIL_MINE => FALSE,
4282
            self::NOTIFS_PUSH => TRUE,
4283
            self::NOTIFS_FACEBOOK => TRUE,
4284
            self::NOTIFS_APP => TRUE
4285
        ];
4286

4287
        $ret = ($notifs && array_key_exists($type, $notifs)) ? $notifs[$type] : $defs[$type];
525✔
4288

4289
        if ($ret && $groupid) {
525✔
4290
            # Check we're an active mod on this group - if not then we don't want the notifications.
4291
            $ret = $this->activeModForGroup($groupid);
5✔
4292
        }
4293

4294
        #error_log("Notifs on for user #{$this->id} type $type ? $ret from " . var_export($notifs, TRUE));
4295
        return ($ret);
525✔
4296
    }
4297

4298
    public function getNotificationPayload($modtools)
4299
    {
4300
        # This gets a notification count/title/message for this user.
4301
        $notifcount = 0;
8✔
4302
        $title = '';
8✔
4303
        $message = NULL;
8✔
4304
        $chatids = [];
8✔
4305
        $route = NULL;
8✔
4306

4307
        if (!$modtools) {
8✔
4308
            # User notification.  We want to show a count of chat messages, or some of the message if there is just one.
4309
            $r = new ChatRoom($this->dbhr, $this->dbhm);
5✔
4310
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_USER2USER, ChatRoom::TYPE_USER2MOD], $modtools);
5✔
4311
            $chatcount = count($unseen);
5✔
4312
            $total = $chatcount;
5✔
4313
            foreach ($unseen as $un) {
5✔
4314
                $chatids[] = $un['chatid'];
3✔
4315
            };
4316

4317
            #error_log("Chats with unseen " . var_export($chatids, TRUE));
4318
            $n = new Notifications($this->dbhr, $this->dbhm);
5✔
4319
            $notifcount = $n->countUnseen($this->id);
5✔
4320

4321
            if ($total ==  1) {
5✔
4322
                $r = new ChatRoom($this->dbhr, $this->dbhm, $unseen[0]['chatid']);
2✔
4323
                $atts = $r->getPublic($this);
2✔
4324
                $title = $atts['name'];
2✔
4325
                list($msgs, $users) = $r->getMessages(100, 0);
2✔
4326

4327
                if (count($msgs) > 0) {
2✔
4328
                    $message = Utils::presdef('message', $msgs[count($msgs) - 1], "You have a message");
2✔
4329
                    $message = strlen($message) > 256 ? (substr($message, 0, 256) . "...") : $message;
2✔
4330
                }
4331

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

4334
                if ($notifcount) {
2✔
4335
                    $total += $notifcount;
2✔
4336
                }
4337
            } else if ($total > 1) {
3✔
4338
                $title = "You have $total new messages";
1✔
4339
                $route = "/chats";
1✔
4340

4341
                if ($notifcount) {
1✔
4342
                    $total += $notifcount;
1✔
4343
                    $title .= " and $notifcount notification" . ($notifcount == 1 ? '' : 's');
1✔
4344
                }
4345
            } else {
4346
                # Add in the notifications you see primarily from the newsfeed.
4347
                if ($notifcount) {
3✔
4348
                    $total += $notifcount;
3✔
4349
                    $ctx = NULL;
3✔
4350
                    $notifs = $n->get($this->id, $ctx);
3✔
4351
                    $title = $n->getNotifTitle($notifs);
3✔
4352
                    $route = '/';
3✔
4353

4354
                    if (count($notifs) > 0) {
3✔
4355
                        # For newsfeed notifications sent a route to the right place.
4356
                        switch ($notifs[0]['type']) {
3✔
4357
                            case Notifications::TYPE_COMMENT_ON_COMMENT:
3✔
4358
                            case Notifications::TYPE_COMMENT_ON_YOUR_POST:
3✔
4359
                            case Notifications::TYPE_LOVED_COMMENT:
3✔
4360
                            case Notifications::TYPE_LOVED_POST:
3✔
4361
                                $route = '/chitchat/' . $notifs[0]['newsfeedid'];
1✔
4362
                                break;
5✔
4363
                        }
4364
                    }
4365
                }
4366
            }
4367
        } else {
4368
            # ModTools notification.  We show the count of work + chats.
4369
            $r = new ChatRoom($this->dbhr, $this->dbhm);
4✔
4370
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_MOD2MOD, ChatRoom::TYPE_USER2MOD], $modtools);
4✔
4371
            $chatcount = count($unseen);
4✔
4372

4373
            $work = $this->getWorkCounts();
4✔
4374
            $total = $work['total'] + $chatcount;
4✔
4375

4376
            // The order of these is important as the route will be the last matching.
4377
            $types = [
4✔
4378
                'pendingvolunteering' => [ 'volunteer op', 'volunteerops', '/modtools/volunteering' ],
4379
                'pendingevents' => [ 'event', 'events', '/modtools/communityevents' ],
4380
                'socialactions' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4381
                'popularposts' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4382
                'stories' => [ 'story', 'stories', '/modtools/members/stories' ],
4383
                'newsletterstories' => [ 'newsletter story', 'newsletter stories', '/modtools/members/newsletter' ],
4384
                'chatreview' => [ 'chat message to review', 'chat messages to review', '/modtools/chats/review' ],
4385
                'pendingadmins' => [ 'admin', 'admins', '/modtools/admins' ],
4386
                'spammembers' => [ 'member to review', 'members to review', '/modtools/members/review' ],
4387
                'relatedmembers' => [ 'related member to review', 'related members to review', '/modtools/members/related' ],
4388
                'editreview' => [ 'edit', 'edits', '/modtools/messages/edits' ],
4389
                'spam' => [ 'message to review', 'messages to review', '/modtools/messages/pending' ],
4390
                'pending' => [ 'pending message', 'pending messages', '/modtools/messages/pending' ]
4391
            ];
4392

4393
            $title = $chatcount ? ("$chatcount chat message" . ($chatcount != 1 ? 's' : '') . "\n") : '';
4✔
4394
            $route = NULL;
4✔
4395

4396
            foreach ($types as $type => $vals) {
4✔
4397
                if (Utils::presdef($type, $work, 0) > 0) {
4✔
4398
                    $title .= $work[$type] . ' ' . ($work[$type] != 1 ? $vals[1] : $vals[0] ) . "\n";
1✔
4399
                    $route = $vals[2];
1✔
4400
                }
4401
            }
4402

4403
            $title = $title == '' ? NULL : $title;
4✔
4404
        }
4405

4406

4407
        return ([$total, $chatcount, $notifcount, $title, $message, array_unique($chatids), $route]);
8✔
4408
    }
4409

4410
    public function hasPermission($perm)
4411
    {
4412
        $perms = $this->user['permissions'];
40✔
4413
        return ($perms && stripos($perms, $perm) !== FALSE);
40✔
4414
    }
4415

4416
    public function sendIt($mailer, $message)
4417
    {
4418
        $mailer->send($message);
27✔
4419
    }
4420

4421
    public function thankDonation()
4422
    {
4423
        try {
4424
            $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/donations');
1✔
4425
            $twig = new \Twig_Environment($loader);
1✔
4426
            list ($transport, $mailer) = Mail::getMailer();
1✔
4427

4428
            $message = \Swift_Message::newInstance()
1✔
4429
                ->setSubject("Thank you for supporting Freegle!")
1✔
4430
                ->setFrom(PAYPAL_THANKS_FROM)
1✔
4431
                ->setReplyTo(PAYPAL_THANKS_FROM)
1✔
4432
                ->setTo($this->getEmailPreferred())
1✔
4433
                ->setBody("Thank you for supporting Freegle!");
1✔
4434

4435
            Mail::addHeaders($message, Mail::THANK_DONATION);
1✔
4436

4437
            $html = $twig->render('thank.html', [
1✔
4438
                'name' => $this->getName(),
1✔
4439
                'email' => $this->getEmailPreferred(),
1✔
4440
                'unsubscribe' => $this->loginLink(USER_SITE, $this->getId(), "/unsubscribe", NULL)
1✔
4441
            ]);
4442

4443
            # Add HTML in base-64 as default quoted-printable encoding leads to problems on
4444
            # Outlook.
4445
            $htmlPart = \Swift_MimePart::newInstance();
1✔
4446
            $htmlPart->setCharset('utf-8');
1✔
4447
            $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
4448
            $htmlPart->setContentType('text/html');
1✔
4449
            $htmlPart->setBody($html);
1✔
4450
            $message->attach($htmlPart);
1✔
4451

4452
            Mail::addHeaders($message, Mail::THANK_DONATION, $this->getId());
1✔
4453

4454
            $this->sendIt($mailer, $message);
1✔
4455
        } catch (\Exception $e) { error_log("Failed " . $e->getMessage()); };
×
4456
    }
4457

4458
    public function invite($email)
4459
    {
4460
        $ret = FALSE;
9✔
4461

4462
        # We can only invite logged in.
4463
        if ($this->id) {
9✔
4464
            # ...and only if we have spare.
4465
            if ($this->user['invitesleft'] > 0) {
9✔
4466
                # They might already be using us - but they might also have forgotten.  So allow that case.  However if
4467
                # they have actively declined a previous invitation we suppress this one.
4468
                $previous = $this->dbhr->preQuery("SELECT id FROM users_invitations WHERE email = ? AND outcome = ?;", [
9✔
4469
                    $email,
4470
                    User::INVITE_DECLINED
4471
                ]);
4472

4473
                if (count($previous) == 0) {
9✔
4474
                    # The table has a unique key on userid and email, so that means we can only invite the same person
4475
                    # once.  That avoids us pestering them.
4476
                    try {
4477
                        $this->dbhm->preExec("INSERT INTO users_invitations (userid, email) VALUES (?,?);", [
9✔
4478
                            $this->id,
9✔
4479
                            $email
4480
                        ]);
4481

4482
                        # We're ok to invite.
4483
                        $fromname = $this->getName();
9✔
4484
                        $frommail = $this->getEmailPreferred();
9✔
4485
                        $url = "https://" . USER_SITE . "/invite/" . $this->dbhm->lastInsertId();
9✔
4486

4487
                        list ($transport, $mailer) = Mail::getMailer();
9✔
4488
                        $message = \Swift_Message::newInstance()
9✔
4489
                            ->setSubject("$fromname has invited you to try Freegle!")
9✔
4490
                            ->setFrom([NOREPLY_ADDR => SITE_NAME])
9✔
4491
                            ->setReplyTo($frommail)
9✔
4492
                            ->setTo($email)
9✔
4493
                            ->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✔
4494

4495
                        Mail::addHeaders($message, Mail::INVITATION);
9✔
4496

4497
                        $html = invite($fromname, $frommail, $url);
9✔
4498

4499
                        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
4500
                        # Outlook.
4501
                        $htmlPart = \Swift_MimePart::newInstance();
9✔
4502
                        $htmlPart->setCharset('utf-8');
9✔
4503
                        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
9✔
4504
                        $htmlPart->setContentType('text/html');
9✔
4505
                        $htmlPart->setBody($html);
9✔
4506
                        $message->attach($htmlPart);
9✔
4507

4508
                        $this->sendIt($mailer, $message);
9✔
4509
                        $ret = TRUE;
9✔
4510

4511
                        $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft - 1 WHERE id = ?;", [
9✔
4512
                            $this->id
9✔
4513
                        ]);
4514
                    } catch (\Exception $e) {
1✔
4515
                        # Probably a duplicate.
4516
                    }
4517
                }
4518
            }
4519
        }
4520

4521
        return ($ret);
9✔
4522
    }
4523

4524
    public function inviteOutcome($id, $outcome)
4525
    {
4526
        $invites = $this->dbhm->preQuery("SELECT * FROM users_invitations WHERE id = ?;", [
1✔
4527
            $id
4528
        ]);
4529

4530
        foreach ($invites as $invite) {
1✔
4531
            if ($invite['outcome'] == User::INVITE_PENDING) {
1✔
4532
                $this->dbhm->preExec("UPDATE users_invitations SET outcome = ?, outcometimestamp = NOW() WHERE id = ?;", [
1✔
4533
                    $outcome,
4534
                    $id
4535
                ]);
4536

4537
                if ($outcome == User::INVITE_ACCEPTED) {
1✔
4538
                    # Give the sender two more invites.  This means that if their invitations are unsuccessful, they will
4539
                    # stall, but if they do ok, they won't.  This isn't perfect - someone could fake up emails and do
4540
                    # successful invitations that way.
4541
                    $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft + 2 WHERE id = ?;", [
1✔
4542
                        $invite['userid']
1✔
4543
                    ]);
4544
                }
4545
            }
4546
        }
4547
    }
4548

4549
    public function listInvitations($since = "30 days ago")
4550
    {
4551
        $ret = [];
8✔
4552

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

4559
        foreach ($invites as $invite) {
8✔
4560
            # Check if this email is now on the platform.
4561
            $invite['date'] = Utils::ISODate($invite['date']);
7✔
4562
            $invite['outcometimestamp'] = $invite['outcometimestamp'] ? Utils::ISODate($invite['outcometimestamp']) : NULL;
7✔
4563
            $ret[] = $invite;
7✔
4564
        }
4565

4566
        return ($ret);
8✔
4567
    }
4568

4569
    public function getLatLng($usedef = TRUE, $usegroup = TRUE, $blur = Utils::BLUR_NONE)
4570
    {
4571
        $ret = [ 0, 0, NULL ];
158✔
4572

4573
        if ($this->id) {
158✔
4574
            $locs = $this->getLatLngs([ $this->user ], $usedef, $usegroup, FALSE, [ $this->user ]);
158✔
4575
            $loc = $locs[$this->id];
158✔
4576

4577
            if ($loc) {
158✔
4578
                if ($blur && ($loc['lat'] || $loc['lng'])) {
157✔
4579
                    list ($loc['lat'], $loc['lng']) = Utils::blur($loc['lat'], $loc['lng'], $blur);
3✔
4580
                }
4581

4582
                $ret = [ $loc['lat'], $loc['lng'], Utils::presdef('loc', $loc, NULL) ];
157✔
4583
            }
4584
        }
4585

4586
        return $ret;
158✔
4587
    }
4588

4589
    public function getPublicLocations(&$users, $atts = NULL)
4590
    {
4591
        $idsleft = [];
101✔
4592
        
4593
        foreach ($users as $userid => $user) {
101✔
4594
            if (!Utils::pres('info', $user) || !Utils::pres('publiclocation', $user['info'])) {
101✔
4595
                $idsleft[] = $userid;
101✔
4596
            }
4597
        }
4598
        
4599
        $areas = NULL;
101✔
4600
        $membs = NULL;
101✔
4601

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

4606
            foreach ($atts as $att) {
101✔
4607
                $loc = NULL;
101✔
4608
                $grp = NULL;
101✔
4609

4610
                $aid = NULL;
101✔
4611
                $lid = NULL;
101✔
4612
                $lat = NULL;
101✔
4613
                $lng = NULL;
101✔
4614

4615
                # Default to nowhere.
4616
                $users[$att['id']]['info']['publiclocation'] = [
101✔
4617
                    'display' => '',
4618
                    'location' => NULL,
4619
                    'groupname' => NULL
4620
                ];
4621

4622
                if (Utils::pres('settings', $att)) {
101✔
4623
                    $settings = $att['settings'];
21✔
4624
                    $settings = json_decode($settings, TRUE);
21✔
4625

4626
                    if (Utils::pres('mylocation', $settings) && Utils::pres('area', $settings['mylocation'])) {
21✔
4627
                        $loc = $settings['mylocation']['area']['name'];
7✔
4628
                        $lid = $settings['mylocation']['id'];
7✔
4629
                        $lat = $settings['mylocation']['lat'];
7✔
4630
                        $lng = $settings['mylocation']['lng'];
7✔
4631
                    }
4632
                }
4633

4634
                if (!$loc) {
101✔
4635
                    # Get the name of the last area we used.
4636
                    if (is_null($areas)) {
94✔
4637
                        $areas = $this->dbhr->preQuery("SELECT l2.id, l2.name, l2.lat, l2.lng, users.id AS userid FROM locations l1 
94✔
4638
                            INNER JOIN users ON users.lastlocation = l1.id
4639
                            INNER JOIN locations l2 ON l2.id = l1.areaid
4640
                            WHERE users.id IN (" . implode(',', $idsleft) . ");", NULL, FALSE, FALSE);
94✔
4641
                    }
4642

4643
                    foreach ($areas as $area) {
94✔
4644
                        if ($att['id'] ==  $area['userid']) {
21✔
4645
                            $loc = $area['name'];
21✔
4646
                            $lid = $area['id'];
21✔
4647
                            $lat = $area['lat'];
21✔
4648
                            $lng = $area['lng'];
21✔
4649
                        }
4650
                    }
4651
                }
4652

4653
                if (!$lid) {
101✔
4654
                    # Find the group of which we are a member which is closest to our location.  We do this because generally
4655
                    # the number of groups we're in is small and therefore this will be quick, whereas the groupsNear call is
4656
                    # fairly slow.
4657
                    $closestdist = PHP_INT_MAX;
94✔
4658
                    $closestname = NULL;
94✔
4659

4660
                    # Get all the memberships.
4661
                    if (!$membs) {
94✔
4662
                        $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(
94✔
4663
                                ',',
4664
                                $idsleft
4665
                            ) . ") ORDER BY added ASC;";
4666
                        $membs = $this->dbhr->preQuery($sql);
94✔
4667
                    }
4668

4669
                    foreach ($membs as $memb) {
94✔
4670
                        if ($memb['userid'] == $att['id']) {
83✔
4671
                            $dist = \GreatCircle::getDistance($lat, $lng, $memb['lat'], $memb['lng']);
83✔
4672

4673
                            if ($dist < $closestdist) {
83✔
4674
                                $closestdist = $dist;
83✔
4675
                                $closestname = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
83✔
4676
                            }
4677
                        }
4678
                    }
4679

4680
                    if (!is_null($closestname)) {
94✔
4681
                        $grp = $closestname;
83✔
4682

4683
                        # The location name might be in the group name, in which case just use the group.
4684
                        $loc = stripos($grp, $loc) !== FALSE ? NULL : $loc;
83✔
4685
                    }
4686
                }
4687

4688
                if ($loc) {
101✔
4689
                    $display = $loc ? ($loc . ($grp ? ", $grp" : "")) : ($grp ? $grp : '');
28✔
4690

4691
                    $users[$att['id']]['info']['publiclocation'] = [
28✔
4692
                        'display' => $display,
4693
                        'location' => $loc,
4694
                        'groupname' => $grp
4695
                    ];
4696

4697
                    $idsleft = array_filter($idsleft, function($val) use ($att) {
28✔
4698
                        return($val != $att['id']);
28✔
4699
                    });
4700
                }
4701
            }
4702

4703
            if (count($idsleft) > 0) {
101✔
4704
                # We have some left which don't have explicit postcodes.  Try for a group name.
4705
                #
4706
                # First check the group we used most recently.
4707
                #error_log("Look for group name only for {$att['id']}");
4708
                $found = [];
94✔
4709
                foreach ($idsleft as $userid) {
94✔
4710
                    $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;", [
94✔
4711
                        $userid
4712
                    ]);
4713

4714
                    foreach ($messages as $msg) {
94✔
4715
                        list ($type, $item, $location) = Message::parseSubject($msg['subject']);
57✔
4716

4717
                        if ($item) {
57✔
4718
                            $grp = $location;
37✔
4719

4720
                            // Handle some misformed locations which end up with spurious brackets.
4721
                            $grp = preg_replace('/\(|\)/', '', $grp);
37✔
4722

4723
                            $users[$userid]['info']['publiclocation'] = [
37✔
4724
                                'display' => $grp,
4725
                                'location' => NULL,
4726
                                'groupname' => $grp
4727
                            ];
4728

4729
                            $found[] = $userid;
37✔
4730
                        }
4731
                    }
4732
                }
4733

4734
                $idsleft = array_diff($idsleft, $found);
94✔
4735
                
4736
                # Now check just membership.
4737
                if (count($idsleft)) {
94✔
4738
                    if (!$membs) {
63✔
4739
                        $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(
20✔
4740
                                ',',
4741
                                $idsleft
4742
                            ) . ") ORDER BY added ASC;";
4743
                        $membs = $this->dbhr->preQuery($sql);
20✔
4744
                    }
4745
                    
4746
                    foreach ($idsleft as $userid) {
63✔
4747
                        # Now check the group we joined most recently.
4748
                        foreach ($membs as $memb) {
63✔
4749
                            if ($memb['userid'] == $userid) {
48✔
4750
                                $grp = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
48✔
4751

4752
                                $users[$userid]['info']['publiclocation'] = [
48✔
4753
                                    'display' => $grp,
4754
                                    'location' => NULL,
4755
                                    'groupname' => $grp
4756
                                ];
4757
                            }
4758
                        }
4759
                    }
4760
                }
4761
            }
4762
        }
4763
    }
4764

4765
    public function getLatLngs($users, $usedef = TRUE, $usegroup = TRUE, $needgroup = FALSE, $atts = NULL, $blur = NULL)
4766
    {
4767
        $userids = array_filter(array_column($users, 'id'));
159✔
4768
        $ret = [];
159✔
4769

4770
        if ($userids && count($userids)) {
159✔
4771
            $atts = $atts ? $atts : $this->dbhr->preQuery("SELECT id, settings, lastlocation FROM users WHERE id in (" . implode(',', $userids) . ");", NULL, FALSE, FALSE);
159✔
4772

4773
            foreach ($atts as $att) {
159✔
4774
                $lat = NULL;
159✔
4775
                $lng = NULL;
159✔
4776
                $loc = NULL;
159✔
4777

4778
                if (Utils::pres('settings', $att)) {
159✔
4779
                    $settings = $att['settings'];
34✔
4780
                    $settings = json_decode($settings, TRUE);
34✔
4781

4782
                    if (Utils::pres('mylocation', $settings)) {
34✔
4783
                        $lat = $settings['mylocation']['lat'];
30✔
4784
                        $lng = $settings['mylocation']['lng'];
30✔
4785
                        $loc = Utils::presdef('name', $settings['mylocation'], NULL);
30✔
4786
                        #error_log("Got from mylocation $lat, $lng, $loc");
4787
                    }
4788
                }
4789

4790
                if (is_null($lat)) {
159✔
4791
                    $lid = $att['lastlocation'];
141✔
4792

4793
                    if ($lid) {
141✔
4794
                        $l = new Location($this->dbhr, $this->dbhm, $lid);
18✔
4795
                        $lat = $l->getPrivate('lat');
18✔
4796
                        $lng = $l->getPrivate('lng');
18✔
4797
                        $loc = $l->getPrivate('name');
18✔
4798
                        #error_log("Got from last location $lat, $lng, $loc");
4799
                    }
4800
                }
4801

4802
                if (!is_null($lat)) {
159✔
4803
                    $ret[$att['id']] = [
44✔
4804
                        'lat' => $lat,
4805
                        'lng' => $lng,
4806
                        'loc' => $loc,
4807
                    ];
4808

4809
                    $userids = array_filter($userids, function($id) use ($att) {
44✔
4810
                        return $id != $att['id'];
44✔
4811
                    });
4812
                }
4813
            }
4814
        }
4815

4816
        if ($userids && count($userids) && $usegroup) {
159✔
4817
            # Still some we haven't handled.  Get the last message posted on a group with a location, if any.
4818
            $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);
136✔
4819
            foreach ($membs as $memb) {
136✔
4820
                $ret[$memb['userid']] = [
3✔
4821
                    'lat' => $memb['lat'],
3✔
4822
                    'lng' => $memb['lng']
3✔
4823
                ];
4824

4825
                #error_log("Got from last message posted {$memb['lat']}, {$memb['lng']}");
4826

4827
                $userids = array_filter($userids, function($id) use ($memb) {
3✔
4828
                    return $id != $memb['userid'];
3✔
4829
                });
4830
            }
4831
        }
4832

4833
        if ($userids && count($userids) && $usegroup) {
159✔
4834
            # Still some we haven't handled.  Get the memberships.  Logic will choose most recently joined.
4835
            $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);
134✔
4836
            foreach ($membs as $memb) {
134✔
4837
                $ret[$memb['userid']] = [
119✔
4838
                    'lat' => $memb['lat'],
119✔
4839
                    'lng' => $memb['lng'],
119✔
4840
                    'group' => Utils::presdef('namefull', $memb, $memb['nameshort'])
119✔
4841
                ];
4842

4843
                #error_log("Got from membership {$memb['lat']}, {$memb['lng']}, " . Utils::presdef('namefull', $memb, $memb['nameshort']));
4844

4845
                $userids = array_filter($userids, function($id) use ($memb) {
119✔
4846
                    return $id != $memb['userid'];
119✔
4847
                });
4848
            }
4849
        }
4850

4851
        if ($userids && count($userids)) {
159✔
4852
            # Still some we haven't handled.
4853
            foreach ($userids as $userid) {
20✔
4854
                if ($usedef) {
20✔
4855
                    $ret[$userid] = [
18✔
4856
                        'lat' => 53.9450,
4857
                        'lng' => -2.5209
4858
                    ];
4859
                } else {
4860
                    $ret[$userid] = NULL;
13✔
4861
                }
4862
            }
4863
        }
4864

4865
        if ($needgroup) {
159✔
4866
            # Get a group name.
4867
            $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✔
4868
            foreach ($membs as $memb) {
7✔
4869
                $ret[$memb['userid']]['group'] = Utils::presdef('namefull', $memb, $memb['nameshort']);
7✔
4870
            }
4871
        }
4872

4873
        if ($blur) {
159✔
4874
            foreach ($ret as &$memb) {
7✔
4875
                if ($memb['lat'] || $memb['lng']) {
7✔
4876
                    list ($memb['lat'], $memb['lng']) = Utils::blur($memb['lat'], $memb['lng'], $blur);
7✔
4877
                }
4878
            }
4879
        }
4880

4881
        return ($ret);
159✔
4882
    }
4883

4884
    public function isFreegleMod()
4885
    {
4886
        $ret = FALSE;
162✔
4887

4888
        $this->cacheMemberships();
162✔
4889

4890
        foreach ($this->memberships as $mem) {
162✔
4891
            if ($mem['type'] == Group::GROUP_FREEGLE && ($mem['role'] == User::ROLE_OWNER || $mem['role'] == User::ROLE_MODERATOR)) {
136✔
4892
                $ret = TRUE;
35✔
4893
            }
4894
        }
4895

4896
        return ($ret);
162✔
4897
    }
4898

4899
    public function getKudos($id = NULL)
4900
    {
4901
        $id = $id ? $id : $this->id;
1✔
4902
        $kudos = [
1✔
4903
            'userid' => $id,
4904
            'posts' => 0,
4905
            'chats' => 0,
4906
            'newsfeed' => 0,
4907
            'events' => 0,
4908
            'vols' => 0,
4909
            'facebook' => 0,
4910
            'platform' => 0,
4911
            'kudos' => 0,
4912
        ];
4913

4914
        $kudi = $this->dbhr->preQuery("SELECT * FROM users_kudos WHERE userid = ?;", [
1✔
4915
            $id
4916
        ]);
4917

4918
        foreach ($kudi as $k) {
1✔
4919
            $kudos = $k;
1✔
4920
        }
4921

4922
        return ($kudos);
1✔
4923
    }
4924

4925
    public function updateKudos($id = NULL, $force = FALSE)
4926
    {
4927
        $current = $this->getKudos($id);
1✔
4928

4929
        # Only update if we don't have one or it's older than a day.  This avoids repeatedly updating the entry
4930
        # for the same user in some bulk operations.
4931
        if (!Utils::pres('timestamp', $current) || (time() - strtotime($current['timestamp']) > 24 * 60 * 60)) {
1✔
4932
            # We analyse a user's activity and assign them a level.
4933
            #
4934
            # Only interested in activity in the last year.
4935
            $id = $id ? $id : $this->id;
1✔
4936
            $start = date('Y-m-d', strtotime("365 days ago"));
1✔
4937

4938
            # First, the number of months in which they have posted.
4939
            $posts = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM messages WHERE fromuser = ? AND date >= '$start';", [
1✔
4940
                $id
4941
            ])[0]['count'];
1✔
4942

4943
            # Ditto communicated with people.
4944
            $chats = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM chat_messages WHERE userid = ? AND date >= '$start';", [
1✔
4945
                $id
4946
            ])[0]['count'];
1✔
4947

4948
            # Newsfeed posts
4949
            $newsfeed = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(timestamp), '-', MONTH(timestamp)))) AS count FROM newsfeed WHERE userid = ? AND added >= '$start';", [
1✔
4950
                $id
4951
            ])[0]['count'];
1✔
4952

4953
            # Events
4954
            $events = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM communityevents WHERE userid = ? AND added >= '$start';", [
1✔
4955
                $id
4956
            ])[0]['count'];
1✔
4957

4958
            # Volunteering
4959
            $vols = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM volunteering WHERE userid = ? AND added >= '$start';", [
1✔
4960
                $id
4961
            ])[0]['count'];
1✔
4962

4963
            # Do they have a Facebook login?
4964
            $facebook = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_logins WHERE userid = ? AND type = ?", [
1✔
4965
                    $id,
4966
                    User::LOGIN_FACEBOOK
4967
                ])[0]['count'] > 0;
1✔
4968

4969
            # Have they posted using the platform?
4970
            $platform = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM messages WHERE fromuser = ? AND arrival >= '$start' AND sourceheader = ?;", [
1✔
4971
                    $id,
4972
                    Message::PLATFORM
4973
                ])[0]['count'] > 0;
1✔
4974

4975
            $kudos = $posts + $chats + $newsfeed + $events + $vols;
1✔
4976

4977
            if ($kudos > 0 || $force) {
1✔
4978
                # No sense in creating entries which are blank or the same.
4979
                $current = $this->getKudos($id);
1✔
4980

4981
                if ($current['kudos'] != $kudos || $force) {
1✔
4982
                    $this->dbhm->preExec("REPLACE INTO users_kudos (userid, kudos, posts, chats, newsfeed, events, vols, facebook, platform) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", [
1✔
4983
                        $id,
4984
                        $kudos,
4985
                        $posts,
4986
                        $chats,
4987
                        $newsfeed,
4988
                        $events,
4989
                        $vols,
4990
                        $facebook,
4991
                        $platform
4992
                    ], FALSE);
4993
                }
4994
            }
4995
        }
4996
    }
4997

4998
    public function topKudos($gid, $limit = 10)
4999
    {
5000
        $limit = intval($limit);
1✔
5001

5002
        $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✔
5003
            $gid,
5004
            User::ROLE_MEMBER
5005
        ]);
5006

5007
        $ret = [];
1✔
5008

5009
        foreach ($kudos as $k) {
1✔
5010
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5011
            $atts = $u->getPublic();
1✔
5012
            $atts['email'] = $u->getEmailPreferred();
1✔
5013

5014
            $thisone = [
1✔
5015
                'user' => $atts,
5016
                'kudos' => $k
5017
            ];
5018

5019
            $ret[] = $thisone;
1✔
5020
        }
5021

5022
        return ($ret);
1✔
5023
    }
5024

5025
    public function possibleMods($gid, $limit = 10)
5026
    {
5027
        # We look for users who are not mods with top kudos who also:
5028
        # - active in last 60 days
5029
        # - not bouncing
5030
        # - using a location which is in the group area
5031
        # - have posted with the platform, as we don't want loyal users of TN or Yahoo.
5032
        # - have a Facebook login, as they are more likely to do publicity.
5033
        $limit = intval($limit);
1✔
5034
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
5035
        $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✔
5036
        $kudos = $this->dbhr->preQuery($sql, [
1✔
5037
            $gid,
5038
            User::ROLE_MEMBER
5039
        ]);
5040

5041
        $ret = [];
1✔
5042

5043
        foreach ($kudos as $k) {
1✔
5044
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5045
            $atts = $u->getPublic();
1✔
5046
            $atts['email'] = $u->getEmailPreferred();
1✔
5047

5048
            $thisone = [
1✔
5049
                'user' => $atts,
5050
                'kudos' => $k
5051
            ];
5052

5053
            $ret[] = $thisone;
1✔
5054
        }
5055

5056
        return ($ret);
1✔
5057
    }
5058

5059
    public function requestExport($sync = FALSE)
5060
    {
5061
        $tag = Utils::randstr(64);
8✔
5062

5063
        # Flag sync ones as started to avoid window with background thread.
5064
        $sync = $sync ? "NOW()" : "NULL";
8✔
5065
        $this->dbhm->preExec("INSERT INTO users_exports (userid, tag, started) VALUES (?, ?, $sync);", [
8✔
5066
            $this->id,
8✔
5067
            $tag
5068
        ]);
5069

5070
        return ([$this->dbhm->lastInsertId(), $tag]);
8✔
5071
    }
5072

5073
    public function export($exportid, $tag)
5074
    {
5075
        $this->dbhm->preExec("UPDATE users_exports SET started = NOW() WHERE id = ? AND tag = ?;", [
7✔
5076
            $exportid,
5077
            $tag
5078
        ]);
5079

5080
        # For GDPR we support the ability for a user to export the data we hold about them.  Key points about this:
5081
        #
5082
        # - It needs to be at a high level of abstraction and understandable by the user, not just a cryptic data
5083
        #   dump.
5084
        # - It needs to include data provided by the user and data observed about the user, but not profiling
5085
        #   or categorisation based on that data.  This means that (for example) we need to return which
5086
        #   groups they have joined, but not whether joining those groups has flagged them up as a potential
5087
        #   spammer.
5088
        $ret = [];
7✔
5089
        error_log("...basic info");
7✔
5090

5091
        # Data in user table.
5092
        $d = [];
7✔
5093
        $d['Our_internal_ID_for_you'] = $this->getPrivate('id');
7✔
5094
        $d['Your_full_name'] = $this->getPrivate('fullname');
7✔
5095
        $d['Your_first_name'] = $this->getPrivate('firstname');
7✔
5096
        $d['Your_last_name'] = $this->getPrivate('lastname');
7✔
5097
        $d['Your_Yahoo_ID'] = $this->getPrivate('yahooid');
7✔
5098
        $d['Your_role_on_the_system'] = $this->getPrivate('systemrole');
7✔
5099
        $d['When_you_joined_the_site'] = Utils::ISODate($this->getPrivate('added'));
7✔
5100
        $d['When_you_last_accessed_the_site'] = Utils::ISODate($this->getPrivate('lastaccess'));
7✔
5101
        $d['When_we_last_checked_for_relevant_posts_for_you'] = Utils::ISODate($this->getPrivate('lastrelevantcheck'));
7✔
5102
        $d['Whether_your_email_is_bouncing'] = $this->getPrivate('bouncing') ? 'Yes' : 'No';
7✔
5103
        $d['Permissions_you_have_on_the_site'] = $this->getPrivate('permissions');
7✔
5104
        $d['Number_of_remaining_invitations_you_can_send_to_other_people'] = $this->getPrivate('invitesleft');
7✔
5105

5106
        $lastlocation = $this->user['lastlocation'];
7✔
5107

5108
        if ($lastlocation) {
7✔
5109
            $l = new Location($this->dbhr, $this->dbhm, $lastlocation);
×
5110
            $d['Last_location_you_posted_from'] = $l->getPrivate('name') . " (" . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
×
5111
        }
5112

5113
        $settings = $this->getPrivate('settings');
7✔
5114

5115
        if ($settings) {
7✔
5116
            $settings = json_decode($settings, TRUE);
7✔
5117

5118
            $location = Utils::presdef('id', Utils::presdef('mylocation', $settings, []), NULL);
7✔
5119

5120
            if ($location) {
7✔
5121
                $l = new Location($this->dbhr, $this->dbhm, $location);
6✔
5122
                $d['Last_location_you_entered'] = $l->getPrivate('name') . ' (' . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
6✔
5123
            }
5124

5125
            $notifications = Utils::pres('notifications', $settings);
7✔
5126

5127
            $d['Notifications']['Send_email_notifications_for_chat_messages'] = Utils::presdef('email', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5128
            $d['Notifications']['Send_email_notifications_of_chat_messages_you_send'] = Utils::presdef('emailmine', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5129
            $d['Notifications']['Send_notifications_for_apps'] = Utils::presdef('app', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5130
            $d['Notifications']['Send_push_notifications_to_web_browsers'] = Utils::presdef('push', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5131
            $d['Notifications']['Send_Facebook_notifications'] = Utils::presdef('facebook', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5132
            $d['Notifications']['Send_emails_about_notifications_on_the_site'] = Utils::presdef('notificationmails', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5133

5134
            $d['Hide_profile_picture'] = Utils::presdef('useprofile', $settings, TRUE) ? 'Yes' : 'No';
7✔
5135

5136
            if ($this->isModerator()) {
7✔
5137
                $d['Show_members_that_you_are_a_moderator'] = Utils::pres('showmod', $settings) ? 'Yes' : 'No';
1✔
5138

5139
                switch (Utils::presdef('modnotifs', $settings, 4)) {
1✔
5140
                    case 24:
1✔
5141
                        $d['Send_notifications_of_active_mod_work'] = 'After 24 hours';
×
5142
                        break;
×
5143
                    case 12:
1✔
5144
                        $d['Send_notifications_of_active_mod_work'] = 'After 12 hours';
×
5145
                        break;
×
5146
                    case 4:
1✔
5147
                        $d['Send_notifications_of_active_mod_work'] = 'After 4 hours';
1✔
5148
                        break;
1✔
5149
                    case 2:
×
5150
                        $d['Send_notifications_of_active_mod_work'] = 'After 2 hours';
×
5151
                        break;
×
5152
                    case 1:
×
5153
                        $d['Send_notifications_of_active_mod_work'] = 'After 1 hours';
×
5154
                        break;
×
5155
                    case 0:
×
5156
                        $d['Send_notifications_of_active_mod_work'] = 'Immediately';
×
5157
                        break;
×
5158
                    case -1:
5159
                        $d['Send_notifications_of_active_mod_work'] = 'Never';
×
5160
                        break;
×
5161
                }
5162

5163
                switch (Utils::presdef('backupmodnotifs', $settings, 12)) {
1✔
5164
                    case 24:
1✔
5165
                        $d['Send_notifications_of_backup_mod_work'] = 'After 24 hours';
×
5166
                        break;
×
5167
                    case 12:
1✔
5168
                        $d['Send_notifications_of_backup_mod_work'] = 'After 12 hours';
1✔
5169
                        break;
1✔
5170
                    case 4:
×
5171
                        $d['Send_notifications_of_backup_mod_work'] = 'After 4 hours';
×
5172
                        break;
×
5173
                    case 2:
×
5174
                        $d['Send_notifications_of_backup_mod_work'] = 'After 2 hours';
×
5175
                        break;
×
5176
                    case 1:
×
5177
                        $d['Send_notifications_of_backup_mod_work'] = 'After 1 hours';
×
5178
                        break;
×
5179
                    case 0:
×
5180
                        $d['Send_notifications_of_backup_mod_work'] = 'Immediately';
×
5181
                        break;
×
5182
                    case -1:
5183
                        $d['Send_notifications_of_backup_mod_work'] = 'Never';
×
5184
                        break;
×
5185
                }
5186

5187
                $d['Show_members_that_you_are_a_moderator'] = Utils::presdef('showmod', $settings, TRUE) ? 'Yes' : 'No';
1✔
5188
            }
5189
        }
5190

5191
        # Invitations.  Only show what we sent; the outcome is not this user's business.
5192
        error_log("...invitations");
7✔
5193
        $invites = $this->listInvitations("1970-01-01");
7✔
5194
        $d['invitations'] = [];
7✔
5195

5196
        foreach ($invites as $invite) {
7✔
5197
            $d['invitations'][] = [
6✔
5198
                'email' => $invite['email'],
6✔
5199
                'date' => Utils::ISODate($invite['date'])
6✔
5200
            ];
5201
        }
5202

5203
        error_log("...emails");
7✔
5204
        $d['emails'] = $this->getEmails();
7✔
5205

5206
        foreach ($d['emails'] as &$email) {
7✔
5207
            $email['added'] = Utils::ISODate($email['added']);
1✔
5208

5209
            if ($email['validated']) {
1✔
5210
                $email['validated'] = Utils::ISODate($email['validated']);
×
5211
            }
5212
        }
5213

5214
        $phones = $this->dbhr->preQuery("SELECT * FROM users_phones WHERE userid = ?;", [
7✔
5215
            $this->id
7✔
5216
        ]);
5217

5218
        foreach ($phones as $phone) {
7✔
5219
            $d['phone'] = $phone['number'];
6✔
5220
            $d['phonelastsent'] = Utils::ISODate($phone['lastsent']);
6✔
5221
            $d['phonelastclicked'] = Utils::ISODate($phone['lastclicked']);
6✔
5222
        }
5223

5224
        error_log("...logins");
7✔
5225
        $d['logins'] = $this->dbhr->preQuery("SELECT type, uid, added, lastaccess FROM users_logins WHERE userid = ?;", [
7✔
5226
            $this->id
7✔
5227
        ]);
5228

5229
        foreach ($d['logins'] as &$dd) {
7✔
5230
            $dd['added'] = Utils::ISODate($dd['added']);
7✔
5231
            $dd['lastaccess'] = Utils::ISODate($dd['lastaccess']);
7✔
5232
        }
5233

5234
        error_log("...memberships");
7✔
5235
        $d['memberships'] = $this->getMemberships();
7✔
5236

5237
        error_log("...memberships history");
7✔
5238
        $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✔
5239
        $membs = $this->dbhr->preQuery($sql, [$this->id]);
7✔
5240
        foreach ($membs as &$memb) {
7✔
5241
            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
7✔
5242
            $memb['namedisplay'] = $name;
7✔
5243
            $memb['added'] = Utils::ISODate($memb['added']);
7✔
5244
        }
5245

5246
        $d['membershipshistory'] = $membs;
7✔
5247

5248
        error_log("...searches");
7✔
5249
        $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✔
5250
            $this->id
7✔
5251
        ]);
5252

5253
        foreach ($d['searches'] as &$s) {
7✔
5254
            $s['date'] = Utils::ISODate($s['date']);
×
5255
        }
5256

5257
        error_log("...alerts");
7✔
5258
        $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✔
5259
            $this->id
7✔
5260
        ]);
5261

5262
        foreach ($d['alerts'] as &$s) {
7✔
5263
            $s['responded'] = Utils::ISODate($s['responded']);
×
5264
        }
5265

5266
        error_log("...donations");
7✔
5267
        $d['donations'] = $this->dbhr->preQuery("SELECT * FROM users_donations WHERE userid = ? ORDER BY timestamp ASC;", [
7✔
5268
            $this->id
7✔
5269
        ]);
5270

5271
        foreach ($d['donations'] as &$s) {
7✔
5272
            $s['timestamp'] = Utils::ISODate($s['timestamp']);
1✔
5273
        }
5274

5275
        error_log("...bans");
7✔
5276
        $d['bans'] = [];
7✔
5277

5278
        $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE byuser = ?;", [
7✔
5279
            $this->id
7✔
5280
        ]);
5281

5282
        foreach ($bans as $ban) {
7✔
5283
            $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
5284
            $u = User::get($this->dbhr, $this->dbhm, $ban['userid']);
1✔
5285
            $d['bans'][] = [
1✔
5286
                'date' => Utils::ISODate($ban['date']),
1✔
5287
                'group' => $g->getName(),
1✔
5288
                'email' => $u->getEmailPreferred(),
1✔
5289
                'userid' => $ban['userid']
1✔
5290
            ];
5291
        }
5292

5293
        error_log("...spammers");
7✔
5294
        $d['spammers'] = $this->dbhr->preQuery("SELECT * FROM spam_users WHERE byuserid = ? ORDER BY added ASC;", [
7✔
5295
            $this->id
7✔
5296
        ]);
5297

5298
        foreach ($d['spammers'] as &$s) {
7✔
5299
            $s['added'] = Utils::ISODate($s['added']);
1✔
5300
            $u = User::get($this->dbhr, $this->dbhm, $s['userid']);
1✔
5301
            $s['email'] = $u->getEmailPreferred();
1✔
5302
        }
5303

5304
        $d['spamdomains'] = $this->dbhr->preQuery("SELECT domain, date FROM spam_whitelist_links WHERE userid = ?;", [
7✔
5305
            $this->id
7✔
5306
        ]);
5307

5308
        foreach ($d['spamdomains'] as &$s) {
7✔
5309
            $s['date'] = Utils::ISODate($s['date']);
×
5310
        }
5311

5312
        error_log("...images");
7✔
5313
        $images = $this->dbhr->preQuery("SELECT id, url FROM users_images WHERE userid = ?;", [
7✔
5314
            $this->id
7✔
5315
        ]);
5316

5317
        $d['images'] = [];
7✔
5318

5319
        foreach ($images as $image) {
7✔
5320
            if (Utils::pres('url', $image)) {
6✔
5321
                $d['images'][] = [
6✔
5322
                    'id' => $image['id'],
6✔
5323
                    'thumb' => $image['url']
6✔
5324
                ];
5325
            } else {
5326
                $a = new Attachment($this->dbhr, $this->dbhm, NULL, Attachment::TYPE_USER);
×
5327
                $d['images'][] = [
×
5328
                    'id' => $image['id'],
×
5329
                    'thumb' => $a->getPath(TRUE, $image['id'])
×
5330
                ];
5331
            }
5332
        }
5333

5334
        error_log("...notifications");
7✔
5335
        $d['notifications'] = $this->dbhr->preQuery("SELECT timestamp, url FROM users_notifications WHERE touser = ? AND seen = 1;", [
7✔
5336
            $this->id
7✔
5337
        ]);
5338

5339
        foreach ($d['notifications'] as &$n) {
7✔
5340
            $n['timestamp'] = Utils::ISODate($n['timestamp']);
×
5341
        }
5342

5343
        error_log("...addresses");
7✔
5344
        $d['addresses'] = [];
7✔
5345

5346
        $addrs = $this->dbhr->preQuery("SELECT * FROM users_addresses WHERE userid = ?;", [
7✔
5347
            $this->id
7✔
5348
        ]);
5349

5350
        foreach ($addrs as $addr) {
7✔
5351
            $a = new Address($this->dbhr, $this->dbhm, $addr['id']);
×
5352
            $d['addresses'][] = $a->getPublic();
×
5353
        }
5354

5355
        error_log("...events");
7✔
5356
        $d['communityevents'] = [];
7✔
5357

5358
        $events = $this->dbhr->preQuery("SELECT id FROM communityevents WHERE userid = ?;", [
7✔
5359
            $this->id
7✔
5360
        ]);
5361

5362
        foreach ($events as $event) {
7✔
5363
            $e = new CommunityEvent($this->dbhr, $this->dbhm, $event['id']);
×
5364
            $d['communityevents'][] = $e->getPublic();
×
5365
        }
5366

5367
        error_log("...volunteering");
7✔
5368
        $d['volunteering'] = [];
7✔
5369

5370
        $events = $this->dbhr->preQuery("SELECT id FROM volunteering WHERE userid = ?;", [
7✔
5371
            $this->id
7✔
5372
        ]);
5373

5374
        foreach ($events as $event) {
7✔
5375
            $e = new Volunteering($this->dbhr, $this->dbhm, $event['id']);
×
5376
            $d['volunteering'][] = $e->getPublic();
×
5377
        }
5378

5379
        error_log("...comments");
7✔
5380
        $d['comments'] = [];
7✔
5381
        $comms = $this->dbhr->preQuery("SELECT * FROM users_comments WHERE byuserid = ? ORDER BY date ASC;", [
7✔
5382
            $this->id
7✔
5383
        ]);
5384

5385
        foreach ($comms as &$comm) {
7✔
5386
            $u = User::get($this->dbhr, $this->dbhm, $comm['userid']);
1✔
5387
            $comm['email'] = $u->getEmailPreferred();
1✔
5388
            $comm['date'] = Utils::ISODate($comm['date']);
1✔
5389
            $d['comments'][] = $comm;
1✔
5390
        }
5391

5392
        error_log("...ratings");
7✔
5393
        $d['ratings'] = $this->getRated();
7✔
5394

5395
        error_log("...locations");
7✔
5396
        $d['locations'] = [];
7✔
5397

5398
        $locs = $this->dbhr->preQuery("SELECT * FROM locations_excluded WHERE userid = ?;", [
7✔
5399
            $this->id
7✔
5400
        ]);
5401

5402
        foreach ($locs as $loc) {
7✔
5403
            $g = Group::get($this->dbhr, $this->dbhm, $loc['groupid']);
×
5404
            $l = new Location($this->dbhr, $this->dbhm, $loc['locationid']);
×
5405
            $d['locations'][] = [
×
5406
                'group' => $g->getName(),
×
5407
                'location' => $l->getPrivate('name'),
×
5408
                'date' => Utils::ISODate($loc['date'])
×
5409
            ];
5410
        }
5411

5412
        error_log("...messages");
7✔
5413
        $msgs = $this->dbhr->preQuery("SELECT id FROM messages WHERE fromuser = ? ORDER BY arrival ASC;", [
7✔
5414
            $this->id
7✔
5415
        ]);
5416

5417
        $d['messages'] = [];
7✔
5418

5419
        foreach ($msgs as $msg) {
7✔
5420
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
×
5421

5422
            # Show all info here even moderator attributes.  This wouldn't normally be shown to users, but none
5423
            # of it is confidential really.
5424
            $thisone = $m->getPublic(FALSE, FALSE, TRUE);
×
5425

5426
            if (count($thisone['groups']) > 0) {
×
5427
                $g = Group::get($this->dbhr, $this->dbhm, $thisone['groups'][0]['groupid']);
×
5428
                $thisone['groups'][0]['namedisplay'] = $g->getName();
×
5429
            }
5430

5431
            $d['messages'][] = $thisone;
×
5432
        }
5433

5434
        # Chats.  Can't use listForUser as that filters on various things and has a ModTools vs FD distinction, and
5435
        # we're interested in information we have provided.  So we get the chats mentioned in the roster (we have
5436
        # provided information about being online) and where we have sent or reviewed a chat message.
5437
        error_log("...chats");
7✔
5438
        $chatids = $this->dbhr->preQuery("SELECT DISTINCT  id 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✔
5439
            $this->id,
7✔
5440
            $this->id,
7✔
5441
            $this->id
7✔
5442
        ]);
5443

5444
        $d['chatrooms'] = [];
7✔
5445
        $count = 0;
7✔
5446

5447
        foreach ($chatids as $chatid) {
7✔
5448
            # We don't return the chat name because it's too slow to produce.
5449
            $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid['id']);
6✔
5450
            $thisone = [
6✔
5451
                'id' => $chatid['id'],
6✔
5452
                'name' => $r->getPublic($this)['name'],
6✔
5453
                'messages' => []
5454
            ];
5455

5456
            $sql = "SELECT date, lastip FROM chat_roster WHERE `chatid` = ? AND userid = ?;";
6✔
5457
            $roster = $this->dbhr->preQuery($sql, [$chatid['id'], $this->id]);
6✔
5458
            foreach ($roster as $rost) {
6✔
5459
                $thisone['lastip'] = $rost['lastip'];
6✔
5460
                $thisone['date'] = Utils::ISODate($rost['date']);
6✔
5461
            }
5462

5463
            # Get the messages we have sent in this chat.
5464
            $msgs = $this->dbhr->preQuery("SELECT id FROM chat_messages WHERE chatid = ? AND (userid = ? OR reviewedby = ?);", [
6✔
5465
                $chatid['id'],
6✔
5466
                $this->id,
6✔
5467
                $this->id
6✔
5468
            ]);
5469

5470
            $userlist = NULL;
6✔
5471

5472
            foreach ($msgs as $msg) {
6✔
5473
                $cm = new ChatMessage($this->dbhr, $this->dbhm, $msg['id']);
6✔
5474
                $thismsg = $cm->getPublic(FALSE, $userlist);
6✔
5475

5476
                # Strip out most of the refmsg detail - it's not ours and we need to save volume of data.
5477
                $refmsg = Utils::pres('refmsg', $thismsg);
6✔
5478

5479
                if ($refmsg) {
6✔
5480
                    $thismsg['refmsg'] = [
×
5481
                        'id' => $msg['id'],
×
5482
                        'subject' => Utils::presdef('subject', $refmsg, NULL)
×
5483
                    ];
5484
                }
5485

5486
                $thismsg['mine'] = Utils::presdef('userid', $thismsg, NULL) == $this->id;
6✔
5487
                $thismsg['date'] = Utils::ISODate($thismsg['date']);
6✔
5488
                $thisone['messages'][] = $thismsg;
6✔
5489

5490
                $count++;
6✔
5491
//
5492
//                if ($count > 200) {
5493
//                    break 2;
5494
//                }
5495
            }
5496

5497
            if (count($thisone['messages']) > 0) {
6✔
5498
                $d['chatrooms'][] = $thisone;
6✔
5499
            }
5500
        }
5501

5502
        error_log("...newsfeed");
7✔
5503
        $newsfeeds = $this->dbhr->preQuery("SELECT * FROM newsfeed WHERE userid = ?;", [
7✔
5504
            $this->id
7✔
5505
        ]);
5506

5507
        $d['newsfeed'] = [];
7✔
5508

5509
        foreach ($newsfeeds as $newsfeed) {
7✔
5510
            $n = new Newsfeed($this->dbhr, $this->dbhm, $newsfeed['id']);
6✔
5511
            $thisone = $n->getPublic(FALSE, FALSE, FALSE, FALSE);
6✔
5512
            $d['newsfeed'][] = $thisone;
6✔
5513
        }
5514

5515
        $d['newsfeed_unfollows'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_unfollow WHERE userid = ?;", [
7✔
5516
            $this->id
7✔
5517
        ]);
5518

5519
        foreach ($d['newsfeed_unfollows'] as &$dd) {
7✔
5520
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5521
        }
5522

5523
        $d['newsfeed_likes'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_likes WHERE userid = ?;", [
7✔
5524
            $this->id
7✔
5525
        ]);
5526

5527
        foreach ($d['newsfeed_likes'] as &$dd) {
7✔
5528
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5529
        }
5530

5531
        $d['newsfeed_reports'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_reports WHERE userid = ?;", [
7✔
5532
            $this->id
7✔
5533
        ]);
5534

5535
        foreach ($d['newsfeed_reports'] as &$dd) {
7✔
5536
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5537
        }
5538

5539
        $d['aboutme'] = $this->dbhr->preQuery("SELECT timestamp, text FROM users_aboutme WHERE userid = ? AND LENGTH(text) > 5;", [
7✔
5540
            $this->id
7✔
5541
        ]);
5542

5543
        foreach ($d['aboutme'] as &$dd) {
7✔
5544
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5545
        }
5546

5547
        error_log("...stories");
7✔
5548
        $d['stories'] = $this->dbhr->preQuery("SELECT date, headline, story FROM users_stories WHERE userid = ?;", [
7✔
5549
            $this->id
7✔
5550
        ]);
5551

5552
        foreach ($d['stories'] as &$dd) {
7✔
5553
            $dd['date'] = Utils::ISODate($dd['date']);
×
5554
        }
5555

5556
        $d['stories_likes'] = $this->dbhr->preQuery("SELECT storyid FROM users_stories_likes WHERE userid = ?;", [
7✔
5557
            $this->id
7✔
5558
        ]);
5559

5560
        error_log("...exports");
7✔
5561
        $d['exports'] = $this->dbhr->preQuery("SELECT userid, started, completed FROM users_exports WHERE userid = ?;", [
7✔
5562
            $this->id
7✔
5563
        ]);
5564

5565
        foreach ($d['exports'] as &$dd) {
7✔
5566
            $dd['started'] = Utils::ISODate($dd['started']);
7✔
5567
            $dd['completed'] = Utils::ISODate($dd['completed']);
7✔
5568
        }
5569

5570
        error_log("...logs");
7✔
5571
        $l = new Log($this->dbhr, $this->dbhm);
7✔
5572
        $ctx = NULL;
7✔
5573
        $d['logs'] = $l->get(NULL, NULL, NULL, NULL, NULL, NULL, PHP_INT_MAX, $ctx, $this->id);
7✔
5574

5575
        error_log("...add group to logs");
7✔
5576
        $loggroups = [];
7✔
5577
        foreach ($d['logs'] as &$log) {
7✔
5578
            if (Utils::pres('groupid', $log)) {
7✔
5579
                # Don't put the whole group info in there, as it is slow to get.
5580
                if (!array_key_exists($log['groupid'], $loggroups)) {
7✔
5581
                    $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
7✔
5582

5583
                    if ($g->getId() == $log['groupid']) {
7✔
5584
                        $loggroups[$log['groupid']] = [
7✔
5585
                            'id' => $log['groupid'],
7✔
5586
                            'nameshort' => $g->getPrivate('nameshort'),
7✔
5587
                            'namedisplay' => $g->getName()
7✔
5588
                        ];
5589
                    } else {
5590
                        $loggroups[$log['groupid']] = [
×
5591
                            'id' => $log['groupid'],
×
5592
                            'nameshort' => "DeletedGroup{$log['groupid']}",
×
5593
                            'namedisplay' => "Deleted group #{$log['groupid']}"
×
5594
                        ];
5595
                    }
5596
                }
5597

5598
                $log['group'] = $loggroups[$log['groupid']];
7✔
5599
            }
5600
        }
5601

5602
        # Gift aid
5603
        $don = new Donations($this->dbhr, $this->dbhm);
7✔
5604
        $d['giftaid'] = $don->getGiftAid($this->id);
7✔
5605

5606
        $ret = $d;
7✔
5607

5608
        # There are some other tables with information which we don't return.  Here's what and why:
5609
        # - Not part of the current UI so can't have any user data
5610
        #     polls_users
5611
        # - Covered by data that we do return from other tables
5612
        #     messages_drafts, messages_history, messages_groups, messages_outcomes,
5613
        #     messages_promises, users_modmails, modnotifs, users_dashboard,
5614
        #     users_nudges
5615
        # - Transient logging data
5616
        #     logs_emails, logs_sql, logs_api, logs_errors, logs_src
5617
        # - Not provided by the user themselves
5618
        #     user_comments, messages_reneged, spam_users, users_banned, users_stories_requested,
5619
        #     users_thanks
5620
        # - Inferred or derived data.  These are not considered to be provided by the user (see p10 of
5621
        #   http://ec.europa.eu/newsroom/document.cfm?doc_id=44099)
5622
        #     users_kudos, visualise
5623

5624
        # Compress the data in the DB because it can be huge.
5625
        #
5626
        error_log("...filter");
7✔
5627
        Utils::filterResult($ret);
7✔
5628
        error_log("...encode");
7✔
5629
        $data = json_encode($ret, JSON_PARTIAL_OUTPUT_ON_ERROR);
7✔
5630
        error_log("...encoded length " . strlen($data) . ", now compress");
7✔
5631
        $data = gzdeflate($data);
7✔
5632
        $this->dbhm->preExec("UPDATE users_exports SET completed = NOW(), data = ? WHERE id = ? AND tag = ?;", [
7✔
5633
            $data,
5634
            $exportid,
5635
            $tag
5636
        ]);
5637
        error_log("...completed, length " . strlen($data));
7✔
5638

5639
        return ($ret);
7✔
5640
    }
5641

5642
    function getExport($userid, $id, $tag)
5643
    {
5644
        $ret = NULL;
2✔
5645

5646
        $exports = $this->dbhr->preQuery("SELECT * FROM users_exports WHERE userid = ? AND id = ? AND tag = ?;", [
2✔
5647
            $userid,
5648
            $id,
5649
            $tag
5650
        ]);
5651

5652
        foreach ($exports as $export) {
2✔
5653
            $ret = $export;
2✔
5654
            $ret['requested'] = $ret['requested'] ? Utils::ISODate($ret['requested']) : NULL;
2✔
5655
            $ret['started'] = $ret['started'] ? Utils::ISODate($ret['started']) : NULL;
2✔
5656
            $ret['completed'] = $ret['completed'] ? Utils::ISODate($ret['completed']) : NULL;
2✔
5657

5658
            if ($ret['completed']) {
2✔
5659
                # This has completed.  Return the data.  Will be zapped in cron exports..
5660
                $ret['data'] = json_decode(gzinflate($export['data']), TRUE);
2✔
5661
                $ret['infront'] = 0;
2✔
5662
            } else {
5663
                # Find how many are in front of us.
5664
                $infront = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_exports WHERE id < ? AND completed IS NULL;", [
2✔
5665
                    $id
5666
                ]);
5667

5668
                $ret['infront'] = $infront[0]['count'];
2✔
5669
            }
5670
        }
5671

5672
        return ($ret);
2✔
5673
    }
5674

5675
    public function forget($reason)
5676
    {
5677
        # Wipe a user of personal data, for the GDPR right to be forgotten.  We don't delete the user entirely
5678
        # otherwise it would mess up the stats.
5679

5680
        # Clear name etc.
5681
        $this->setPrivate('firstname', NULL);
6✔
5682
        $this->setPrivate('lastname', NULL);
6✔
5683
        $this->setPrivate('fullname', "Deleted User #" . $this->id);
6✔
5684
        $this->setPrivate('settings', NULL);
6✔
5685
        $this->setPrivate('yahooid', NULL);
6✔
5686

5687
        # Delete emails which aren't ours.
5688
        $emails = $this->getEmails();
6✔
5689

5690
        foreach ($emails as $email) {
6✔
5691
            if (!$email['ourdomain']) {
2✔
5692
                $this->removeEmail($email['email']);
2✔
5693
            }
5694
        }
5695

5696
        # Delete all logins.
5697
        $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ?;", [
6✔
5698
            $this->id
6✔
5699
        ]);
5700

5701
        # Delete any phone numbers.
5702
        $this->dbhm->preExec("DELETE FROM users_phones WHERE userid = ?;", [
6✔
5703
            $this->id
6✔
5704
        ]);
5705

5706
        # Delete the content (but not subject) of any messages, and any email header information such as their
5707
        # name and email address.
5708
        $msgs = $this->dbhm->preQuery("SELECT id FROM messages WHERE fromuser = ? AND messages.type IN (?, ?);", [
6✔
5709
            $this->id,
6✔
5710
            Message::TYPE_OFFER,
5711
            Message::TYPE_WANTED
5712
        ]);
5713

5714
        foreach ($msgs as $msg) {
6✔
5715
            $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✔
5716
                $msg['id']
1✔
5717
            ]);
5718

5719
            $this->dbhm->preExec("UPDATE messages_groups SET deleted = 1 WHERE msgid = ?;", [
1✔
5720
                $msg['id']
1✔
5721
            ]);
5722

5723
            # Delete outcome comments that they've added - just about might have personal data.
5724
            $this->dbhm->preExec("UPDATE messages_outcomes SET comments = NULL WHERE msgid = ?;", [
1✔
5725
                $msg['id']
1✔
5726
            ]);
5727

5728
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
1✔
5729

5730
            if (!$m->hasOutcome()) {
1✔
5731
                $m->withdraw('Withdrawn on user unsubscribe', NULL);
1✔
5732
            }
5733
        }
5734

5735
        # Remove all the content of all chat messages which they have sent (but not received).
5736
        $msgs = $this->dbhm->preQuery("SELECT id FROM chat_messages WHERE userid = ?;", [
6✔
5737
            $this->id
6✔
5738
        ]);
5739

5740
        foreach ($msgs as $msg) {
6✔
5741
            $this->dbhm->preExec("UPDATE chat_messages SET message = NULL WHERE id = ?;", [
1✔
5742
                $msg['id']
1✔
5743
            ]);
5744
        }
5745

5746
        # Delete completely any community events, volunteering opportunities, newsfeed posts, searches and stories
5747
        # they have created (their personal details might be in there), and any ratings by or about them.
5748
        $this->dbhm->preExec("DELETE FROM communityevents WHERE userid = ?;", [
6✔
5749
            $this->id
6✔
5750
        ]);
5751
        $this->dbhm->preExec("DELETE FROM volunteering WHERE userid = ?;", [
6✔
5752
            $this->id
6✔
5753
        ]);
5754
        $this->dbhm->preExec("DELETE FROM newsfeed WHERE userid = ?;", [
6✔
5755
            $this->id
6✔
5756
        ]);
5757
        $this->dbhm->preExec("DELETE FROM users_stories WHERE userid = ?;", [
6✔
5758
            $this->id
6✔
5759
        ]);
5760
        $this->dbhm->preExec("DELETE FROM users_searches WHERE userid = ?;", [
6✔
5761
            $this->id
6✔
5762
        ]);
5763
        $this->dbhm->preExec("DELETE FROM users_aboutme WHERE userid = ?;", [
6✔
5764
            $this->id
6✔
5765
        ]);
5766
        $this->dbhm->preExec("DELETE FROM ratings WHERE rater = ?;", [
6✔
5767
            $this->id
6✔
5768
        ]);
5769
        $this->dbhm->preExec("DELETE FROM ratings WHERE ratee = ?;", [
6✔
5770
            $this->id
6✔
5771
        ]);
5772

5773
        # Remove them from all groups.
5774
        $membs = $this->getMemberships();
6✔
5775

5776
        foreach ($membs as $memb) {
6✔
5777
            $this->removeMembership($memb['id']);
2✔
5778
        }
5779

5780
        # Delete any postal addresses
5781
        $this->dbhm->preExec("DELETE FROM users_addresses WHERE userid = ?;", [
6✔
5782
            $this->id
6✔
5783
        ]);
5784

5785
        # Delete any profile images
5786
        $this->dbhm->preExec("DELETE FROM users_images WHERE userid = ?;", [
6✔
5787
            $this->id
6✔
5788
        ]);
5789

5790
        # Remove any promises.
5791
        $this->dbhm->preExec("DELETE FROM messages_promises WHERE userid = ?;", [
6✔
5792
            $this->id
6✔
5793
        ]);
5794

5795
        $this->dbhm->preExec("UPDATE users SET deleted = NOW(), tnuserid = NULL WHERE id = ?;", [
6✔
5796
            $this->id
6✔
5797
        ]);
5798

5799
        $l = new Log($this->dbhr, $this->dbhm);
6✔
5800
        $l->log([
6✔
5801
            'type' => Log::TYPE_USER,
5802
            'subtype' => Log::SUBTYPE_DELETED,
5803
            'user' => $this->id,
6✔
5804
            'text' => $reason
5805
        ]);
5806
    }
5807

5808
    public function userRetention($userid = NULL)
5809
    {
5810
        # Find users who:
5811
        # - were added six months ago
5812
        # - are not on any groups
5813
        # - have not logged in for six months
5814
        # - are not on the spammer list
5815
        # - do not have mod notes
5816
        # - have no logs for six months
5817
        #
5818
        # We have no good reason to keep any data about them, and should therefore purge them.
5819
        $count = 0;
1✔
5820
        $userq = $userid ? " users.id = $userid AND " : '';
1✔
5821
        $mysqltime = date("Y-m-d", strtotime("6 months ago"));
1✔
5822
        $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✔
5823
        $users = $this->dbhr->preQuery($sql, [
1✔
5824
            User::SYSTEMROLE_USER
5825
        ]);
5826

5827
        foreach ($users as $user) {
1✔
5828
            $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✔
5829
                $user['id'],
1✔
5830
                Log::TYPE_USER,
5831
                Log::SUBTYPE_CREATED,
5832
                Log::SUBTYPE_DELETED
5833
            ]);
5834

5835
            error_log("#{$user['id']} Found logs " . count($logs) . " age " . (count($logs) > 0 ? $logs['0']['logsago'] : ' none '));
1✔
5836

5837
            if (count($logs) == 0 || $logs[0]['logsago'] > 90) {
1✔
5838
                error_log("...forget user #{$user['id']} " . (count($logs) > 0 ? $logs[0]['logsago'] : ''));
1✔
5839
                $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
5840
                $u->forget('Inactive');
1✔
5841
                $count++;
1✔
5842
            }
5843

5844
            # Prod garbage collection, as we've seen high memory usage by this.
5845
            User::clearCache();
1✔
5846
            gc_collect_cycles();
1✔
5847
        }
5848

5849
        error_log("...forgot $count");
1✔
5850

5851
        # The only reason for preserving deleted users is as a placeholder user for messages they sent.  If they
5852
        # don't have any messages, they can go.
5853
        $ids = $this->dbhr->preQuery("SELECT users.id FROM `users` LEFT JOIN messages ON messages.fromuser = users.id WHERE users.deleted IS NOT NULL AND users.lastaccess < ? AND messages.id IS NULL LIMIT 100000;", [
1✔
5854
            $mysqltime
5855
        ]);
5856

5857
        $total = count($ids);
1✔
5858
        $count = 0;
1✔
5859

5860
        foreach ($ids as $id) {
1✔
5861
            $u = new User($this->dbhr, $this->dbhm, $id['id']);
1✔
5862
            #error_log("...delete user #{$id['id']}");
5863
            $u->delete();
1✔
5864

5865
            $count++;
1✔
5866

5867
            if ($count % 1000 == 0) {
1✔
5868
                error_log("...delete $count / $total");
×
5869
            }
5870

5871

5872
            # Prod garbage collection, as we've seen high memory usage by this.
5873
            User::clearCache();
1✔
5874
            gc_collect_cycles();
1✔
5875
        }
5876

5877
        return ($count);
1✔
5878
    }
5879

5880
    public function recordActive()
5881
    {
5882
        # We record this on an hourly basis.  Avoid pointless mod ops for cluster health.
5883
        $now = date("Y-m-d H:00:00", time());
2✔
5884
        $already = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ? AND timestamp = ?;", [
2✔
5885
            $this->id,
2✔
5886
            $now
5887
        ]);
5888

5889
        if (count($already) == 0) {
2✔
5890
            $this->dbhm->background("INSERT IGNORE INTO users_active (userid, timestamp) VALUES ({$this->id}, '$now');");
2✔
5891
        }
5892
    }
5893

5894
    public function getActive()
5895
    {
5896
        $active = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ?;", [$this->id]);
1✔
5897
        return ($active);
1✔
5898
    }
5899

5900
    public function mostActive($gid, $limit = 20)
5901
    {
5902
        $limit = intval($limit);
1✔
5903
        $earliest = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
5904

5905
        $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✔
5906
            $gid,
5907
            User::SYSTEMROLE_USER,
5908
            $earliest
5909
        ]);
5910

5911
        $ret = [];
1✔
5912

5913
        foreach ($users as $user) {
1✔
5914
            $u = User::get($this->dbhr, $this->dbhm, $user['userid']);
1✔
5915
            $thisone = $u->getPublic();
1✔
5916
            $thisone['groupid'] = $gid;
1✔
5917
            $thisone['email'] = $u->getEmailPreferred();
1✔
5918

5919
            if (Utils::pres('memberof', $thisone)) {
1✔
5920
                foreach ($thisone['memberof'] as $group) {
1✔
5921
                    if ($group['id'] == $gid) {
1✔
5922
                        $thisone['joined'] = $group['added'];
1✔
5923
                    }
5924
                }
5925
            }
5926

5927
            $ret[] = $thisone;
1✔
5928
        }
5929

5930
        return ($ret);
1✔
5931
    }
5932

5933
    public function formatPhone($num)
5934
    {
5935
        $num = str_replace(' ', '', $num);
11✔
5936
        $num = preg_replace('/^(\+)?[04]+([^4])/', '$2', $num);
11✔
5937

5938
        if (substr($num, 0, 1) ==  '0') {
11✔
5939
            $num = substr($num, 1);
×
5940
        }
5941

5942
        $num = "+44$num";
11✔
5943

5944
        return ($num);
11✔
5945
    }
5946

5947
    public function sms($msg, $url, $from = TWILIO_FROM, $sid = TWILIO_SID, $auth = TWILIO_AUTH, $forcemsg = NULL)
5948
    {
5949
        # We only want to send SMS to people who are clicking on the links.  So if we've sent them one and they've
5950
        # not clicked on it, we stop.  This saves significant amounts of money.
5951
        $phones = $this->dbhr->preQuery("SELECT * FROM users_phones WHERE userid = ? AND valid = 1 AND (lastsent IS NULL OR (lastclicked IS NOT NULL AND DATE(lastclicked) >= DATE(lastsent)));", [
15✔
5952
            $this->id
15✔
5953
        ]);
5954

5955
        foreach ($phones as $phone) {
15✔
5956
            try {
5957
                $last = Utils::presdef('lastsent', $phone, NULL);
2✔
5958
                $last = $last ? strtotime($last) : NULL;
2✔
5959

5960
                # Only send one SMS per day.  This keeps the cost down.
5961
                if ($forcemsg || !$last || (time() - $last > 24 * 60 * 60)) {
2✔
5962
                    $client = new Client($sid, $auth);
2✔
5963

5964
                    $text = $forcemsg ? $forcemsg : "$msg Click $url Don't reply to this text.  No more texts sent today.";
2✔
5965
                    $rsp = $client->messages->create(
2✔
5966
                        $this->formatPhone($phone['number']),
2✔
5967
                        array(
5968
                            'from' => $from,
5969
                            'body' => $text,
5970
                            'statusCallback' => 'https://' . USER_SITE . '/twilio/status.php'
5971
                        )
5972
                    );
5973

5974
                    $this->dbhr->preExec("UPDATE users_phones SET lastsent = NOW(), count = count + 1, lastresponse = ? WHERE id = ?;", [
1✔
5975
                        $rsp->sid,
1✔
5976
                        $phone['id']
1✔
5977
                    ]);
5978
                    error_log("Sent SMS to {$phone['number']} result {$rsp->sid}");
1✔
5979
                } else {
5980
                    error_log("Don't send SMS to {$phone['number']}, too recent");
1✔
5981
                }
5982
            } catch (\Exception $e) {
2✔
5983
                error_log("Send to {$phone['number']} failed with " . $e->getMessage());
2✔
5984
                $this->dbhr->preExec("UPDATE users_phones SET lastsent = NOW(), lastresponse = ? WHERE id = ?;", [
2✔
5985
                    $e->getMessage(),
2✔
5986
                    $phone['id']
2✔
5987
                ]);
5988
            }
5989

5990
        }
5991
    }
5992

5993
    public function addPhone($phone)
5994
    {
5995
        $this->dbhm->preExec("REPLACE INTO users_phones (userid, number, valid) VALUES (?, ?, 1);", [
10✔
5996
            $this->id,
10✔
5997
            $this->formatPhone($phone),
10✔
5998
        ]);
5999

6000
        return($this->dbhm->lastInsertId());
10✔
6001
    }
6002

6003
    public function removePhone()
6004
    {
6005
        $this->dbhm->preExec("DELETE FROM users_phones WHERE userid = ?;", [
2✔
6006
            $this->id
2✔
6007
        ]);
6008
    }
6009

6010
    public function getPhone()
6011
    {
6012
        $ret = NULL;
21✔
6013
        $phones = $this->dbhr->preQuery("SELECT *, DATE(lastclicked) AS lastclicked, DATE(lastsent) AS lastsent FROM users_phones WHERE userid = ?;", [
21✔
6014
            $this->id
21✔
6015
        ]);
6016

6017
        foreach ($phones as $phone) {
21✔
6018
            $ret = [ $phone['number'], Utils::ISODate($phone['lastsent']), Utils::ISODate($phone['lastclicked']) ];
2✔
6019
        }
6020

6021
        return ($ret);
21✔
6022
    }
6023

6024
    public function setAboutMe($text) {
6025
        $this->dbhm->preExec("INSERT INTO users_aboutme (userid, text) VALUES (?, ?);", [
3✔
6026
            $this->id,
3✔
6027
            $text
6028
        ]);
6029

6030
        $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id = {$this->id};");
3✔
6031

6032
        return($this->dbhm->lastInsertId());
3✔
6033
    }
6034

6035
    public function rate($rater, $ratee, $rating, $reason = NULL, $text = NULL) {
6036
        $ret = NULL;
2✔
6037

6038
        if ($rater != $ratee) {
2✔
6039
            # Can't rate yourself.
6040
            $review = $rating == User::RATING_DOWN && $reason && $text;
2✔
6041
            $this->dbhm->preExec("REPLACE INTO ratings (rater, ratee, rating, reason, text, timestamp, reviewrequired) VALUES (?, ?, ?, ?, ?, NOW(), ?);", [
2✔
6042
                $rater,
6043
                $ratee,
6044
                $rating,
6045
                $reason,
6046
                $text,
6047
                $review
6048
            ]);
6049

6050
            $ret = $this->dbhm->lastInsertId();
2✔
6051

6052
            $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id IN ($rater, $ratee);");
2✔
6053
        }
6054

6055
        return($ret);
2✔
6056
    }
6057

6058
    public function getRatings($uids) {
6059
        $mysqltime = date("Y-m-d", strtotime("Midnight 182 days ago"));
119✔
6060
        $ret = [];
119✔
6061
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
119✔
6062
        $myid = $me ? $me->getId() : NULL;
119✔
6063

6064
        # We show visible ratings, ones we have made ourselves, or those from TN.
6065
        $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;";
119✔
6066
        $ratings = $this->dbhr->preQuery($sql, [ $myid, $myid ]);
119✔
6067

6068
        foreach ($uids as $uid) {
119✔
6069
            $ret[$uid] = [
119✔
6070
                User::RATING_UP => 0,
6071
                User::RATING_DOWN => 0,
6072
                User::RATING_MINE => NULL
6073
            ];
6074

6075
            foreach ($ratings as $rate) {
119✔
6076
                if ($rate['ratee'] == $uid) {
1✔
6077
                    $ret[$uid][$rate['rating']] = $rate['count'];
1✔
6078
                }
6079
            }
6080
        }
6081

6082
        $ratings = $this->dbhr->preQuery("SELECT rating, ratee FROM ratings WHERE ratee IN (" . implode(',', $uids) . ") AND rater = ? AND timestamp >= '$mysqltime';", [
119✔
6083
            $myid
6084
        ]);
6085

6086
        foreach ($uids as $uid) {
119✔
6087
            if ($myid != $this->id) {
119✔
6088
                # We can't rate ourselves, so don't bother checking.
6089

6090
                foreach ($ratings as $rating) {
76✔
6091
                    if ($rating['ratee'] == $uid) {
1✔
6092
                        $ret[$uid][User::RATING_MINE] = $rating['rating'];
1✔
6093
                    }
6094
                }
6095
            }
6096
        }
6097

6098
        return($ret);
119✔
6099
    }
6100

6101
    public function getAllRatings($since) {
6102
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6103

6104
        $sql = "SELECT * FROM ratings WHERE timestamp >= ? AND visible = 1;";
1✔
6105
        $ratings = $this->dbhr->preQuery($sql, [
1✔
6106
            $mysqltime
6107
        ]);
6108

6109
        foreach ($ratings as &$rating) {
1✔
6110
            $rating['timestamp'] = Utils::ISODate($rating['timestamp']);
1✔
6111
        }
6112

6113
        return $ratings;
1✔
6114
    }
6115

6116
    public function getVisibleRatings($unreviewedonly, $since = '7 days ago') {
6117
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
3✔
6118
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
6119

6120
        $modships = $me->getModeratorships(NULL, TRUE);
3✔
6121

6122
        $ret = [];
3✔
6123
        $revq = $unreviewedonly ? " AND reviewrequired = 1" : '';
3✔
6124

6125
        if (count($modships)) {
3✔
6126
            $sql = "SELECT ratings.*, m1.groupid,
2✔
6127
       CASE WHEN u1.fullname IS NOT NULL THEN u1.fullname ELSE CONCAT(u1.firstname, ' ', u1.lastname) END AS raterdisplayname,
6128
       CASE WHEN u2.fullname IS NOT NULL THEN u2.fullname ELSE CONCAT(u2.firstname, ' ', u2.lastname) END AS rateedisplayname
6129
    FROM ratings 
6130
    INNER JOIN memberships m1 ON m1.userid = ratings.rater
6131
    INNER JOIN memberships m2 ON m2.userid = ratings.ratee
6132
    INNER JOIN users u1 ON ratings.rater = u1.id
6133
    INNER JOIN users u2 ON ratings.ratee = u2.id
6134
    WHERE ratings.timestamp >= ? AND 
6135
        m1.groupid IN (" . implode(',', $modships) . ") AND
2✔
6136
        m2.groupid IN (" . implode(',', $modships) . ") AND
2✔
6137
        m1.groupid = m2.groupid AND
6138
        ratings.rating IS NOT NULL 
6139
        $revq    
6140
        GROUP BY ratings.rater ORDER BY ratings.timestamp DESC;";
6141

6142
            $ret = $this->dbhr->preQuery($sql, [
2✔
6143
                $mysqltime
6144
            ]);
6145

6146
            foreach ($ret as &$r) {
2✔
6147
                $r['timestamp'] = Utils::ISODate($r['timestamp']);
1✔
6148
            }
6149
        }
6150

6151
        return $ret;
3✔
6152
    }
6153

6154
    public function ratingReviewed($ratingid) {
6155
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6156

6157
        $unreviewed = $me->getVisibleRatings(TRUE);
1✔
6158

6159
        foreach ($unreviewed as $r) {
1✔
6160
            if ($r['id'] == $ratingid) {
1✔
6161
                $this->dbhm->preExec("UPDATE ratings SET reviewrequired = 0 WHERE id = ?;", [
1✔
6162
                    $ratingid
6163
                ]);
6164
            }
6165
        }
6166
    }
6167

6168
    public function getChanges($since) {
6169
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6170

6171
        $users = $this->dbhr->preQuery("SELECT id, lastupdated FROM users WHERE lastupdated >= ?;", [
1✔
6172
            $mysqltime
6173
        ]);
6174

6175
        foreach ($users as &$user) {
1✔
6176
            $user['lastupdated'] = Utils::ISODate($user['lastupdated']);
1✔
6177
        }
6178

6179
        return $users;
1✔
6180
    }
6181

6182
    public function getRated() {
6183
        $rateds = $this->dbhr->preQuery("SELECT * FROM ratings WHERE rater = ?;", [
8✔
6184
            $this->id
8✔
6185
        ]);
6186

6187
        foreach ($rateds as &$rate) {
8✔
6188
            $rate['timestamp'] = Utils::ISODate($rate['timestamp']);
1✔
6189
        }
6190

6191
        return($rateds);
8✔
6192
    }
6193

6194
    public function getActiveSince($since, $createdbefore) {
6195
        $sincetime = date("Y-m-d H:i:s", strtotime($since));
1✔
6196
        $beforetime = date("Y-m-d H:i:s", strtotime($createdbefore));
1✔
6197
        $ids = $this->dbhr->preQuery("SELECT id FROM users WHERE lastaccess >= ? AND added <= ?;", [
1✔
6198
            $sincetime,
6199
            $beforetime
6200
        ]);
6201

6202
        return(count($ids) ? array_filter(array_column($ids, 'id')) : []);
1✔
6203
    }
6204

6205
    public static function encodeId($id) {
6206
        $bin = base_convert($id, 10, 2);
10✔
6207
        $bin = str_replace('0', '-', $bin);
10✔
6208
        $bin = str_replace('1', '~', $bin);
10✔
6209
        return($bin);
10✔
6210
    }
6211

6212
    public static function decodeId($enc) {
6213
        $enc = trim($enc);
1✔
6214
        $enc = str_replace('-', '0', $enc);
1✔
6215
        $enc = str_replace('~', '1', $enc);
1✔
6216
        $id  = base_convert($enc, 2, 10);
1✔
6217
        return($id);
1✔
6218
    }
6219

6220
    public function getCity()
6221
    {
6222
        $city = NULL;
23✔
6223

6224
        # Find the closest town
6225
        list ($lat, $lng, $loc) = $this->getLatLng(FALSE, TRUE);
23✔
6226

6227
        if ($lat || $lng) {
23✔
6228
            $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✔
6229
            #error_log("Get $sql, $lng, $lat");
6230
            $towns = $this->dbhr->preQuery($sql);
1✔
6231

6232
            foreach ($towns as $town) {
1✔
6233
                $city = $town['name'];
1✔
6234
            }
6235
        }
6236

6237
        return([ $city, $lat, $lng ]);
23✔
6238
    }
6239

6240
    public function microVolunteering() {
6241
        // Are we on a group where microvolunteering is enabled.
6242
        $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✔
6243
            $this->id
22✔
6244
        ]);
6245

6246
        return count($groups);
22✔
6247
    }
6248

6249
    public function getJobAds() {
6250
        # We want to show a few job ads from nearby.
6251
        $search = NULL;
28✔
6252
        $ret = '<span class="jobads">';
28✔
6253

6254
        list ($lat, $lng) = $this->getLatLng();
28✔
6255

6256
        if ($lat || $lng) {
28✔
6257
            $j = new Jobs($this->dbhr, $this->dbhm);
3✔
6258
            $jobs = $j->query($lat, $lng, 4);
3✔
6259

6260
            foreach ($jobs as $job) {
3✔
6261
                $loc = Utils::presdef('location', $job, '');
1✔
6262
                $title = "{$job['title']}" . ($loc !== ' ' ? " ($loc)" : '');
1✔
6263

6264
                # Link via our site to avoid spam trap warnings.
6265
                $url = "https://" . USER_SITE . "/job/{$job['id']}";
1✔
6266
                $ret .= '<a href="' . $url . '" target="_blank">' . htmlentities($title) . '</a><br />';
1✔
6267
            }
6268
        }
6269

6270
        $ret .= '</span>';
28✔
6271

6272
        return([
6273
            'location' => $search,
28✔
6274
            'jobs' => $ret
6275
        ]);
6276
    }
6277

6278
    public function updateModMails($uid = NULL) {
6279
        # We maintain a count of recent modmails by scanning logs regularly, and pruning old ones.  This means we can
6280
        # find the value in a well-indexed way without the disk overhead of having a two-column index on logs.
6281
        #
6282
        # Ignore logs where the user is the same as the byuser - for example a user can delete their own posts, and we are
6283
        # only interested in things where a mod has done something to another user.
6284
        $mysqltime = date("Y-m-d H:i:s", strtotime("10 minutes ago"));
1✔
6285
        $uidq = $uid ? " AND user = $uid " : '';
1✔
6286

6287
        $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✔
6288
            $mysqltime
6289
        ]);
6290

6291
        foreach ($logs as $log) {
1✔
6292
            $this->dbhm->preExec("INSERT IGNORE INTO users_modmails (userid, logid, timestamp, groupid) VALUES (?,?,?,?);", [
1✔
6293
                $log['user'],
1✔
6294
                $log['id'],
1✔
6295
                $log['timestamp'],
1✔
6296
                $log['groupid']
1✔
6297
            ]);
6298
        }
6299

6300
        # Prune old ones.
6301
        $mysqltime = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6302
        $uidq2 = $uid ? " AND userid = $uid " : '';
1✔
6303

6304
        $logs = $this->dbhr->preQuery("SELECT id FROM users_modmails WHERE timestamp < ? $uidq2;", [
1✔
6305
            $mysqltime
6306
        ]);
6307

6308
        foreach ($logs as $log) {
1✔
6309
            $this->dbhm->preExec("DELETE FROM users_modmails WHERE id = ?;", [ $log['id'] ], FALSE);
×
6310
        }
6311
    }
6312

6313
    public function getModGroupsByActivity() {
6314
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
6315
        $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✔
6316
        return $this->dbhr->preQuery($sql, [
1✔
6317
            $this->id
1✔
6318
        ]);
6319
    }
6320

6321
    public function related($userlist) {
6322
        $userlist = array_unique($userlist);
2✔
6323

6324
        foreach ($userlist as $user1) {
2✔
6325
            foreach ($userlist as $user2) {
2✔
6326
                if ($user1 && $user2 && $user1 !== $user2) {
2✔
6327
                    # We may be passed user ids which no longer exist.
6328
                    $u1 = User::get($this->dbhr, $this->dbhm, $user1);
2✔
6329
                    $u2 = User::get($this->dbhr, $this->dbhm, $user2);
2✔
6330

6331
                    if ($u1->getId() && $u2->getId() && !$u1->isAdminOrSupport() && !$u2->isAdminOrSupport()) {
2✔
6332
                        $this->dbhm->background("INSERT INTO users_related (user1, user2) VALUES ($user1, $user2) ON DUPLICATE KEY UPDATE timestamp = NOW();");
2✔
6333
                    }
6334
                }
6335
            }
6336
        }
6337
    }
6338

6339
    public function getRelated($userid, $since = "30 days ago") {
6340
        $starttime = date("Y-m-d H:i:s", strtotime($since));
1✔
6341
        $users = $this->dbhr->preQuery("SELECT * FROM users_related WHERE user1 = ? AND timestamp >= '$starttime';", [
1✔
6342
            $userid
6343
        ]);
6344

6345
        return ($users);
1✔
6346
    }
6347

6348
    public function listRelated($groupids, &$ctx, $limit = 10) {
6349
        # The < condition ensures we don't duplicate during a single run.
6350
        $limit = intval($limit);
1✔
6351
        $ret = [];
1✔
6352
        $backstop = 100;
1✔
6353

6354
        do {
6355
            $ctx = $ctx ? $ctx : [ 'id'  => NULL ];
1✔
6356

6357
            if ($groupids && count($groupids)) {
1✔
6358
                $ctxq = ($ctx && intval($ctx['id'])) ? (" WHERE id < " . intval($ctx['id'])) : '';
1✔
6359
                $groupq = "(" . implode(',', $groupids) . ")";
1✔
6360
                $sql = "SELECT DISTINCT id, user1, user2 FROM (
1✔
6361
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6362
INNER JOIN memberships ON users_related.user1 = memberships.userid 
6363
INNER JOIN users u1 ON users_related.user1 = u1.id AND u1.deleted IS NULL AND u1.systemrole = 'User'
6364
WHERE 
6365
user1 < user2 AND
6366
notified = 0 AND
6367
memberships.groupid IN $groupq UNION
6368
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6369
INNER JOIN memberships ON users_related.user2 = memberships.userid 
6370
INNER JOIN users u2 ON users_related.user2 = u2.id AND u2.deleted IS NULL AND u2.systemrole = 'User'
6371
WHERE 
6372
user1 < user2 AND
6373
notified = 0 AND
6374
memberships.groupid IN $groupq 
6375
) t $ctxq ORDER BY id DESC LIMIT $limit;";
6376
                $members = $this->dbhr->preQuery($sql);
1✔
6377
            } else {
6378
                $ctxq = ($ctx && intval($ctx['id'])) ? (" AND users_related.id < " . intval($ctx['id'])) : '';
1✔
6379
                $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✔
6380
                $members = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
1✔
6381
            }
6382

6383
            $uids1 = array_column($members, 'user1');
1✔
6384
            $uids2 = array_column($members, 'user2');
1✔
6385

6386
            $related = [];
1✔
6387
            foreach ($members as $member) {
1✔
6388
                $related[$member['user1']] = $member['user2'];
1✔
6389
                $ctx['id'] = $member['id'];
1✔
6390
            }
6391

6392
            $users = $this->getPublicsById(array_merge($uids1, $uids2));
1✔
6393

6394
            foreach ($users as &$user1) {
1✔
6395
                if (Utils::pres($user1['id'], $related)) {
1✔
6396
                    $thisone = $user1;
1✔
6397

6398
                    foreach ($users as $user2) {
1✔
6399
                        if ($user2['id'] == $related[$user1['id']]) {
1✔
6400
                            $user2['userid'] = $user2['id'];
1✔
6401
                            $thisone['relatedto'] = $user2;
1✔
6402
                            break;
1✔
6403
                        }
6404
                    }
6405

6406
                    $logins = $this->getLogins(FALSE, $thisone['id'], TRUE);
1✔
6407
                    $rellogins = $this->getLogins(FALSE, $thisone['relatedto']['id'], TRUE);
1✔
6408

6409
                    if ($thisone['deleted'] ||
1✔
6410
                        $thisone['relatedto']['deleted'] ||
1✔
6411
                        $thisone['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6412
                        $thisone['relatedto']['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6413
                        !count($logins) ||
1✔
6414
                        !count($rellogins)) {
1✔
6415
                        # No sense in telling people about these.
6416
                        #
6417
                        # If there are n valid login types for one of the users - no way they can log in again so no point notifying.
6418
                        $this->dbhm->preExec("UPDATE users_related SET notified = 1 WHERE (user1 = ? AND user2 = ?) OR (user1 = ? AND user2 = ?);", [
1✔
6419
                            $thisone['id'],
1✔
6420
                            $thisone['relatedto']['id'],
1✔
6421
                            $thisone['relatedto']['id'],
1✔
6422
                            $thisone['id']
1✔
6423
                        ]);
6424
                    } else {
6425
                        $thisone['userid'] = $thisone['id'];
1✔
6426
                        $thisone['logins'] = $logins;
1✔
6427
                        $thisone['relatedto']['logins'] = $rellogins;
1✔
6428

6429
                        $ret[] = $thisone;
1✔
6430
                    }
6431
                }
6432
            }
6433

6434
            $backstop--;
1✔
6435
        } while ($backstop > 0 && count($ret) < $limit && count($members));
1✔
6436

6437
        return $ret;
1✔
6438
    }
6439

6440
    public function getExpectedReplies($uids, $since = ChatRoom::ACTIVELIM, $grace = 30) {
6441
        # We count replies where the user has been active since the reply was requested, which means they've had
6442
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6443
        #
6444
        # $since here has to match the value in ChatRoom::
6445
        $starttime = date("Y-m-d H:i:s", strtotime($since));
119✔
6446
        $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) >= ?", [
119✔
6447
            $grace
6448
        ]);
6449

6450
        return($replies);
119✔
6451
    }
6452

6453
    public function listExpectedReplies($uid, $since = ChatRoom::ACTIVELIM, $grace = 30) {
6454
        # We count replies where the user has been active since the reply was requested, which means they've had
6455
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6456
        #
6457
        # $since here has to match the value in ChatRoom::
6458
        $starttime = date("Y-m-d H:i:s", strtotime($since));
21✔
6459
        $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✔
6460
            $uid,
6461
            $grace
6462
        ]);
6463

6464
        $ret = [];
21✔
6465

6466
        if (count($replies)) {
21✔
6467
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6468
            $myid = $me ? $me->getId() : NULL;
1✔
6469

6470
            $r = new ChatRoom($this->dbhr, $this->dbhm);
1✔
6471
            $rooms = $r->fetchRooms(array_column($replies, 'chatid'), $myid, TRUE);
1✔
6472

6473
            foreach ($rooms as $room) {
1✔
6474
                $ret[] = [
1✔
6475
                    'id' => $room['id'],
1✔
6476
                    'name' => $room['name']
1✔
6477
                ];
6478
            }
6479
        }
6480

6481
        return $ret;
21✔
6482
    }
6483
    
6484
    public function getWorkCounts($groups = NULL) {
6485
        # Tell them what mod work there is.  Similar code in Notifications.
6486
        $ret = [];
26✔
6487
        $total = 0;
26✔
6488

6489
        $national = $this->hasPermission(User::PERM_NATIONAL_VOLUNTEERS);
26✔
6490

6491
        if ($national) {
26✔
6492
            $v = new Volunteering($this->dbhr, $this->dbhm);
1✔
6493
            $ret['pendingvolunteering'] = $v->systemWideCount();
1✔
6494
        }
6495

6496
        $s = new Spam($this->dbhr, $this->dbhm);
26✔
6497
        $spamcounts = $s->collectionCounts();
26✔
6498
        $ret['spammerpendingadd'] = $spamcounts[Spam::TYPE_PENDING_ADD];
26✔
6499
        $ret['spammerpendingremove'] = $spamcounts[Spam::TYPE_PENDING_REMOVE];
26✔
6500

6501
        # Show social actions from last 4 days.
6502
        $ctx = NULL;
26✔
6503
        $f = new GroupFacebook($this->dbhr, $this->dbhm);
26✔
6504
        $ret['socialactions'] = count($f->listSocialActions($ctx,$this));
26✔
6505

6506
        $g = new Group($this->dbhr, $this->dbhm);
26✔
6507
        $ret['popularposts'] = count($g->getPopularMessages());
26✔
6508

6509
        if ($this->hasPermission(User::PERM_GIFTAID)) {
26✔
6510
            $d = new Donations($this->dbhr, $this->dbhm);
1✔
6511
            $ret['giftaid'] = $d->countGiftAidReview();
1✔
6512
        }
6513

6514
        if (!$groups) {
26✔
6515
            $groups = $this->getMemberships(FALSE, NULL, TRUE, TRUE, $this->id, FALSE);
11✔
6516
        }
6517

6518
        foreach ($groups as &$group) {
26✔
6519
            if (Utils::pres('work', $group)) {
19✔
6520
                foreach ($group['work'] as $key => $work) {
17✔
6521
                    if (Utils::pres($key, $ret)) {
17✔
6522
                        $ret[$key] += $work;
2✔
6523
                    } else {
6524
                        $ret[$key] = $work;
17✔
6525
                    }
6526
                }
6527
            }
6528
        }
6529

6530
        $s = new Story($this->dbhr, $this->dbhm);
26✔
6531
        $ret['stories'] = $s->getReviewCount(FALSE, $this, $groups);
26✔
6532
        $ret['newsletterstories'] = $this->hasPermission(User::PERM_NEWSLETTER) ? $s->getReviewCount(TRUE) : 0;
26✔
6533

6534
        // All the types of work which are worth nagging about.
6535
        $worktypes = [
26✔
6536
            'pendingvolunteering',
6537
            'socialactions',
6538
            'popularposts',
6539
            'chatreview',
6540
            'relatedmembers',
6541
            'stories',
6542
            'newsletterstories',
6543
            'pending',
6544
            'spam',
6545
            'pendingmembers',
6546
            'pendingevents',
6547
            'spammembers',
6548
            'editreview',
6549
            'pendingadmins'
6550
        ];
6551

6552
        if ($this->isAdminOrSupport()) {
26✔
6553
            $worktypes[] = 'spammerpendingadd';
1✔
6554
            $worktypes[] = 'spammerpendingremove';
1✔
6555
        }
6556

6557
        foreach ($worktypes as $key) {
26✔
6558
            $total += Utils::presdef($key, $ret, 0);
26✔
6559
        }
6560

6561
        $ret['total'] = $total;
26✔
6562

6563
        return $ret;
26✔
6564
    }
6565

6566
    public function ratingVisibility($since = "1 hour ago") {
6567
        $mysqltime = date("Y-m-d", strtotime($since));
1✔
6568

6569
        $ratings = $this->dbhr->preQuery("SELECT * FROM ratings WHERE timestamp >= ?;", [
1✔
6570
            $mysqltime
6571
        ]);
6572

6573
        foreach ($ratings as $rating) {
1✔
6574
            # A rating is visible to others if there is a chat between the two members, and
6575
            # - the ratee replied to a post, or
6576
            # - there is at least one message from each of them.
6577
            # This means that has been an exchange substantial enough for the rating not to just be frivolous.  It
6578
            # deliberately excludes interactions on ChitChat, where we have seen some people go a bit overboard on
6579
            # rating people.
6580
            $visible = FALSE;
1✔
6581
            #error_log("Check {$rating['rater']} rating of {$rating['ratee']}");
6582

6583
            $chats = $this->dbhr->preQuery("SELECT id FROM chat_rooms WHERE (user1 = ? AND user2 = ?) OR (user2 = ? AND user1 = ?)", [
1✔
6584
                $rating['rater'],
1✔
6585
                $rating['ratee'],
1✔
6586
                $rating['rater'],
1✔
6587
                $rating['ratee'],
1✔
6588
            ]);
6589

6590
            foreach ($chats as $chat) {
1✔
6591
                $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✔
6592
                    $chat['id']
1✔
6593
                ]);
6594

6595
                if ($distincts[0]['count'] >= 2) {
1✔
6596
                    #error_log("At least one real message from each of them in {$chat['id']}");
6597
                    $visible = TRUE;
1✔
6598
                } else {
6599
                    $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✔
6600
                        $chat['id'],
1✔
6601
                        $rating['ratee']
1✔
6602
                    ]);
6603

6604
                    if ($replies[0]['count']) {
1✔
6605
                        #error_log("Significant reply from {$rating['ratee']} in {$chat['id']}");
6606
                        $visible = TRUE;
1✔
6607
                    }
6608
                }
6609
            }
6610

6611
            #error_log("Use {$rating['rating']} from {$rating['rater']} ? " . ($visible ? 'yes': 'no'));
6612
            $oldvisible = intval($rating['visible']) ? TRUE : FALSE;
1✔
6613

6614
            if ($visible != $oldvisible) {
1✔
6615
                $this->dbhm->preExec("UPDATE ratings SET visible = ?, timestamp = NOW() WHERE id = ?;", [
1✔
6616
                    $visible,
6617
                    $rating['id']
1✔
6618
                ]);
6619
            }
6620
        }
6621
    }
6622

6623
    public function unban($groupid) {
6624
        $this->dbhm->preExec("DELETE FROM users_banned WHERE userid = ? AND groupid = ?;", [
2✔
6625
            $this->id,
2✔
6626
            $groupid
6627
        ]);
6628
    }
6629

6630
    public function hasFacebookLogin() {
6631
        $logins = $this->getLogins();
4✔
6632
        $ret = FALSE;
4✔
6633

6634
        foreach ($logins as $login) {
4✔
6635
            if ($login['type'] == User::LOGIN_FACEBOOK) {
4✔
6636
                $ret = TRUE;
1✔
6637
            }
6638
        }
6639

6640
        return $ret;
4✔
6641
    }
6642

6643
    public function memberReview($groupid, $request, $reason) {
6644
        $mysqltime = date('Y-m-d H:i');
8✔
6645

6646
        if ($request) {
8✔
6647
            # Requesting review.  Leave reviewedat unchanged, so that we can use it to avoid asking too
6648
            # frequently.
6649
            $this->setMembershipAtt($groupid, 'reviewreason', $reason);
6✔
6650
            $this->setMembershipAtt($groupid, 'reviewrequestedat', $mysqltime);
6✔
6651
        } else {
6652
            # We have reviewed.  Note that they might have been removed, in which case the set will do nothing.
6653
            $this->setMembershipAtt($groupid, 'reviewrequestedat', NULL);
3✔
6654
            $this->setMembershipAtt($groupid, 'reviewedat', $mysqltime);
3✔
6655
        }
6656
    }
6657

6658
    private function checkSupporterSettings($settings) {
6659
        $ret = TRUE;
75✔
6660

6661
        if ($settings) {
75✔
6662
            $s = json_decode($settings, TRUE);
13✔
6663

6664
            if ($s && array_key_exists('hidesupporter', $s)) {
13✔
6665
                $ret = !$s['hidesupporter'];
1✔
6666
            }
6667
        }
6668

6669
        return $ret;
75✔
6670
    }
6671

6672
    public function getSupporters(&$rets, $users) {
6673
        $idsleft = [];
260✔
6674

6675
        foreach ($rets as $userid => $ret) {
260✔
6676
            if (Utils::pres($userid, $users)) {
236✔
6677
                if (array_key_exists('supporter', $users[$userid])) {
10✔
6678
                    $rets[$userid]['supporter'] = $users[$userid]['supporter'];
3✔
6679
                    $rets[$userid]['donor'] = Utils::presdef('donor', $users[$userid], FALSE);
10✔
6680
                }
6681
            } else {
6682
                $idsleft[] = $userid;
232✔
6683
            }
6684
        }
6685

6686
        if (count($idsleft)) {
260✔
6687
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
232✔
6688
            $myid = $me ? $me->getId() : null;
232✔
6689

6690
            # A supporter is a mod, someone who has donated recently, or done microvolunteering recently.
6691
            if (count($idsleft)) {
232✔
6692
                $start = date('Y-m-d', strtotime("360 days ago"));
232✔
6693
                $info = $this->dbhr->preQuery(
232✔
6694
                    "SELECT DISTINCT users.id AS userid, settings, systemrole FROM users 
6695
    LEFT JOIN microactions ON users.id = microactions.userid
6696
    LEFT JOIN users_donations ON users_donations.userid = users.id 
6697
    WHERE users.id IN (" . implode(
232✔
6698
                        ',',
6699
                        $idsleft
6700
                    ) . ") AND 
6701
                    (systemrole IN (?, ?, ?) OR microactions.timestamp >= ? OR users_donations.timestamp >= ?);",
6702
                    [
6703
                        User::SYSTEMROLE_ADMIN,
6704
                        User::SYSTEMROLE_SUPPORT,
6705
                        User::SYSTEMROLE_MODERATOR,
6706
                        $start,
6707
                        $start
6708
                    ]
6709
                );
6710

6711
                $found = [];
232✔
6712

6713
                foreach ($info as $i) {
232✔
6714
                    $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
75✔
6715
                    $found[] = $i['userid'];
75✔
6716
                }
6717

6718
                $left = array_diff($idsleft, $found);
232✔
6719

6720
                # If we are one of the users, then we want to return whether we are a donor.
6721
                if (in_array($myid, $idsleft)) {
232✔
6722
                    $left[] = $myid;
135✔
6723
                    $left = array_filter(array_unique($left));
135✔
6724
                }
6725

6726
                if (count($left)) {
232✔
6727
                    $info = $this->dbhr->preQuery(
230✔
6728
                        "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(
230✔
6729
                            ',',
6730
                            $left
6731
                        ) . ") GROUP BY TransactionType;",
6732
                        [
6733
                            $start
6734
                        ]
6735
                    );
6736

6737
                    foreach ($info as $i) {
230✔
6738
                        $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
3✔
6739

6740
                        if ($i['userid'] == $myid) {
3✔
6741
                            # Only return this info for ourselves, otherwise it's a privacy leak.
6742
                            $rets[$i['userid']]['donor'] = TRUE;
3✔
6743

6744
                            if ($i['TransactionType'] == 'subscr_payment' || $i['TransactionType'] == 'recurring_payment') {
3✔
6745
                                $rets[$i['userid']]['donorrecurring'] = TRUE;
1✔
6746
                            }
6747
                        }
6748
                    }
6749
                }
6750
            }
6751
        }
6752
    }
6753

6754
    public function obfuscateEmail($email) {
6755
        $p = strpos($email, '@');
2✔
6756
        $q = strpos($email, 'privaterelay.appleid.com');
2✔
6757

6758
        if ($q) {
2✔
6759
            $email = 'Your Apple ID';
1✔
6760
        } else {
6761
            # For very short emails, we just show the first character.
6762
            if ($p <= 3) {
2✔
6763
                $email = substr($email, 0, 1) . "***" . substr($email, $p);
1✔
6764
            } else if ($p < 10) {
2✔
6765
                $email = substr($email, 0, 1) . "***" . substr($email, $p - 1);
2✔
6766
            } else {
6767
                $email = substr($email, 0, 3) . "***" . substr($email, $p - 3);
1✔
6768
            }
6769
        }
6770

6771
        return $email;
2✔
6772
    }
6773

6774
    public function setSimpleMail($simplemail) {
6775
        $s = $this->getPrivate('settings');
1✔
6776

6777
        if ($s) {
1✔
6778
            $settings = json_decode($s, TRUE);
×
6779
        } else {
6780
            $settings = [];
1✔
6781
        }
6782

6783
        $this->dbhm->beginTransaction();
1✔
6784

6785
        switch ($simplemail) {
6786
            case User::SIMPLE_MAIL_NONE: {
6787
                # No digests, no events/volunteering.
6788
                # No relevant or newsletters.
6789
                # No email notifications.
6790
                # No enagement.
6791
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 0, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
1✔
6792
                    $this->id
1✔
6793
                ]);
6794

6795
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
1✔
6796
                    $this->id
1✔
6797
                ]);
6798

6799
                $settings['notifications']['email'] = false;
1✔
6800
                $settings['notifications']['emailmine'] = false;
1✔
6801
                $settings['notificationmails']= false;
1✔
6802
                $settings['engagement'] = false;
1✔
6803
                break;
1✔
6804
            }
6805
            case User::SIMPLE_MAIL_BASIC: {
6806
                # Daily digests, no events/volunteering.
6807
                # No relevant or newsletters.
6808
                # Chat email notifications.
6809
                # No enagement.
6810
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 24, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
1✔
6811
                    $this->id
1✔
6812
                ]);
6813

6814
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
1✔
6815
                    $this->id
1✔
6816
                ]);
6817

6818
                $settings['notifications']['email'] = true;
1✔
6819
                $settings['notifications']['emailmine'] = false;
1✔
6820
                $settings['notificationmails']= false;
1✔
6821
                $settings['engagement']= false;
1✔
6822
                break;
1✔
6823
            }
6824
            case User::SIMPLE_MAIL_FULL: {
6825
                # Immediate mails, events/volunteering.
6826
                # Relevant and newsletters.
6827
                # Email notifications.
6828
                # Enagement.
6829
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = -1, eventsallowed = 1, volunteeringallowed = 1 WHERE userid = ?;", [
1✔
6830
                    $this->id
1✔
6831
                ]);
6832

6833
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 1,  newslettersallowed = 1 WHERE id = ?;", [
1✔
6834
                    $this->id
1✔
6835
                ]);
6836

6837
                $settings['notifications']['email'] = true;
1✔
6838
                $settings['notifications']['emailmine'] = false;
1✔
6839
                $settings['notificationmails']= true;
1✔
6840
                $settings['engagement']= true;
1✔
6841
                break;
1✔
6842
            }
6843
        }
6844

6845
        $settings['simplemail'] = $simplemail;
1✔
6846

6847
        # Holiday no longer exposed so turn off.
6848
        $this->dbhm->preExec("UPDATE users SET onholidaytill = NULL, settings = ? WHERE id = ?;", [
1✔
6849
            json_encode($settings),
1✔
6850
            $this->id
1✔
6851
        ]);
6852

6853
        $this->dbhm->commit();
1✔
6854
    }
6855
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc