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

Freegle / iznik-server / 997e0f4f-3cfe-42a9-a6a2-be212777be4a

25 May 2024 05:16PM UTC coverage: 94.869% (-0.01%) from 94.88%
997e0f4f-3cfe-42a9-a6a2-be212777be4a

push

circleci

edwh
Test fixes.

25425 of 26800 relevant lines covered (94.87%)

31.61 hits per line

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

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

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

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

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

19
    const OPEN_AGE = 90;
20

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

248
                    return (TRUE);
269✔
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 = ?;";
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,
2✔
275
                    'subtype' => Log::SUBTYPE_LOGIN,
2✔
276
                    'byuser' => $this->id,
2✔
277
                    'text' => 'Using link'
2✔
278
                ]);
2✔
279

280
                $this->dbhm->preExec("UPDATE users_logins SET lastaccess = NOW() WHERE userid = ? AND type = ?;", [
2✔
281
                    $this->id,
2✔
282
                    User::LOGIN_LINK
2✔
283
                ]);
2✔
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);
34✔
295
    }
296

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

301
        $name = NULL;
561✔
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)) {
561✔
306
            $name = $atts['fullname'];
495✔
307
        } else if (Utils::pres('firstname', $atts) || Utils::pres('lastname', $atts)) {
85✔
308
            $first = Utils::pres('firstname', $atts);
81✔
309
            $last = Utils::pres('lastname', $atts);
81✔
310

311
            $name = $first && $last ? "$first $last" : ($first ? $first : $last);
81✔
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;
561✔
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;
561✔
319

320
        if ($default &&
561✔
321
            $this->id &&
561✔
322
            (strlen(trim($name)) === 0 ||
561✔
323
                $name == 'A freegler' ||
561✔
324
                $resurrect ||
561✔
325
                (strlen($name) == 32 && preg_match('/[A-Za-z].*[0-9]|[0-9].*[A-Za-z]/', $name)) ||
561✔
326
                strpos($name, 'FBUser') !== FALSE)
561✔
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;
561✔
342

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

346
        # TN group numbers in names confuse everyone.
347
        $name = User::removeTNGroup($name);
561✔
348

349
        return ($name);
561✔
350
    }
351

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

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

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

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

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

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

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

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

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

411
        $pw = '';
5✔
412

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

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

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

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

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

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

445
        return ($this->emails);
359✔
446
    }
447

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

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

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

461
        return ($ret);
169✔
462
    }
463

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

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

481
        return ($ret);
347✔
482
    }
483

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

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

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

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

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

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

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

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

530
        foreach ($ids as $id) {
22✔
531
            return ($id);
22✔
532
        }
533

534
        return (NULL);
1✔
535
    }
536

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

543
        $ret = NULL;
1✔
544

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

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

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

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

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

563
        return $ret;
4✔
564
    }
565

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

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

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

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

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

603
        return [ NULL, FALSE ];
167✔
604
    }
605

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

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

618
        return (NULL);
1✔
619
    }
620

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

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

631
        return (NULL);
4✔
632
    }
633

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

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

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

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

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

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

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

670
        return ($email);
453✔
671
    }
672

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

677
        # Invalidate cache.
678
        $this->emails = NULL;
453✔
679

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

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

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

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

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

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

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

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

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

753
        return ($rc);
453✔
754
    }
755

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

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

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

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

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

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

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

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

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

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

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

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

836
        foreach ($banneds as $banned) {
448✔
837
            return TRUE;
5✔
838
        }
839

840
        return FALSE;
448✔
841
    }
842

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

849
        Session::clearSessionCache();
448✔
850

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

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

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

867
        $emailfrequency = 24;
448✔
868
        $eventsallowed = 1;
448✔
869
        $volunteeringallowed = 1;
448✔
870

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

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

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

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

909
        $membershipid = $this->dbhm->lastInsertId();
448✔
910

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

914
        # Record the operation for abuse detection.  Setting processingrequired will cause background code to do
915
        # work.
916
        $this->dbhm->preExec("INSERT INTO memberships_history (userid, groupid, collection, processingrequired) VALUES (?,?,?,?);", [
448✔
917
            $this->id,
448✔
918
            $groupid,
448✔
919
            $collection,
448✔
920
            $added
448✔
921
        ]);
448✔
922

923
        $historyid = $this->dbhm->lastInsertId();
448✔
924

925
        # We might need to update the systemrole.
926
        #
927
        # Not the end of the world if this fails.
928
        $this->updateSystemRole($role);
448✔
929

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

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

943
            $this->sendIt($mailer, $message);
944
        }
945
        // @codeCoverageIgnoreEnd
946

947
        if ($added) {
448✔
948
            $l = new Log($this->dbhr, $this->dbhm);
448✔
949
            $text = NULL;
448✔
950

951
            if ($manual !== NULL) {
448✔
952
                $text = $manual ? 'Manual' : 'Auto';
4✔
953
            }
954

955
            $l->log([
448✔
956
                'type' => Log::TYPE_GROUP,
448✔
957
                'subtype' => Log::SUBTYPE_JOINED,
448✔
958
                'user' => $this->id,
448✔
959
                'byuser' => $me ? $me->getId() : NULL,
448✔
960
                'groupid' => $groupid,
448✔
961
                'text' => $text
448✔
962
            ]);
448✔
963
        }
964

965
        return ($rc);
448✔
966
    }
967

968
    public function cacheMemberships($id = NULL)
969
    {
970
        $id = $id ? $id : $this->id;
340✔
971

972
        # We get all the memberships in a single call, because some members are on many groups and this can
973
        # save hundreds of calls to the DB.
974
        if (!$this->memberships) {
340✔
975
            $this->memberships = [];
340✔
976

977
            $membs = $this->dbhr->preQuery("SELECT memberships.*, groups.type FROM memberships INNER JOIN `groups` ON groups.id = memberships.groupid WHERE userid = ?;", [ $id ]);
340✔
978
            foreach ($membs as $memb) {
340✔
979
                $this->memberships[$memb['groupid']] = $memb;
315✔
980
            }
981
        }
982

983
        return ($this->memberships);
340✔
984
    }
985

986
    public function clearMembershipCache()
987
    {
988
        $this->memberships = NULL;
284✔
989
    }
990

991
    public function getMembershipAtt($groupid, $att)
992
    {
993
        $this->cacheMemberships();
179✔
994
        $val = NULL;
179✔
995
        if (Utils::pres($groupid, $this->memberships)) {
179✔
996
            $val = Utils::presdef($att, $this->memberships[$groupid], NULL);
172✔
997
        }
998

999
        return ($val);
179✔
1000
    }
1001

1002
    public function setMembershipAtt($groupid, $att, $val)
1003
    {
1004
        $this->clearMembershipCache();
230✔
1005
        Session::clearSessionCache();
230✔
1006
        $sql = "UPDATE memberships SET $att = ? WHERE groupid = ? AND userid = ?;";
230✔
1007
        $rc = $this->dbhm->preExec($sql, [
230✔
1008
            $val,
230✔
1009
            $groupid,
230✔
1010
            $this->id
230✔
1011
        ]);
230✔
1012

1013
        return ($rc);
230✔
1014
    }
1015

1016
    public function removeMembership($groupid, $ban = FALSE, $spam = FALSE, $byemail = NULL)
1017
    {
1018
        $this->clearMembershipCache();
36✔
1019
        $g = Group::get($this->dbhr, $this->dbhm, $groupid);
36✔
1020
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
36✔
1021
        $meid = $me ? $me->getId() : NULL;
36✔
1022

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

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

1039
            $this->sendIt($mailer, $message);
1040
        }
1041
        // @codeCoverageIgnoreEnd
1042

1043
        if ($ban) {
36✔
1044
            $sql = "INSERT IGNORE INTO users_banned (userid, groupid, byuser) VALUES (?,?,?);";
13✔
1045
            $this->dbhm->preExec($sql, [
13✔
1046
                $this->id,
13✔
1047
                $groupid,
13✔
1048
                $meid
13✔
1049
            ]);
13✔
1050

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

1063
            foreach ($msgs as $msg) {
13✔
1064
                $m = new Message($this->dbhr, $this->dbhm, $msg['msgid']);
1✔
1065
                if (!$m->hasOutcome()) {
1✔
1066
                    $m->mark(Message::OUTCOME_WITHDRAWN, "Marked as withdrawn by ban", NULL, NULL);
1✔
1067
                }
1068
            }
1069
        }
1070

1071
        # Now remove the membership.
1072
        $rc = $this->dbhm->preExec("DELETE FROM memberships WHERE userid = ? AND groupid = ?;",
36✔
1073
            [
36✔
1074
                $this->id,
36✔
1075
                $groupid
36✔
1076
            ]);
36✔
1077

1078
        if ($this->dbhm->rowsAffected() || $ban) {
36✔
1079
            $l = new Log($this->dbhr, $this->dbhm);
36✔
1080
            $l->log([
36✔
1081
                'type' => Log::TYPE_GROUP,
36✔
1082
                'subtype' => Log::SUBTYPE_LEFT,
36✔
1083
                'user' => $this->id,
36✔
1084
                'byuser' => $meid,
36✔
1085
                'groupid' => $groupid,
36✔
1086
                'text' => $spam ? "Autoremoved spammer" : ($ban ? "via ban" : NULL)
36✔
1087
            ]);
36✔
1088
        }
1089

1090
        return ($rc);
36✔
1091
    }
1092

1093
    public function getMembershipGroupIds($modonly = FALSE, $grouptype = NULL, $id = NULL) {
1094
        $id = $id ? $id : $this->id;
5✔
1095

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

1107
    public function getMemberships($modonly = FALSE, $grouptype = NULL, $getwork = FALSE, $pernickety = FALSE, $id = NULL)
1108
    {
1109
        $id = $id ? $id : $this->id;
157✔
1110

1111
        $ret = [];
157✔
1112
        $modq = $modonly ? " AND role IN ('Owner', 'Moderator') " : "";
157✔
1113
        $typeq = $grouptype ? (" AND `type` = " . $this->dbhr->quote($grouptype)) : '';
157✔
1114
        $publishq = Session::modtools() ? "" : "AND groups.publish = 1";
157✔
1115
        $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;";
157✔
1116
        $groups = $this->dbhr->preQuery($sql, [$id]);
157✔
1117
        #error_log("getMemberships $sql {$id} " . var_export($groups, TRUE));
1118

1119
        $c = new ModConfig($this->dbhr, $this->dbhm);
157✔
1120

1121
        # Get all the groups efficiently.
1122
        $groupids = array_filter(array_column($groups, 'groupid'));
157✔
1123
        $gc = new GroupCollection($this->dbhr, $this->dbhm, $groupids);
157✔
1124
        $groupobjs = $gc->get();
157✔
1125
        $getworkids = [];
157✔
1126
        $groupsettings = [];
157✔
1127

1128
        for ($i = 0; $i < count($groupids); $i++) {
157✔
1129
            $group = $groups[$i];
122✔
1130
            $g = $groupobjs[$i];
122✔
1131
            $one = $g->getPublic();
122✔
1132

1133
            $one['role'] = $group['role'];
122✔
1134
            $one['collection'] = $group['collection'];
122✔
1135
            $amod = ($one['role'] == User::ROLE_MODERATOR || $one['role'] == User::ROLE_OWNER);
122✔
1136
            $one['configid'] = Utils::presdef('configid', $group, NULL);
122✔
1137

1138
            if ($amod && !Utils::pres('configid', $one)) {
122✔
1139
                # Get a config using defaults.
1140
                $one['configid'] = $c->getForGroup($id, $group['groupid']);
29✔
1141
            }
1142

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

1145
            # If we don't have our own email on this group we won't be sending mails.  This is what affects what
1146
            # gets shown on the Settings page for the user, and we only want to check this here
1147
            # for performance reasons.
1148
            $one['mysettings']['emailfrequency'] = ($group['type'] ==  Group::GROUP_FREEGLE &&
122✔
1149
                ($pernickety || $this->sendOurMails($g, FALSE, FALSE))) ?
122✔
1150
                (array_key_exists('emailfrequency', $one['mysettings']) ? $one['mysettings']['emailfrequency'] :  24)
83✔
1151
                : 0;
40✔
1152

1153
            $groupsettings[$group['groupid']] = $one['mysettings'];
122✔
1154

1155
            if ($getwork) {
122✔
1156
                # We need to find out how much work there is whether or not we are an active mod because we need
1157
                # to be able to see that it is there.  The UI shows it less obviously.
1158
                if ($amod) {
20✔
1159
                    $getworkids[] = $group['groupid'];
18✔
1160
                }
1161
            }
1162

1163
            $ret[] = $one;
122✔
1164
        }
1165

1166
        if ($getwork) {
157✔
1167
            # Get all the work.  This is across all groups for performance.
1168
            $g = new Group($this->dbhr, $this->dbhm);
27✔
1169
            $work = $g->getWorkCounts($groupsettings, $groupids);
27✔
1170

1171
            foreach ($getworkids as $groupid) {
27✔
1172
                foreach ($ret as &$group) {
18✔
1173
                    if ($group['id'] == $groupid) {
18✔
1174
                        $group['work'] = $work[$groupid];
18✔
1175
                    }
1176
                }
1177
            }
1178
        }
1179

1180
        return ($ret);
157✔
1181
    }
1182

1183
    public function getConfigs($all)
1184
    {
1185
        $ret = [];
23✔
1186
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
23✔
1187

1188
        if ($all) {
23✔
1189
            # We can see configs which
1190
            # - we created
1191
            # - are used by mods on groups on which we are a mod
1192
            # - defaults
1193
            $modships = $me ? $this->getModeratorships() : [];
22✔
1194
            $modships = count($modships) > 0 ? $modships : [0];
22✔
1195

1196
            $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✔
1197
            $ids = $this->dbhr->preQuery($sql);
22✔
1198
        } else {
1199
            # We only want to see the configs that we are actively using.  This reduces the size of what we return
1200
            # for people on many groups.
1201
            $sql = "SELECT DISTINCT configid AS id FROM memberships WHERE userid = ? AND configid IS NOT NULL;";
3✔
1202
            $ids = $this->dbhr->preQuery($sql, [ $me->getId() ]);
3✔
1203
        }
1204

1205
        $configids = array_filter(array_column($ids, 'id'));
23✔
1206

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

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

1220
            foreach ($configs as $config) {
4✔
1221
                $c = new ModConfig($this->dbhr, $this->dbhm, $config['id'], $config, $stdmsgs, $bulkops);
4✔
1222
                $thisone = $c->getPublic(FALSE);
4✔
1223

1224
                if (Utils::pres('createdby', $config)) {
4✔
1225
                    $ctx = NULL;
3✔
1226
                    $thisone['createdby'] = [
3✔
1227
                        'id' => $config['createdby'],
3✔
1228
                        'displayname' => $config['createdname']
3✔
1229
                    ];
3✔
1230
                }
1231

1232
                $ret[] = $thisone;
4✔
1233
            }
1234
        }
1235

1236
        # Return in alphabetical order.
1237
        $rc = usort($ret, function ($a, $b) {
23✔
1238
            return strcasecmp($a['name'], $b['name']);
1✔
1239
        });
23✔
1240

1241
        return ($ret);
23✔
1242
    }
1243

1244
    public function getModeratorships($id = NULL, $activeonly = FALSE)
1245
    {
1246
        $this->cacheMemberships($id);
148✔
1247
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
148✔
1248

1249
        $ret = [];
148✔
1250
        foreach ($this->memberships AS $membership) {
148✔
1251
            if ($membership['role'] == 'Owner' || $membership['role'] == 'Moderator') {
125✔
1252
                if (!$activeonly || $me->activeModForGroup($membership['groupid'])) {
90✔
1253
                    $ret[] = $membership['groupid'];
90✔
1254
                }
1255
            }
1256
        }
1257

1258
        return ($ret);
148✔
1259
    }
1260

1261
    public function isModOrOwner($groupid)
1262
    {
1263
        # Very frequently used.  Cache in session.
1264
        #error_log("modOrOwner " . var_export($_SESSION['modorowner'], TRUE));
1265
        if ((session_status() !== PHP_SESSION_NONE || getenv('UT')) &&
172✔
1266
            array_key_exists('modorowner', $_SESSION) &&
172✔
1267
            array_key_exists($this->id, $_SESSION['modorowner']) &&
172✔
1268
            array_key_exists($groupid, $_SESSION['modorowner'][$this->id])) {
172✔
1269
            return ($_SESSION['modorowner'][$this->id][$groupid]);
27✔
1270
        } else {
1271
            $sql = "SELECT groupid FROM memberships WHERE userid = ? AND role IN ('Moderator', 'Owner') AND groupid = ?;";
172✔
1272
            #error_log("$sql {$this->id}, $groupid");
1273
            $groups = $this->dbhr->preQuery($sql, [
172✔
1274
                $this->id,
172✔
1275
                $groupid
172✔
1276
            ]);
172✔
1277

1278
            foreach ($groups as $group) {
172✔
1279
                $_SESSION['modorowner'][$this->id][$groupid] = TRUE;
42✔
1280
                return TRUE;
42✔
1281
            }
1282

1283
            $_SESSION['modorowner'][$this->id][$groupid] = FALSE;
155✔
1284
            return (FALSE);
155✔
1285
        }
1286
    }
1287

1288
    public function getLogins($credentials = TRUE, $id = NULL, $excludelink = FALSE)
1289
    {
1290
        $excludelinkq = $excludelink ? (" AND type != '" . User::LOGIN_LINK . "'") : '';
280✔
1291

1292
        $logins = $this->dbhr->preQuery("SELECT * FROM users_logins WHERE userid = ? $excludelinkq ORDER BY lastaccess DESC;",
280✔
1293
            [$id ? $id : $this->id]);
280✔
1294

1295
        foreach ($logins as &$login) {
280✔
1296
            if (!$credentials) {
273✔
1297
                unset($login['credentials']);
21✔
1298
            }
1299
            $login['added'] = Utils::ISODate($login['added']);
273✔
1300
            $login['lastaccess'] = Utils::ISODate($login['lastaccess']);
273✔
1301
            $login['uid'] = '' . $login['uid'];
273✔
1302
        }
1303

1304
        return ($logins);
280✔
1305
    }
1306

1307
    public function findByLogin($type, $uid)
1308
    {
1309
        $logins = $this->dbhr->preQuery("SELECT * FROM users_logins WHERE uid = ? AND type = ?;",
5✔
1310
            [$uid, $type]);
5✔
1311

1312
        foreach ($logins as $login) {
5✔
1313
            return ($login['userid']);
4✔
1314
        }
1315

1316
        return (NULL);
5✔
1317
    }
1318

1319
    public function addLogin($type, $uid, $creds = NULL, $salt = PASSWORD_SALT)
1320
    {
1321
        if ($type == User::LOGIN_NATIVE) {
421✔
1322
            # Native login - encrypt the password a bit.  The password salt is global in FD, but per-login for users
1323
            # migrated from Norfolk.
1324
            $creds = $this->hashPassword($creds, $salt);
420✔
1325
            $uid = $this->id;
420✔
1326
        }
1327

1328
        # If the login with this type already exists in the table, that's fine.
1329
        $sql = "INSERT INTO users_logins (userid, uid, type, credentials, salt) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE credentials = ?, salt = ?;";
421✔
1330
        $rc = $this->dbhm->preExec($sql,
421✔
1331
            [$this->id, $uid, $type, $creds, $salt, $creds, $salt]);
421✔
1332

1333
        # If we add a login, we might be about to log in.
1334
        # TODO This is a bit hacky.
1335
        global $sessionPrepared;
421✔
1336
        $sessionPrepared = FALSE;
421✔
1337

1338
        return ($rc);
421✔
1339
    }
1340

1341
    public function removeLogin($type, $uid)
1342
    {
1343
        $rc = $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ? AND type = ? AND uid = ?;",
4✔
1344
            [$this->id, $type, $uid]);
4✔
1345
        return ($rc);
4✔
1346
    }
1347

1348
    public function getRoleForGroup($groupid, $overrides = TRUE)
1349
    {
1350
        # We can have a number of roles on a group
1351
        # - none, we can only see what is member
1352
        # - member, we are a group member and can see some extra info
1353
        # - moderator, we can see most info on a group
1354
        # - owner, we can see everything
1355
        #
1356
        # If our system role is support then we get moderator status; if it's admin we get owner status.
1357
        $role = User::ROLE_NONMEMBER;
74✔
1358

1359
        if ($overrides) {
74✔
1360
            switch ($this->getPrivate('systemrole')) {
35✔
1361
                case User::SYSTEMROLE_SUPPORT:
1362
                    $role = User::ROLE_MODERATOR;
3✔
1363
                    break;
3✔
1364
                case User::SYSTEMROLE_ADMIN:
1365
                    $role = User::ROLE_OWNER;
1✔
1366
                    break;
1✔
1367
            }
1368
        }
1369

1370
        # Now find if we have any membership of the group which might also give us a role.
1371
        $membs = $this->dbhr->preQuery("SELECT role FROM memberships WHERE userid = ? AND groupid = ?;", [
74✔
1372
            $this->id,
74✔
1373
            $groupid
74✔
1374
        ]);
74✔
1375

1376
        foreach ($membs as $memb) {
74✔
1377
            switch ($memb['role']) {
71✔
1378
                case 'Moderator':
71✔
1379
                    # Don't downgrade from owner if we have that by virtue of an override.
1380
                    $role = $role == User::ROLE_OWNER ? $role : User::ROLE_MODERATOR;
35✔
1381
                    break;
35✔
1382
                case 'Owner':
63✔
1383
                    $role = User::ROLE_OWNER;
8✔
1384
                    break;
8✔
1385
                case 'Member':
61✔
1386
                    # Don't downgrade if we already have a role by virtue of an override.
1387
                    $role = $role == User::ROLE_NONMEMBER ? User::ROLE_MEMBER : $role;
61✔
1388
                    break;
61✔
1389
            }
1390
        }
1391

1392
        return ($role);
74✔
1393
    }
1394

1395
    public function moderatorForUser($userid, $allowmod = FALSE)
1396
    {
1397
        # There are times when we want to check whether we can administer a user, but when we are not immediately
1398
        # within the context of a known group.  We can administer a user when:
1399
        # - they're only a user themselves
1400
        # - we are a mod on one of the groups on which they are a member.
1401
        # - it's us
1402
        if ($userid != $this->getId()) {
13✔
1403
            $u = User::get($this->dbhr, $this->dbhm, $userid);
10✔
1404

1405
            $usermemberships = [];
10✔
1406
            $modq = $allowmod ? ", 'Moderator', 'Owner'" : '';
10✔
1407
            $groups = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ? AND role IN ('Member' $modq);", [$userid]);
10✔
1408
            foreach ($groups as $group) {
10✔
1409
                $usermemberships[] = $group['groupid'];
7✔
1410
            }
1411

1412
            $mymodships = $this->getModeratorships();
10✔
1413

1414
            # Is there any group which we mod and which they are a member of?
1415
            $canmod = count(array_intersect($usermemberships, $mymodships)) > 0;
10✔
1416
        } else {
1417
            $canmod = TRUE;
5✔
1418
        }
1419

1420
        return ($canmod);
13✔
1421
    }
1422

1423
    public function getSetting($setting, $default)
1424
    {
1425
        $ret = $default;
450✔
1426
        $s = $this->getPrivate('settings');
450✔
1427

1428
        if ($s) {
450✔
1429
            $settings = json_decode($s, TRUE);
20✔
1430
            $ret = array_key_exists($setting, $settings) ? $settings[$setting] : $default;
20✔
1431
        }
1432

1433
        return ($ret);
450✔
1434
    }
1435

1436
    public function setSetting($setting, $val)
1437
    {
1438
        $s = $this->getPrivate('settings');
29✔
1439

1440
        if ($s) {
29✔
1441
            $settings = json_decode($s, TRUE);
1✔
1442
        } else {
1443
            $settings = [];
29✔
1444
        }
1445

1446
        $settings[$setting] = $val;
29✔
1447
        $this->setPrivate('settings', json_encode($settings));
29✔
1448
    }
1449

1450
    public function setGroupSettings($groupid, $settings)
1451
    {
1452
        $this->clearMembershipCache();
5✔
1453
        $sql = "UPDATE memberships SET settings = ? WHERE userid = ? AND groupid = ?;";
5✔
1454
        return ($this->dbhm->preExec($sql, [
5✔
1455
            json_encode($settings),
5✔
1456
            $this->id,
5✔
1457
            $groupid
5✔
1458
        ]));
5✔
1459
    }
1460

1461
    public function activeModForGroup($groupid, $mysettings = NULL)
1462
    {
1463
        $mysettings = $mysettings ? $mysettings : $this->getGroupSettings($groupid);
38✔
1464

1465
        # If we have the active flag use that; otherwise assume that the legacy showmessages flag tells us.  Default
1466
        # to active.
1467
        # TODO Retire showmessages entirely and remove from user configs.
1468
        $active = array_key_exists('active', $mysettings) ? $mysettings['active'] : (!array_key_exists('showmessages', $mysettings) || $mysettings['showmessages']);
38✔
1469
        return ($active);
38✔
1470
    }
1471

1472
    public function getGroupSettings($groupid, $configid = NULL, $id = NULL)
1473
    {
1474
        $id = $id ? $id : $this->id;
152✔
1475

1476
        # We have some parameters which may give us some info which saves queries
1477
        $this->cacheMemberships($id);
152✔
1478

1479
        # Defaults match memberships ones in Group.php.
1480
        $defaults = [
152✔
1481
            'active' => 1,
152✔
1482
            'showchat' => 1,
152✔
1483
            'pushnotify' => 1,
152✔
1484
            'eventsallowed' => 1,
152✔
1485
            'volunteeringallowed' => 1
152✔
1486
        ];
152✔
1487

1488
        $settings = $defaults;
152✔
1489

1490
        if (Utils::pres($groupid, $this->memberships)) {
152✔
1491
            $set = $this->memberships[$groupid];
152✔
1492

1493
            if ($set['settings']) {
152✔
1494
                $settings = json_decode($set['settings'], TRUE);
4✔
1495

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

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

1504
            # Base active setting on legacy showmessages setting if not present.
1505
            $settings['active'] = array_key_exists('active', $settings) ? $settings['active'] : (!array_key_exists('showmessages', $settings) || $settings['showmessages']);
152✔
1506
            $settings['active'] = $settings['active'] ? 1 : 0;
152✔
1507

1508
            foreach ($defaults as $key => $val) {
152✔
1509
                if (!array_key_exists($key, $settings)) {
152✔
1510
                    $settings[$key] = $val;
4✔
1511
                }
1512
            }
1513

1514
            $settings['emailfrequency'] = $set['emailfrequency'];
152✔
1515
            $settings['eventsallowed'] = $set['eventsallowed'];
152✔
1516
            $settings['volunteeringallowed'] = $set['volunteeringallowed'];
152✔
1517
        }
1518

1519
        return ($settings);
152✔
1520
    }
1521

1522
    public function setRole($role, $groupid)
1523
    {
1524
        $rc = TRUE;
45✔
1525
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
45✔
1526

1527
        Session::clearSessionCache();
45✔
1528

1529
        $currentRole = $this->getRoleForGroup($groupid, FALSE);
45✔
1530

1531
        if ($currentRole != $role) {
45✔
1532
            $l = new Log($this->dbhr, $this->dbhm);
45✔
1533
            $l->log([
45✔
1534
                        'type' => Log::TYPE_USER,
45✔
1535
                        'byuser' => $me ? $me->getId() : NULL,
45✔
1536
                        'subtype' => Log::SUBTYPE_ROLE_CHANGE,
45✔
1537
                        'groupid' => $groupid,
45✔
1538
                        'user' => $this->id,
45✔
1539
                        'text' => $role
45✔
1540
                    ]);
45✔
1541

1542
            $this->clearMembershipCache();
45✔
1543
            $sql = "UPDATE memberships SET role = ? WHERE userid = ? AND groupid = ?;";
45✔
1544
            $rc = $this->dbhm->preExec($sql, [
45✔
1545
                $role,
45✔
1546
                $this->id,
45✔
1547
                $groupid
45✔
1548
            ]);
45✔
1549

1550
            # We might need to update the systemrole.
1551
            #
1552
            # Not the end of the world if this fails.
1553
            $this->updateSystemRole($role);
45✔
1554

1555
            if ($currentRole == User::ROLE_MEMBER) {
45✔
1556
                # We have promoted this member.  We want to ensure that they have no unread old chats.
1557
                $r = new ChatRoom($this->dbhr, $this->dbhm);
44✔
1558
                $r->upToDateAll($this->getId(),[
44✔
1559
                    ChatRoom::TYPE_USER2MOD
44✔
1560
                ]);
44✔
1561
            }
1562

1563
            $this->memberships = NULL;
45✔
1564
        }
1565

1566
        return ($rc);
45✔
1567
    }
1568

1569
    public function getActiveCounts() {
1570
        $users = [
1✔
1571
            $this->id => [
1✔
1572
                'id' => $this->id
1✔
1573
            ]];
1✔
1574

1575
        $this->getActiveCountss($users);
1✔
1576
        return($users[$this->id]['activecounts']);
1✔
1577
    }
1578

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

1583
        if (count($uids)) {
17✔
1584
            $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✔
1585
                $start,
16✔
1586
                MessageCollection::APPROVED
16✔
1587
            ]);
16✔
1588

1589
            foreach ($users as $user) {
16✔
1590
                $offers = 0;
16✔
1591
                $wanteds = 0;
16✔
1592

1593
                foreach ($counts as $count) {
16✔
1594
                    if ($count['userid'] == $user['id']) {
1✔
1595
                        if ($count['type'] == Message::TYPE_OFFER) {
1✔
1596
                            $offers += $count['count'];
1✔
1597
                        } else if ($count['type'] == Message::TYPE_WANTED) {
1✔
1598
                            $wanteds += $count['count'];
1✔
1599
                        }
1600
                    }
1601
                }
1602

1603
                $users[$user['id']]['activecounts'] = [
16✔
1604
                    'offers' => $offers,
16✔
1605
                    'wanteds' => $wanteds
16✔
1606
                ];
16✔
1607
            }
1608
        }
1609
    }
1610

1611
    public function getInfos(&$users, $grace = ChatRoom::REPLY_GRACE) {
1612
        $uids = array_filter(array_column($users, 'id'));
121✔
1613

1614
        $start = date('Y-m-d', strtotime(User::OPEN_AGE . " days ago"));
121✔
1615
        $days90 = date("Y-m-d", strtotime("90 days ago"));
121✔
1616
        $userq = "userid IN (" . implode(',', $uids) . ")";
121✔
1617

1618
        foreach ($uids as $uid) {
121✔
1619
            $users[$uid]['info']['replies'] = 0;
121✔
1620
            $users[$uid]['info']['taken'] = 0;
121✔
1621
            $users[$uid]['info']['reneged'] = 0;
121✔
1622
            $users[$uid]['info']['collected'] = 0;
121✔
1623
            $users[$uid]['info']['openage'] = User::OPEN_AGE;
121✔
1624
        }
1625

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

1635
        if (Session::modtools()) {
121✔
1636
            $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 ";
119✔
1637
            $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 ";
119✔
1638
        }
1639

1640
        $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
121✔
1641
(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
121✔
1642
(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
121✔
1643
;";
121✔
1644
        $counts = $this->dbhr->preQuery($sql, [
121✔
1645
            $start,
121✔
1646
            ChatMessage::TYPE_INTERESTED,
121✔
1647
            $start,
121✔
1648
            Message::TYPE_OFFER,
121✔
1649
            ChatMessage::TYPE_INTERESTED
121✔
1650
        ]);
121✔
1651

1652
        foreach ($users as $uid => $user) {
121✔
1653
            foreach ($counts as $count) {
121✔
1654
                if ($count['theuserid'] == $users[$uid]['id']) {
121✔
1655
                    $users[$uid]['info']['replies'] = $count['replycount'] ? $count['replycount'] : 0;
121✔
1656

1657
                    if (Session::modtools()) {
121✔
1658
                        $users[$uid]['info']['repliesoffer'] = $count['replycountoffer'] ? $count['replycountoffer'] : 0;
119✔
1659
                        $users[$uid]['info']['replieswanted'] = $count['replycountwanted'] ? $count['replycountwanted'] : 0;
119✔
1660
                    }
1661

1662
                    $users[$uid]['info']['reneged'] = $count['reneged'] ? $count['reneged'] : 0;
121✔
1663
                    $users[$uid]['info']['collected'] = $count['collected'] ? $count['collected'] : 0;
121✔
1664
                    $users[$uid]['info']['lastaccess'] = $count['lastaccess'] ? Utils::ISODate($count['lastaccess']) : NULL;
121✔
1665
                    $users[$uid]['info']['count'] = $count;
121✔
1666

1667
                    if (Utils::pres('abouttime', $count)) {
121✔
1668
                        $users[$uid]['info']['aboutme'] = [
2✔
1669
                            'timestamp' => Utils::ISODate($count['abouttime']),
2✔
1670
                            'text' => $count['abouttext']
2✔
1671
                        ];
2✔
1672
                    }
1673
                }
1674
            }
1675
        }
1676

1677
        $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;";
121✔
1678
        $counts = $this->dbhr->preQuery($sql, [
121✔
1679
            $start,
121✔
1680
            MessageCollection::APPROVED
121✔
1681
        ]);
121✔
1682

1683
        foreach ($users as $uid => $user) {
121✔
1684
            $users[$uid]['info']['offers'] = 0;
121✔
1685
            $users[$uid]['info']['wanteds'] = 0;
121✔
1686
            $users[$uid]['info']['openoffers'] = 0;
121✔
1687
            $users[$uid]['info']['openwanteds'] = 0;
121✔
1688
            $users[$uid]['info']['expectedreply'] = 0;
121✔
1689

1690
            foreach ($counts as $count) {
121✔
1691
                if ($count['userid'] == $users[$uid]['id']) {
58✔
1692
                    if ($count['type'] == Message::TYPE_OFFER) {
58✔
1693
                        $users[$uid]['info']['offers'] += $count['count'];
40✔
1694

1695
                        if (!Utils::pres('outcome', $count)) {
40✔
1696
                            $users[$uid]['info']['openoffers'] += $count['count'];
40✔
1697
                        }
1698
                    } else if ($count['type'] == Message::TYPE_WANTED) {
20✔
1699
                        $users[$uid]['info']['wanteds'] += $count['count'];
7✔
1700

1701
                        if (!Utils::pres('outcome', $count)) {
7✔
1702
                            $users[$uid]['info']['openwanteds'] += $count['count'];
3✔
1703
                        }
1704
                    }
1705
                }
1706
            }
1707
        }
1708

1709
        # Distance away.
1710
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
121✔
1711

1712
        if ($me) {
121✔
1713
            list ($mylat, $mylng, $myloc) = $me->getLatLng();
82✔
1714

1715
            if (!is_null($myloc)) {
82✔
1716
                $latlngs = $this->getLatLngs($users, FALSE, TRUE);
14✔
1717

1718
                foreach ($latlngs as $userid => $latlng) {
14✔
1719
                    if ($latlng) {
14✔
1720
                        $users[$userid]['info']['milesaway'] = $this->getDistanceBetween($mylat, $mylng, $latlng['lat'], $latlng['lng']);
14✔
1721
                    }
1722
                }
1723
            }
1724

1725
            $this->getPublicLocations($users);
82✔
1726
        }
1727

1728
        $r = new ChatRoom($this->dbhr, $this->dbhm);
121✔
1729
        $replytimes = $r->replyTimes($uids);
121✔
1730

1731
        foreach ($replytimes as $uid => $replytime) {
121✔
1732
            $users[$uid]['info']['replytime'] = $replytime;
121✔
1733
        }
1734

1735
        $nudges = $r->nudgeCounts($uids);
121✔
1736

1737
        foreach ($nudges as $uid => $nudgecount) {
121✔
1738
            $users[$uid]['info']['nudges'] = $nudgecount;
121✔
1739
        }
1740

1741
        $ratings = $this->getRatings($uids);
121✔
1742

1743
        foreach ($ratings as $uid => $rating) {
121✔
1744
            $users[$uid]['info']['ratings'] = $rating;
121✔
1745
        }
1746

1747
        $replies = $this->getExpectedReplies($uids, ChatRoom::ACTIVELIM, $grace);
121✔
1748

1749
        foreach ($replies as $reply) {
121✔
1750
            if ($reply['expectee']) {
121✔
1751
                $users[$reply['expectee']]['info']['expectedreply'] = $reply['count'];
1✔
1752
            }
1753
        }
1754
    }
1755
    
1756
    public function getInfo($grace = ChatRoom::REPLY_GRACE)
1757
    {
1758
        $users = [
13✔
1759
            $this->id => [
13✔
1760
                'id' => $this->id
13✔
1761
            ]
13✔
1762
        ];
13✔
1763

1764
        $this->getInfos($users, $grace);
13✔
1765

1766
        return ($users[$this->id]['info']);
13✔
1767
    }
1768

1769
    public function getAboutMe() {
1770
        $ret = NULL;
46✔
1771

1772
        $aboutmes = $this->dbhr->preQuery("SELECT * FROM users_aboutme WHERE userid = ? ORDER BY timestamp DESC LIMIT 1;", [
46✔
1773
            $this->id
46✔
1774
        ]);
46✔
1775

1776
        foreach ($aboutmes as $aboutme) {
46✔
1777
            $ret = [
1✔
1778
                'timestamp' => Utils::ISODate($aboutme['timestamp']),
1✔
1779
                'text' => $aboutme['text']
1✔
1780
            ];
1✔
1781
        }
1782

1783
        return($ret);
46✔
1784
    }
1785

1786
    private function md5_hex_to_dec($hex_str)
1787
    {
1788
        $arr = str_split($hex_str, 4);
21✔
1789
        foreach ($arr as $grp) {
21✔
1790
            $dec[] = str_pad(hexdec($grp), 5, '0', STR_PAD_LEFT);
21✔
1791
        }
1792
        return floatval("0." . implode('', $dec));
21✔
1793
    }
1794

1795
    public function getDistance($mylat, $mylng) {
1796
        list ($tlat, $tlng, $tloc) = $this->getLatLng();
1✔
1797
        #error_log("Get distance $mylat, $mylng, $tlat, $tlng = " . $this->getDistanceBetween($mylat, $mylng, $tlat, $tlng));
1798
        return($this->getDistanceBetween($mylat, $mylng, $tlat, $tlng));
1✔
1799
    }
1800

1801
    public function getDistanceBetween($mylat, $mylng, $tlat, $tlng)
1802
    {
1803
        $p1 = new POI($mylat, $mylng);
21✔
1804

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

1811
        # Now randomise the distance a bit each time we get it, so that anyone attempting repeated measurements
1812
        # will get conflicting results around the precise location that isn't actually theirs.  But still close
1813
        # enough to be useful for our purposes.
1814
        $tlat += mt_rand(-100, 100) / 20000;
21✔
1815
        $tlng += mt_rand(-100, 100) / 20000;
21✔
1816

1817
        $p2 = new POI($tlat, $tlng);
21✔
1818
        $metres = $p1->getDistanceInMetersTo($p2);
21✔
1819
        $miles = $metres / 1609.344;
21✔
1820
        $miles = $miles > 2 ? round($miles) : round($miles, 1);
21✔
1821
        return ($miles);
21✔
1822
    }
1823

1824
    public function gravatar($email, $s = 80, $d = 'mm', $r = 'g')
1825
    {
1826
        $url = 'https://www.gravatar.com/avatar/';
19✔
1827
        $url .= md5(strtolower(trim($email)));
19✔
1828
        $url .= "?s=$s&d=$d&r=$r";
19✔
1829
        return $url;
19✔
1830
    }
1831

1832
    public function getPublicLocation()
1833
    {
1834
        $users = [
29✔
1835
            $this->id => [
29✔
1836
                'id' => $this->id
29✔
1837
            ]
29✔
1838
        ];
29✔
1839

1840
        $this->getLatLngs($users);
29✔
1841
        $this->getPublicLocations($users);
29✔
1842

1843
        return($users[$this->id]['info']['publiclocation']);
29✔
1844
    }
1845

1846
    public function ensureAvatar(&$atts)
1847
    {
1848
        # This involves querying external sites, so we need to use it with care, otherwise we can hang our
1849
        # system.  It can also cause updates, so if we call it lots of times, it can result in cluster issues.
1850
        $forcedefault = FALSE;
18✔
1851
        $settings = Utils::presdef('settings', $atts, NULL);
18✔
1852

1853
        if ($settings) {
18✔
1854
            if (array_key_exists('useprofile', $settings) && !$settings['useprofile']) {
18✔
1855
                $forcedefault = TRUE;
1✔
1856
            }
1857
        }
1858

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

1863
            try {
1864
                foreach ($emails as $email) {
18✔
1865
                    if (preg_match('/(.*)-g.*@user.trashnothing.com/', $email['email'], $matches)) {
5✔
1866
                        # TrashNothing has an API we can use.
1867
                        $url = "https://trashnothing.com/api/users/{$matches[1]}/profile-image?default=" . urlencode('https://' . IMAGE_DOMAIN . '/defaultprofile.png');
1✔
1868
                        $atts['profile'] = [
1✔
1869
                            'url' => $url,
1✔
1870
                            'turl' => $url,
1✔
1871
                            'default' => FALSE,
1✔
1872
                            'TN' => TRUE
1✔
1873
                        ];
1✔
1874
                    } else if (!Mail::ourDomain($email['email'])) {
5✔
1875
                        # Try for gravatar
1876
                        $gurl = $this->gravatar($email['email'], 200, 404);
5✔
1877
                        $g = @file_get_contents($gurl);
5✔
1878

1879
                        if ($g) {
5✔
1880
                            $atts['profile'] = [
1✔
1881
                                'url' => $gurl,
1✔
1882
                                'turl' => $this->gravatar($email['email'], 100, 404),
1✔
1883
                                'default' => FALSE,
1✔
1884
                                'gravatar' => TRUE
1✔
1885
                            ];
1✔
1886

1887
                            break;
1✔
1888
                        }
1889
                    }
1890
                }
1891

1892
                if ($atts['profile']['default']) {
18✔
1893
                    # Try for Facebook.
1894
                    $logins = $this->getLogins(TRUE);
18✔
1895
                    foreach ($logins as $login) {
18✔
1896
                        if ($login['type'] == User::LOGIN_FACEBOOK) {
5✔
1897
                            if (Utils::presdef('useprofile', $atts['settings'], TRUE)) {
×
1898
                                // As of October 2020 we can no longer just access the profile picture via the UID, we need to make a
1899
                                // call to the Graph API to fetch it.
1900
                                $f = new Facebook($this->dbhr, $this->dbhm);
×
1901
                                $atts['profile'] = $f->getProfilePicture($login['uid']);
×
1902
                            }
1903
                        }
1904
                    }
1905
                }
1906
            } catch (Throwable $e) {}
×
1907

1908
            $hash = NULL;
18✔
1909

1910
            if (!Utils::pres('default', $atts['profile'])) {
18✔
1911
                # We think we have a profile.  Make sure we can fetch it and filter out other people's
1912
                # default images.
1913
                $atts['profile']['default'] = TRUE;
1✔
1914
                $this->filterDefault($atts['profile'], $hash);
1✔
1915
            }
1916

1917
            if (Utils::pres('default', $atts['profile'])) {
18✔
1918
                # Nothing - so get gravatar to generate a default for us.
1919
                $atts['profile'] = [
18✔
1920
                    'url' => $this->gravatar($this->getEmailPreferred(), 200, 'identicon'),
18✔
1921
                    'turl' => $this->gravatar($this->getEmailPreferred(), 100, 'identicon'),
18✔
1922
                    'default' => FALSE,
18✔
1923
                    'gravatar' => TRUE
18✔
1924
                ];
18✔
1925
            }
1926

1927
            # Save for next time.
1928
            $this->dbhm->preExec("INSERT INTO users_images (userid, url, `default`, hash) VALUES (?, ?, ?, ?);", [
18✔
1929
                $atts['id'],
18✔
1930
                $atts['profile']['default'] ? NULL : $atts['profile']['url'],
18✔
1931
                $atts['profile']['default'],
18✔
1932
                $hash
18✔
1933
            ]);
18✔
1934
        }
1935
    }
1936

1937
    public function filterDefault(&$profile, &$hash) {
1938
        $hasher = new ImageHash;
1✔
1939
        $data = Utils::pres('url', $profile) && strlen($profile['url']) ? @file_get_contents($profile['url']) : NULL;
1✔
1940
        $hash = NULL;
1✔
1941

1942
        if ($data) {
1✔
1943
            $img = @imagecreatefromstring($data);
1✔
1944

1945
            if ($img) {
1✔
1946
                $hash = $hasher->hash($img);
1✔
1947
                $profile['default'] = FALSE;
1✔
1948
            }
1949
        }
1950

1951
        if ($hash == 'e070716060607120' || $hash == 'd0f0323171707030' || $hash == '13130f4e0e0e4e52' ||
1✔
1952
            $hash == '1f0fcf9f9f9fcfff' || $hash == '23230f0c0e0e0c24' || $hash == 'c0c0e070e0603100' ||
1✔
1953
            $hash == 'f0f0316870f07130' || $hash == '242e070e060b0d24' || $hash == '69aa49558e4da88e') {
1✔
1954
            # This is a default profile - replace it with ours.
1955
            $profile['url'] = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
1956
            $profile['turl'] = 'https://' . IMAGE_DOMAIN . '/defaultprofile.png';
×
1957
            $profile['default'] = TRUE;
×
1958
            $hash = NULL;
×
1959
        }
1960
    }
1961

1962
    public function getPublic($groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [ MessageCollection::APPROVED ], $historyfull = FALSE)
1963
    {
1964
        $atts = [];
243✔
1965

1966
        if ($this->id) {
243✔
1967
            $users = [ $this->user ];
243✔
1968
            $rets = $this->getPublics($users, $groupids, $history, $comments, $memberof, $applied, $modmailsonly, $emailhistory, $msgcoll, $historyfull);
243✔
1969
            $atts = $rets[$this->id];
243✔
1970
        }
1971

1972
        return($atts);
243✔
1973
    }
1974

1975
    public function getPublicAtts(&$rets, $users, $me) {
1976
        foreach ($users as &$user) {
247✔
1977
            if (!array_key_exists($user['id'], $rets)) {
247✔
1978
                $rets[$user['id']] = [];
247✔
1979
            }
1980

1981
            $atts = $this->publicatts;
247✔
1982

1983
            if (Session::modtools()) {
247✔
1984
                # We have some extra attributes.
1985
                $atts[] = 'deleted';
231✔
1986
                $atts[] = 'lastaccess';
231✔
1987
                $atts[] = 'trustlevel';
231✔
1988
            }
1989

1990
            foreach ($atts as $att) {
247✔
1991
                $rets[$user['id']][$att] = Utils::presdef($att, $user, NULL);
247✔
1992
            }
1993

1994
            $rets[$user['id']]['settings'] = ['dummy' => TRUE];
247✔
1995

1996
            if (Utils::presdef('settings', $user, NULL)) {
247✔
1997
                # This is a bit of a type guddle.
1998
                if (gettype($user['settings']) == 'string') {
34✔
1999
                    $rets[$user['id']]['settings'] = json_decode($user['settings'], TRUE);
33✔
2000
                } else {
2001
                    $rets[$user['id']]['settings'] = $user['settings'];
1✔
2002
                }
2003
            }
2004

2005
            if (Utils::pres('mylocation', $rets[$user['id']]['settings']) && Utils::pres('groupsnear', $rets[$user['id']]['settings']['mylocation'])) {
247✔
2006
                # This is large - no need for it.
2007
                $rets[$user['id']]['settings']['mylocation']['groupsnear'] = NULL;
×
2008
            }
2009

2010
            $rets[$user['id']]['settings']['notificationmails'] = array_key_exists('notificationmails', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['notificationmails'] : TRUE;
247✔
2011
            $rets[$user['id']]['settings']['engagement'] = array_key_exists('engagement', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['engagement'] : TRUE;
247✔
2012
            $rets[$user['id']]['settings']['modnotifs'] = array_key_exists('modnotifs', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['modnotifs'] : 4;
247✔
2013
            $rets[$user['id']]['settings']['backupmodnotifs'] = array_key_exists('backupmodnotifs', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['backupmodnotifs'] : 12;
247✔
2014

2015
            $rets[$user['id']]['displayname'] = $this->getName(TRUE, $user);
247✔
2016

2017
            $rets[$user['id']]['added'] = array_key_exists('added', $user) ? Utils::ISODate($user['added']) : NULL;
247✔
2018

2019
            foreach (['fullname', 'firstname', 'lastname'] as $att) {
247✔
2020
                # Make sure we don't return an email if somehow one has snuck in.
2021
                $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];
247✔
2022
            }
2023

2024
            if ($me && $rets[$user['id']]['id'] == $me->getId()) {
247✔
2025
                # Add in private attributes for our own entry.
2026
                $rets[$user['id']]['emails'] = $me->getEmails();
141✔
2027
                $rets[$user['id']]['email'] = $me->getEmailPreferred();
141✔
2028
                $rets[$user['id']]['relevantallowed'] = $me->getPrivate('relevantallowed');
141✔
2029
                $rets[$user['id']]['permissions'] = $me->getPrivate('permissions');
141✔
2030
            }
2031

2032
            if ($me && ($me->isModerator() || $user['id'] == $me->getId())) {
247✔
2033
                # Mods can see email settings, no matter which group.
2034
                $rets[$user['id']]['onholidaytill'] = (Utils::pres('onholidaytill', $rets[$user['id']]) && (time() < strtotime($rets[$user['id']]['onholidaytill']))) ? Utils::ISODate($rets[$user['id']]['onholidaytill']) : NULL;
160✔
2035
            } else {
2036
                # Don't show some attributes unless they're a mod or ourselves.
2037
                $ismod = $rets[$user['id']]['systemrole'] == User::SYSTEMROLE_ADMIN ||
128✔
2038
                    $rets[$user['id']]['systemrole'] == User::SYSTEMROLE_SUPPORT ||
128✔
2039
                    $rets[$user['id']]['systemrole'] == User::SYSTEMROLE_MODERATOR;
128✔
2040
                $showmod = $ismod && Utils::presdef('showmod', $rets[$user['id']]['settings'], FALSE);
128✔
2041
                $rets[$user['id']]['settings']['showmod'] = $showmod;
128✔
2042
                $rets[$user['id']]['yahooid'] = NULL;
128✔
2043
            }
2044

2045
            if (Utils::pres('deleted', $rets[$user['id']])) {
247✔
2046
                $rets[$user['id']]['deleted'] = Utils::ISODate($rets[$user['id']]['deleted']);
×
2047
            }
2048

2049
            if (Utils::pres('lastaccess', $rets[$user['id']])) {
247✔
2050
                $rets[$user['id']]['lastaccess'] = Utils::ISODate($rets[$user['id']]['lastaccess']);
231✔
2051
            }
2052
        }
2053
    }
2054
    
2055
    public function getPublicProfiles(&$rets, $users) {
2056
        $idsleft = [];
248✔
2057

2058
        foreach ($rets as $userid => $ret) {
248✔
2059
            if (Utils::pres($userid, $users)) {
248✔
2060
                if (Utils::pres('profile', $users[$userid])) {
7✔
2061
                    $rets[$userid]['profile'] = $users[$userid]['profile'];
1✔
2062
                } else {
2063
                    $idsleft[] = $userid;
7✔
2064
                }
2065
            } else {
2066
                $idsleft[] = $userid;
244✔
2067
            }
2068
        }
2069

2070
        if (count($idsleft)) {
248✔
2071
            foreach ($idsleft as $id) {
248✔
2072
                $rets[$id]['profile'] = [
248✔
2073
                    'url' => 'https://' . IMAGE_DOMAIN . '/defaultprofile.png',
248✔
2074
                    'turl' => 'https://' . IMAGE_DOMAIN . '/defaultprofile.png',
248✔
2075
                    'default' => TRUE
248✔
2076
                ];
248✔
2077
            }
2078

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

2082
            $profiles = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
248✔
2083

2084
            foreach ($profiles as $profile) {
248✔
2085
                # Get a profile.  This function is called so frequently that we can't afford to query external sites
2086
                # within it, so if we don't find one, we default to none.
2087
                if (Utils::pres('settings', $rets[$profile['userid']]) &&
12✔
2088
                    gettype($rets[$profile['userid']]['settings']) == 'array' &&
12✔
2089
                    (!array_key_exists('useprofile', $rets[$profile['userid']]['settings']) || $rets[$profile['userid']]['settings']['useprofile'])) {
12✔
2090
                    # We found a profile that we can use.
2091
                    if (!$profile['default']) {
12✔
2092
                        # If it's a gravatar image we can return a thumbnail url that specifies a different size.
2093
                        $turl = Utils::pres('url', $profile) ? $profile['url'] : ('https://' . IMAGE_DOMAIN . "/tuimg_{$profile['id']}.jpg");
12✔
2094
                        $turl = strpos($turl, 'https://www.gravatar.com') === 0 ? str_replace('?s=200', '?s=100', $turl) : $turl;
12✔
2095
                        $rets[$profile['userid']]['profile'] = [
12✔
2096
                            'id' => $profile['id'],
12✔
2097
                            'url' => Utils::pres('url', $profile) ? $profile['url'] : ('https://' . IMAGE_DOMAIN . "/uimg_{$profile['id']}.jpg"),
12✔
2098
                            'turl' => $turl,
12✔
2099
                            'default' => FALSE
12✔
2100
                        ];
12✔
2101
                    }
2102
                }
2103
            }
2104
        }
2105
    }
2106

2107
    public function getPublicHistory($me, &$rets, $users, $historyfull, $systemrole, $msgcoll = [ MessageCollection::APPROVED ]) {
2108
        $idsleft = [];
113✔
2109

2110
        foreach ($rets as $userid => $ret) {
113✔
2111
            if (Utils::pres($userid, $users)) {
113✔
2112
                if (array_key_exists('messagehistory', $users[$userid])) {
5✔
2113
                    $rets[$userid]['messagehistory'] = $users[$userid]['messagehistory'];
1✔
2114
                    $rets[$userid]['modmails'] = $users[$userid]['modmails'];
1✔
2115
                } else {
2116
                    $idsleft[] = $userid;
5✔
2117
                }
2118
            } else {
2119
                $idsleft[] = $userid;
109✔
2120
            }
2121
        }
2122

2123
        if (count($idsleft)) {
113✔
2124
            foreach ($rets as &$atts) {
113✔
2125
                $atts['messagehistory'] = [];
113✔
2126
            }
2127

2128
            # Add in the message history - from any of the emails associated with this user.
2129
            #
2130
            # We want one entry in here for each repost, so we LEFT JOIN with the reposts table.
2131
            $sql = null;
113✔
2132

2133
            if (count($idsleft)) {
113✔
2134
                $collq = " AND messages_groups.collection IN ('" . implode("','", $msgcoll) . "') ";
113✔
2135
                $earliest = $historyfull ? '1970-01-01' : date('Y-m-d', strtotime("midnight 30 days ago"));
113✔
2136
                $delq = $historyfull ? '' : ' AND messages_groups.deleted = 0';
113✔
2137

2138
                $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(
113✔
2139
                        ',',
113✔
2140
                        $idsleft
113✔
2141
                    ) . ") $delq LEFT JOIN messages_postings ON messages.id = messages_postings.msgid HAVING postdate > ? ORDER BY postdate DESC;";
113✔
2142
            }
2143

2144
            if ($sql) {
113✔
2145
                $histories = $this->dbhr->preQuery(
113✔
2146
                    $sql,
113✔
2147
                    [
113✔
2148
                        $earliest
113✔
2149
                    ]
113✔
2150
                );
113✔
2151

2152
                foreach ($rets as $userid => $ret) {
113✔
2153
                    foreach ($histories as $history) {
113✔
2154
                        if ($history['fromuser'] == $ret['id']) {
35✔
2155
                            $history['arrival'] = Utils::pres('repostdate', $history) ? Utils::ISODate(
35✔
2156
                                $history['repostdate']
35✔
2157
                            ) : Utils::ISODate($history['arrival']);
35✔
2158
                            $history['date'] = Utils::ISODate($history['date']);
35✔
2159
                            $rets[$userid]['messagehistory'][] = $history;
35✔
2160
                        }
2161
                    }
2162
                }
2163
            }
2164

2165
            # Add in a count of recent "modmail" type logs which a mod might care about.
2166
            $modships = $me ? $me->getModeratorships() : [];
113✔
2167
            $modships = count($modships) == 0 ? [0] : $modships;
113✔
2168
            $sql = "SELECT COUNT(*) AS count, userid FROM `users_modmails` WHERE userid IN (" . implode(
113✔
2169
                    ',',
113✔
2170
                    $idsleft
113✔
2171
                ) . ") AND groupid IN (" . implode(',', $modships) . ") GROUP BY userid;";
113✔
2172
            $modmails = $this->dbhr->preQuery($sql, null, false, false);
113✔
2173

2174
            foreach ($idsleft as $userid) {
113✔
2175
                $rets[$userid]['modmails'] = 0;
113✔
2176
            }
2177

2178
            foreach ($rets as $userid => $ret) {
113✔
2179
                foreach ($modmails as $modmail) {
113✔
2180
                    if ($modmail['userid'] == $ret['id']) {
1✔
2181
                        $rets[$userid]['modmails'] = $modmail['count'] ? $modmail['count'] : 0;
1✔
2182
                    }
2183
                }
2184
            }
2185
        }
2186
    }
2187

2188
    public function getPublicMemberOf(&$rets, $me, $freeglemod, $memberof, $systemrole) {
2189
        $userids = [];
231✔
2190

2191
        foreach ($rets as $ret) {
231✔
2192
            $ret['activearea'] = NULL;
231✔
2193

2194
            if (!Utils::pres('memberof', $ret)) {
231✔
2195
                # We haven't provided the complete list already, e.g. because the user is suspect.
2196
                $userids[] = $ret['id'];
231✔
2197
            }
2198
        }
2199

2200
        if ($memberof &&
231✔
2201
            count($userids) &&
231✔
2202
            ($systemrole == User::ROLE_MODERATOR || $systemrole == User::SYSTEMROLE_ADMIN || $systemrole == User::SYSTEMROLE_SUPPORT)
231✔
2203
        ) {
2204
            # Gt the recent ones (which preserves some privacy for the user but allows us to spot abuse) and any which
2205
            # are on our groups.
2206
            $addmax = ($systemrole == User::SYSTEMROLE_ADMIN || $systemrole == User::SYSTEMROLE_SUPPORT) ? PHP_INT_MAX : 31;
75✔
2207
            $modids = array_merge([0], $me->getModeratorships());
75✔
2208
            $freegleq = $freeglemod ? " OR groups.type = 'Freegle' " : '';
75✔
2209
            $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);";
75✔
2210
            $groups = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
75✔
2211
            #error_log("Get groups $sql, {$this->id}");
2212

2213
            foreach ($rets as &$ret) {
75✔
2214
                $ret['memberof'] = [];
75✔
2215
                $ourEmailId = NULL;
75✔
2216

2217
                if (Utils::pres('emails', $ret)) {
75✔
2218
                    foreach ($ret['emails'] as $email) {
59✔
2219
                        if (Mail::ourDomain($email['email'])) {
59✔
2220
                            $ourEmailId = $email['id'];
6✔
2221
                        }
2222
                    }
2223
                }
2224

2225
                foreach ($groups as $group) {
75✔
2226
                    if ($ret['id'] ==  $group['userid']) {
64✔
2227
                        $name = $group['namefull'] ? $group['namefull'] : $group['nameshort'];
64✔
2228
                        $added = Utils::ISODate(Utils::pres('yadded', $group) ? $group['yadded'] : $group['added']);
64✔
2229
                        $addedago = floor((time() - strtotime($added)) / 86400);
64✔
2230

2231
                        $ret['memberof'][] = [
64✔
2232
                            'id' => $group['groupid'],
64✔
2233
                            'membershipid' => $group['id'],
64✔
2234
                            'namedisplay' => $name,
64✔
2235
                            'nameshort' => $group['nameshort'],
64✔
2236
                            'added' => $added,
64✔
2237
                            'collection' => $group['coll'],
64✔
2238
                            'role' => $group['role'],
64✔
2239
                            'emailfrequency' => $group['emailfrequency'],
64✔
2240
                            'eventsallowed' => $group['eventsallowed'],
64✔
2241
                            'volunteeringallowed' => $group['volunteeringallowed'],
64✔
2242
                            'ourpostingstatus' => $group['ourPostingStatus'],
64✔
2243
                            'type' => $group['type'],
64✔
2244
                            'onhere' => $group['onhere'],
64✔
2245
                            'reviewrequestedat' => $group['reviewrequestedat'] ? Utils::ISODate($group['reviewrequestedat']) : NULL,
64✔
2246
                            'reviewreason' => $group['reviewreason'],
64✔
2247
                            'reviewedat' => $group['reviewedat'] ? Utils::ISODate($group['reviewedat']) : NULL,
64✔
2248
                        ];
64✔
2249

2250

2251
                        // Counts as active if recently joined.
2252
                        if ($group['lat'] && $group['lng'] && $addedago <= 31) {
64✔
2253
                            $box = Utils::presdef('activearea', $ret, NULL);
2✔
2254

2255
                            $ret['activearea'] = [
2✔
2256
                                'swlat' => is_null($box)? $group['lat'] : min($group['lat'], $box['swlat']),
2✔
2257
                                'swlng' => is_null($box)? $group['lng'] : min($group['lng'], $box['swlng']),
2✔
2258
                                'nelng' => is_null($box)? $group['lng'] : max($group['lng'], $box['nelng']),
2✔
2259
                                'nelat' => is_null($box)? $group['lat'] : max($group['lat'], $box['nelat'])
2✔
2260
                            ];
2✔
2261
                        }
2262
                    }
2263
                }
2264
            }
2265
        }
2266
    }
2267

2268
    public function getPublicApplied(&$rets, $users, $applied, $systemrole) {
2269
        if ($applied &&
231✔
2270
            $systemrole == User::ROLE_MODERATOR ||
175✔
2271
            $systemrole == User::SYSTEMROLE_ADMIN ||
198✔
2272
            $systemrole == User::SYSTEMROLE_SUPPORT
231✔
2273
        ) {
2274
            $idsleft = [];
75✔
2275

2276
            foreach ($rets as $userid => $ret) {
75✔
2277
                if (Utils::pres($userid, $users)) {
75✔
2278
                    if (array_key_exists('applied', $users[$userid])) {
1✔
2279
                        $rets[$userid]['applied'] = $users[$userid]['applied'];
1✔
2280
                        $rets[$userid]['activedistance'] = $users[$userid]['activedistance'];
1✔
2281
                    } else {
2282
                        $idsleft[] = $userid;
1✔
2283
                    }
2284
                } else {
2285
                    $idsleft[] = $userid;
75✔
2286
                }
2287
            }
2288

2289
            if (count($idsleft)) {
75✔
2290
                # As well as being a member of a group, they might have joined and left, or applied and been rejected.
2291
                # This is useful info for moderators.
2292
                $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(
75✔
2293
                        ',',
75✔
2294
                        $idsleft
75✔
2295
                    ) . ") AND DATEDIFF(NOW(), added) <= 31 AND groups.publish = 1 AND groups.onmap = 1 ORDER BY added DESC;";
75✔
2296
                $membs = $this->dbhr->preQuery($sql);
75✔
2297

2298
                foreach ($rets as &$ret) {
75✔
2299
                    $ret['applied'] = [];
75✔
2300
                    $ret['activedistance'] = null;
75✔
2301

2302
                    foreach ($membs as $memb) {
75✔
2303
                        if ($ret['id'] == $memb['userid']) {
65✔
2304
                            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
65✔
2305
                            $memb['namedisplay'] = $name;
65✔
2306
                            $memb['added'] = Utils::ISODate($memb['added']);
65✔
2307
                            $memb['id'] = $memb['groupid'];
65✔
2308
                            unset($memb['groupid']);
65✔
2309

2310
                            if ($memb['lat'] && $memb['lng']) {
65✔
2311
                                $box = Utils::presdef('activearea', $ret, null);
2✔
2312

2313
                                $box = [
2✔
2314
                                    'swlat' => is_null($box)? $memb['lat'] : min($memb['lat'], $box['swlat']),
2✔
2315
                                    'swlng' => is_null($box)? $memb['lng'] : min($memb['lng'], $box['swlng']),
2✔
2316
                                    'nelng' => is_null($box)? $memb['lng'] : max($memb['lng'], $box['nelng']),
2✔
2317
                                    'nelat' => is_null($box)? $memb['lat'] : max($memb['lat'], $box['nelat'])
2✔
2318
                                ];
2✔
2319

2320
                                $ret['activearea'] = $box;
2✔
2321

2322
                                if ($box) {
2✔
2323
                                    $ret['activedistance'] = round(
2✔
2324
                                        Location::getDistance(
2✔
2325
                                            $box['swlat'],
2✔
2326
                                            $box['swlng'],
2✔
2327
                                            $box['nelat'],
2✔
2328
                                            $box['nelng']
2✔
2329
                                        )
2✔
2330
                                    );
2✔
2331
                                }
2332
                            }
2333

2334
                            $ret['applied'][] = $memb;
65✔
2335
                        }
2336
                    }
2337
                }
2338
            }
2339
        }
2340
    }
2341

2342
    public function getPublicSpammer(&$rets, $me, $systemrole) {
2343
        # We want to check for spammers.  If we have suitable rights then we can
2344
        # return detailed info; otherwise just that they are on the list.
2345
        #
2346
        # We don't do this for our own logged in user, otherwise we recurse to death.
2347
        $myid = $me ? $me->getId() : NULL;
231✔
2348
        $userids = array_filter(array_keys($rets), function($val) use ($myid) {
231✔
2349
            return($val != $myid);
231✔
2350
        });
231✔
2351

2352
        if (count($userids)) {
231✔
2353
            # Fetch the users.  There are so many users that there is no point trying to use the query cache.
2354
            $sql = "SELECT * FROM spam_users WHERE userid IN (" . implode(',', $userids) . ");";
151✔
2355

2356
            $users = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
151✔
2357

2358
            foreach ($rets as &$ret) {
151✔
2359
                foreach ($users as &$user) {
151✔
2360
                    if ($user['userid'] == $ret['id']) {
3✔
2361
                        if (Session::modtools() && ($systemrole == User::ROLE_MODERATOR ||
3✔
2362
                                $systemrole == User::SYSTEMROLE_ADMIN ||
3✔
2363
                                $systemrole == User::SYSTEMROLE_SUPPORT)) {
3✔
2364
                            $ret['spammer'] = [];
2✔
2365
                            foreach (['id', 'userid', 'byuserid', 'added', 'collection', 'reason'] as $att) {
2✔
2366
                                $ret['spammer'][$att]= $user[$att];
2✔
2367
                            }
2368

2369
                            $ret['spammer']['added'] = Utils::ISODate($ret['spammer']['added']);
2✔
2370
                        } else if ($user['collection'] == Spam::TYPE_SPAMMER) {
2✔
2371
                            # Only return to members that they are a spammer once approved.
2372
                            $ret['spammer'] = TRUE;
2✔
2373
                        }
2374
                    }
2375
                }
2376
            }
2377
        }
2378
    }
2379

2380
    public function getEmailHistory(&$rets) {
2381
        $userids = array_keys($rets);
3✔
2382

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

2385
        foreach ($rets as $retind => $ret) {
3✔
2386
            $rets[$retind]['emailhistory'] = [];
3✔
2387

2388
            foreach ($emails as $email) {
3✔
2389
                if ($rets[$retind]['id'] == $email['userid']) {
1✔
2390
                    $email['timestamp'] = Utils::ISODate($email['timestamp']);
1✔
2391
                    unset($email['userid']);
1✔
2392
                    $rets[$retind]['emailhistory'][] = $email;
1✔
2393
                }
2394
            }
2395
        }
2396
    }
2397

2398
    public function getPublicsById($uids, $groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [MessageCollection::APPROVED], $historyfull = FALSE) {
2399
        $rets = [];
130✔
2400

2401
        # We might have some of these in cache, especially ourselves.
2402
        $uidsleft = [];
130✔
2403

2404
        foreach ($uids as $uid) {
130✔
2405
            $u = User::get($this->dbhr, $this->dbhm, $uid, TRUE, TRUE);
127✔
2406

2407
            if ($u) {
127✔
2408
                $rets[$uid] = $u->getPublic($groupids, $history, $comments, $memberof, $applied, $modmailsonly, $emailhistory, $msgcoll, $historyfull);
122✔
2409
            } else {
2410
                $uidsleft[] = $uid;
8✔
2411
            }
2412
        }
2413

2414
        $uidsleft = array_filter($uidsleft);
130✔
2415

2416
        if (count($uidsleft)) {
130✔
2417
            $us = $this->dbhr->preQuery("SELECT * FROM users WHERE id IN (" . implode(',', $uidsleft) . ");", NULL, FALSE, FALSE);
7✔
2418
            $users = [];
7✔
2419
            foreach ($us as $u) {
7✔
2420
                $users[$u['id']] = $u;
6✔
2421
            }
2422

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

2426
                foreach ($users as $user) {
6✔
2427
                    $rets[$user['id']] = $user;
6✔
2428
                }
2429
            }
2430
        }
2431

2432
        return($rets);
130✔
2433
    }
2434

2435
    public function isTN() {
2436
        return strpos($this->getEmailPreferred(), '@user.trashnothing.com') !== FALSE;
68✔
2437
    }
2438

2439
    public function isLJ() {
2440
        return $this->user['ljuserid'] !== NULL;
18✔
2441
    }
2442

2443
    public function getPublicEmails(&$rets) {
2444
        $userids = array_keys($rets);
94✔
2445
        $emails = $this->getEmailsById($userids);
94✔
2446

2447
        foreach ($rets as &$ret) {
94✔
2448
            if (Utils::pres($ret['id'], $emails)) {
93✔
2449
                $ret['emails'] = $emails[$ret['id']];
67✔
2450
            }
2451
        }
2452
    }
2453

2454
    public static function purgedUser($id) {
2455
        return [
1✔
2456
            'id' => $id,
1✔
2457
            'displayname' => 'Purged user #' . $id,
1✔
2458
            'systemrole' => User::SYSTEMROLE_USER
1✔
2459
        ];
1✔
2460
    }
2461

2462
    public function getPublicLogs($me, &$rets, $modmailsonly, &$ctx, $suppress = TRUE, $seeall = FALSE) {
2463
        # Add in the log entries we have for this user.  We exclude some logs of little interest to mods.
2464
        # - creation - either of ourselves or others during syncing.
2465
        # - deletion of users due to syncing
2466
        # Don't cache as there might be a lot, they're rarely used, and it can cause UT issues.
2467
        $myid = $me ? $me->getId() : NULL;
18✔
2468
        $uids = array_keys($rets);
18✔
2469
        $startq = $ctx ? (" AND id < " . intval($ctx['id']) . " ") : '';
18✔
2470
        $modships = $me ? $me->getModeratorships() : [];
18✔
2471
        $groupq = count($modships) ? (" AND groupid IN (" . implode(',', $modships) . ")") : '';
18✔
2472
        $modmailq = " AND ((type = 'Message' AND subtype IN ('Rejected', 'Deleted', 'Replied')) OR (type = 'User' AND subtype IN ('Mailed', 'Rejected', 'Deleted'))) $groupq";
18✔
2473
        $modq = $modmailsonly ? $modmailq : '';
18✔
2474
        $suppq = $suppress ? " AND NOT (type = 'User' AND subtype IN('Created', 'Merged', 'YahooConfirmed')) " : '';
18✔
2475
        $sql = "SELECT DISTINCT * FROM logs WHERE (user IN (" . implode(',', $uids) . ") OR byuser IN (" . implode(',', $uids) . ")) $startq $suppq $modq ORDER BY id DESC LIMIT 50;";
18✔
2476
        $logs = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
18✔
2477
        $groups = [];
18✔
2478
        $users = [];
18✔
2479
        $configs = [];
18✔
2480

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

2484
        if (count($loguids)) {
18✔
2485
            $u = new User($this->dbhr, $this->dbhm);
×
2486
            $users = $u->getPublicsById($loguids, NULL, FALSE, FALSE, FALSE, FALSE);
×
2487
        }
2488

2489
        if (!$ctx) {
18✔
2490
            $ctx = ['id' => 0];
18✔
2491
        }
2492

2493
        foreach ($rets as $uid => $ret) {
18✔
2494
            $rets[$uid]['logs'] = [];
18✔
2495

2496
            foreach ($logs as $log) {
18✔
2497
                if ($log['user'] == $ret['id'] || $log['byuser'] == $ret['id']) {
17✔
2498
                    $ctx['id'] = $ctx['id'] == 0 ? $log['id'] : intval(min($ctx['id'], $log['id']));
17✔
2499

2500
                    if (Utils::pres('byuser', $log)) {
17✔
2501
                        if (!Utils::pres($log['byuser'], $users)) {
13✔
2502
                            $u = User::get($this->dbhr, $this->dbhm, $log['byuser']);
10✔
2503

2504
                            if ($u->getId() == $log['byuser']) {
10✔
2505
                                $users[$log['byuser']] = $u->getPublic(NULL, FALSE);
10✔
2506
                            } else {
2507
                                $users[$log['byuser']] = User::purgedUser($log['byuser']);
×
2508
                            }
2509
                        }
2510

2511
                        $log['byuser'] = $users[$log['byuser']];
13✔
2512
                    }
2513

2514
                    if (Utils::pres('user', $log)) {
17✔
2515
                        if (!Utils::pres($log['user'], $users)) {
16✔
2516
                            $u = User::get($this->dbhr, $this->dbhm, $log['user']);
14✔
2517

2518
                            if ($u->getId() == $log['user']) {
14✔
2519
                                $users[$log['user']] = $u->getPublic(NULL, FALSE);
14✔
2520
                            } else {
2521
                                $users[$log['user']] = User::purgedUser($log['user']);
×
2522
                            }
2523
                        }
2524

2525
                        $log['user'] = $users[$log['user']];
16✔
2526
                    }
2527

2528
                    if (Utils::pres('groupid', $log)) {
17✔
2529
                        if (!Utils::pres($log['groupid'], $groups)) {
13✔
2530
                            $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
13✔
2531

2532
                            if ($g->getId()) {
13✔
2533
                                $groups[$log['groupid']] = $g->getPublic();
13✔
2534
                                $groups[$log['groupid']]['myrole'] = $me ? $me->getRoleForGroup($log['groupid']) : User::ROLE_NONMEMBER;
13✔
2535
                            }
2536
                        }
2537

2538
                        # We can see logs for ourselves.
2539
                        if (!($myid != NULL && Utils::pres('user', $log) && Utils::presdef('id', $log['user'], NULL) == $myid) &&
13✔
2540
                            $g->getId() &&
13✔
2541
                            $groups[$log['groupid']]['myrole'] != User::ROLE_OWNER &&
13✔
2542
                            $groups[$log['groupid']]['myrole'] != User::ROLE_MODERATOR &&
13✔
2543
                            !$seeall
13✔
2544
                        ) {
2545
                            # We can only see logs for this group if we have a mod role, or if we have appropriate system
2546
                            # rights.  Skip this log.
2547
                            continue;
2✔
2548
                        }
2549

2550
                        $log['group'] = Utils::presdef($log['groupid'], $groups, NULL);
13✔
2551
                    }
2552

2553
                    if (Utils::pres('configid', $log)) {
17✔
2554
                        if (!Utils::pres($log['configid'], $configs)) {
2✔
2555
                            $c = new ModConfig($this->dbhr, $this->dbhm, $log['configid']);
2✔
2556

2557
                            if ($c->getId()) {
2✔
2558
                                $configs[$log['configid']] = $c->getPublic();
2✔
2559
                            }
2560
                        }
2561

2562
                        if (Utils::pres($log['configid'], $configs)) {
2✔
2563
                            $log['config'] = $configs[$log['configid']];
2✔
2564
                        }
2565
                    }
2566

2567
                    if (Utils::pres('stdmsgid', $log)) {
17✔
2568
                        $s = new StdMessage($this->dbhr, $this->dbhm, $log['stdmsgid']);
2✔
2569
                        $log['stdmsg'] = $s->getPublic();
2✔
2570
                    }
2571

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

2575
                        if ($m->getID()) {
8✔
2576
                            $log['message'] = $m->getPublic(FALSE);
8✔
2577

2578
                            # If we're a mod (which we must be because we're accessing logs) we need to see the
2579
                            # envelopeto, because this is displayed in MT.  No real privacy issues in that.
2580
                            $log['message']['envelopeto'] = $m->getPrivate('envelopeto');
8✔
2581
                        } else {
2582
                            # The message has been deleted.
2583
                            $log['message'] = [
1✔
2584
                                'id' => $log['msgid'],
1✔
2585
                                'deleted' => true
1✔
2586
                            ];
1✔
2587

2588
                            # See if we can find out why.
2589
                            $sql = "SELECT * FROM logs WHERE msgid = ? AND type = 'Message' AND subtype = 'Deleted' ORDER BY id DESC LIMIT 1;";
1✔
2590
                            $deletelogs = $this->dbhr->preQuery($sql, [$log['msgid']]);
1✔
2591
                            foreach ($deletelogs as $deletelog) {
1✔
2592
                                $log['message']['deletereason'] = $deletelog['text'];
1✔
2593
                            }
2594
                        }
2595

2596
                        # Prune large attributes.
2597
                        unset($log['message']['textbody']);
8✔
2598
                        unset($log['message']['htmlbody']);
8✔
2599
                        unset($log['message']['message']);
8✔
2600
                    }
2601

2602
                    $log['timestamp'] = Utils::ISODate($log['timestamp']);
17✔
2603

2604
                    $rets[$uid]['logs'][] = $log;
17✔
2605
                }
2606
            }
2607
        }
2608

2609
        # Get merge history
2610
        $merges = [];
18✔
2611
        do {
2612
            $added = FALSE;
18✔
2613
            $sql = "SELECT * FROM logs WHERE type = 'User' AND subtype = 'Merged' AND user IN (" . implode(',', $uids) . ");";
18✔
2614
            $logs = $this->dbhr->preQuery($sql);
18✔
2615
            foreach ($logs as $log) {
18✔
2616
                #error_log("Consider merge log {$log['text']}");
2617
                if (preg_match('/Merged (.*) into (.*?) \((.*)\)/', $log['text'], $matches)) {
1✔
2618
                    #error_log("Matched " . var_export($matches, TRUE));
2619
                    #error_log("Check ids {$matches[1]} and {$matches[2]}");
2620
                    foreach ([$matches[1], $matches[2]] as $id) {
1✔
2621
                        if (!in_array($id, $uids, TRUE)) {
1✔
2622
                            $added = TRUE;
1✔
2623
                            $uids[] = $id;
1✔
2624
                            $merges[] = ['timestamp' => Utils::ISODate($log['timestamp']), 'from' => $matches[1], 'to' => $matches[2], 'reason' => $matches[3]];
1✔
2625
                        }
2626
                    }
2627
                }
2628
            }
2629
        } while ($added);
18✔
2630

2631
        $merges = array_unique($merges, SORT_REGULAR);
18✔
2632

2633
        foreach ($rets as $uid => $ret) {
18✔
2634
            $rets[$uid]['merges'] = [];
18✔
2635

2636
            foreach ($merges as $merge) {
18✔
2637
                if ($merge['from'] == $ret['id'] || $merge['to'] == $ret['id']) {
1✔
2638
                    $rets[$uid]['merges'][] = $merge;
1✔
2639
                }
2640
            }
2641
        }
2642
    }
2643

2644
    public function getPublics($users, $groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [MessageCollection::APPROVED], $historyfull = FALSE)
2645
    {
2646
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
247✔
2647
        $systemrole = $me ? $me->getPrivate('systemrole') : User::SYSTEMROLE_USER;
247✔
2648
        $freeglemod = $me && $me->isFreegleMod();
247✔
2649

2650
        $rets = [];
247✔
2651

2652
        $this->getPublicAtts($rets, $users, $me);
247✔
2653
        $this->getPublicProfiles($rets, $users);
247✔
2654
        $this->getSupporters($rets, $users);
247✔
2655

2656
        if ($systemrole == User::ROLE_MODERATOR || $systemrole == User::SYSTEMROLE_ADMIN || $systemrole == User::SYSTEMROLE_SUPPORT) {
247✔
2657
            $this->getPublicEmails($rets);
93✔
2658
        }
2659

2660
        if ($history) {
247✔
2661
            $this->getPublicHistory($me, $rets, $users, $historyfull, $systemrole, $msgcoll);
113✔
2662
        }
2663

2664
        if (Session::modtools()) {
247✔
2665
            $this->getPublicMemberOf($rets, $me, $freeglemod, $memberof, $systemrole);
231✔
2666
            $this->getPublicApplied($rets, $users, $applied, $systemrole);
231✔
2667
            $this->getPublicSpammer($rets, $me, $systemrole);
231✔
2668

2669
            if ($comments) {
231✔
2670
                $this->getComments($me, $rets);
189✔
2671
            }
2672

2673
            if ($emailhistory) {
231✔
2674
                $this->getEmailHistory($rets);
3✔
2675
            }
2676
        }
2677

2678
        return ($rets);
247✔
2679
    }
2680

2681
    public function isAdmin()
2682
    {
2683
        return ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN);
13✔
2684
    }
2685

2686
    public function isAdminOrSupport()
2687
    {
2688
        return ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN || $this->user['systemrole'] == User::SYSTEMROLE_SUPPORT);
93✔
2689
    }
2690

2691
    public function isModerator()
2692
    {
2693
        return ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN ||
249✔
2694
            $this->user['systemrole'] == User::SYSTEMROLE_SUPPORT ||
249✔
2695
            $this->user['systemrole'] == User::SYSTEMROLE_MODERATOR);
249✔
2696
    }
2697

2698
    public function systemRoleMax($role1, $role2)
2699
    {
2700
        $role = User::SYSTEMROLE_USER;
14✔
2701

2702
        if ($role1 == User::SYSTEMROLE_MODERATOR || $role2 == User::SYSTEMROLE_MODERATOR) {
14✔
2703
            $role = User::SYSTEMROLE_MODERATOR;
4✔
2704
        }
2705

2706
        if ($role1 == User::SYSTEMROLE_SUPPORT || $role2 == User::SYSTEMROLE_SUPPORT) {
14✔
2707
            $role = User::SYSTEMROLE_SUPPORT;
1✔
2708
        }
2709

2710
        if ($role1 == User::SYSTEMROLE_ADMIN || $role2 == User::SYSTEMROLE_ADMIN) {
14✔
2711
            $role = User::SYSTEMROLE_ADMIN;
1✔
2712
        }
2713

2714
        return ($role);
14✔
2715
    }
2716

2717
    public function roleMax($role1, $role2)
2718
    {
2719
        $role = User::ROLE_NONMEMBER;
15✔
2720

2721
        if ($role1 == User::ROLE_MEMBER || $role2 == User::ROLE_MEMBER) {
15✔
2722
            $role = User::ROLE_MEMBER;
14✔
2723
        }
2724

2725
        if ($role1 == User::ROLE_MODERATOR || $role2 == User::ROLE_MODERATOR) {
15✔
2726
            $role = User::ROLE_MODERATOR;
8✔
2727
        }
2728

2729
        if ($role1 == User::ROLE_OWNER || $role2 == User::ROLE_OWNER) {
15✔
2730
            $role = User::ROLE_OWNER;
2✔
2731
        }
2732

2733
        return ($role);
15✔
2734
    }
2735

2736
    public function roleMin($role1, $role2)
2737
    {
2738
        $role = User::ROLE_OWNER;
6✔
2739

2740
        if ($role1 == User::ROLE_MODERATOR || $role2 == User::ROLE_MODERATOR) {
6✔
2741
            $role = User::ROLE_MODERATOR;
6✔
2742
        }
2743

2744
        if ($role1 == User::ROLE_MEMBER || $role2 == User::ROLE_MEMBER) {
6✔
2745
            $role = User::ROLE_MEMBER;
6✔
2746
        }
2747

2748
        if ($role1 == User::ROLE_NONMEMBER || $role2 == User::ROLE_NONMEMBER) {
6✔
2749
            $role = User::ROLE_NONMEMBER;
1✔
2750
        }
2751

2752
        return ($role);
6✔
2753
    }
2754

2755
    public function merge($id1, $id2, $reason, $forcemerge = FALSE)
2756
    {
2757
        error_log("Merge $id2 into $id1, $reason");
16✔
2758

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

2764
        if ($id1 != $id2 && (($u1->canMerge() && $u2->canMerge()) || ($forcemerge))) {
16✔
2765
            #
2766
            # We want to merge two users.  At present we just merge the memberships, comments, emails and logs; we don't try to
2767
            # merge any conflicting settings.
2768
            #
2769
            # Both users might have membership of the same group, including at different levels.
2770
            #
2771
            # A useful query to find foreign key references is of this form:
2772
            #
2773
            # USE information_schema; SELECT * FROM KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = 'iznik' AND REFERENCED_TABLE_NAME = 'users';
2774
            #
2775
            # We avoid too much use of quoting in preQuery/preExec because quoted numbers can't use a numeric index and therefore
2776
            # perform slowly.
2777
            #error_log("Merge $id2 into $id1");
2778
            $l = new Log($this->dbhr, $this->dbhm);
14✔
2779
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
14✔
2780

2781
            $rc = $this->dbhm->beginTransaction();
14✔
2782
            $rollback = FALSE;
14✔
2783

2784
            if ($rc) {
14✔
2785
                try {
2786
                    #error_log("Started transaction");
2787
                    $rollback = TRUE;
14✔
2788

2789
                    # Merge the top-level memberships
2790
                    $id2membs = $this->dbhr->preQuery("SELECT * FROM memberships WHERE userid = $id2;");
14✔
2791
                    foreach ($id2membs as $id2memb) {
14✔
2792
                        $rc2 = $rc;
8✔
2793
                        # Jiggery-pokery with $rc for UT purposes.
2794
                        #error_log("$id2 member of {$id2memb['groupid']} ");
2795
                        $id1membs = $this->dbhr->preQuery("SELECT * FROM memberships WHERE userid = $id1 AND groupid = {$id2memb['groupid']};");
8✔
2796

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

2802
                            #error_log("Membership UPDATE merge returned $rc2");
2803
                        } else {
2804
                            # id1 is already a member, so we really have to merge.
2805
                            #
2806
                            # Our new membership has the highest role.
2807
                            $id1memb = $id1membs[0];
7✔
2808
                            $role = User::roleMax($id1memb['role'], $id2memb['role']);
7✔
2809
                            #error_log("...as is $id1, roles {$id1memb['role']} vs {$id2memb['role']} => $role");
2810

2811
                            if ($role != $id1memb['role']) {
7✔
2812
                                $rc2 = $this->dbhm->preExec("UPDATE memberships SET role = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
2✔
2813
                                    $role
2✔
2814
                                ]);
2✔
2815
                                #error_log("Set role $rc2");
2816
                            }
2817

2818
                            if ($rc2) {
7✔
2819
                                #  Our added date should be the older of the two.
2820
                                $date = min(strtotime($id1memb['added']), strtotime($id2memb['added']));
7✔
2821
                                $mysqltime = date("Y-m-d H:i:s", $date);
7✔
2822
                                $rc2 = $this->dbhm->preExec("UPDATE memberships SET added = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
7✔
2823
                                    $mysqltime
7✔
2824
                                ]);
7✔
2825
                                #error_log("Added $rc2");
2826
                            }
2827

2828
                            # There are several attributes we want to take the non-NULL version.
2829
                            foreach (['configid', 'settings', 'heldby'] as $key) {
6✔
2830
                                #error_log("Check {$id2memb['groupid']} memb $id2 $key = " . Utils::presdef($key, $id2memb, NULL));
2831
                                if ($id2memb[$key]) {
6✔
2832
                                    if ($rc2) {
1✔
2833
                                        $rc2 = $this->dbhm->preExec("UPDATE memberships SET $key = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
1✔
2834
                                            $id2memb[$key]
1✔
2835
                                        ]);
1✔
2836
                                        #error_log("Set att $key = {$id2memb[$key]} $rc2");
2837
                                    }
2838
                                }
2839
                            }
2840
                        }
2841

2842
                        $rc = $rc2 && $rc ? $rc2 : 0;
7✔
2843
                    }
2844

2845
                    # Merge the emails.  Both might have a primary address; if so then id1 wins.
2846
                    # There is a unique index, so there can't be a conflict on email.
2847
                    if ($rc) {
13✔
2848
                        $primary = NULL;
13✔
2849
                        $foundprim = FALSE;
13✔
2850
                        $sql = "SELECT * FROM users_emails WHERE userid = $id2 AND preferred = 1;";
13✔
2851
                        $emails = $this->dbhr->preQuery($sql);
13✔
2852
                        foreach ($emails as $email) {
13✔
2853
                            $primary = $email['id'];
5✔
2854
                            $foundprim = TRUE;
5✔
2855
                        }
2856

2857
                        $sql = "SELECT * FROM users_emails WHERE userid = $id1 AND preferred = 1;";
13✔
2858
                        $emails = $this->dbhr->preQuery($sql);
13✔
2859
                        foreach ($emails as $email) {
13✔
2860
                            $primary = $email['id'];
8✔
2861
                            $foundprim = TRUE;
8✔
2862
                        }
2863

2864
                        if (!$foundprim) {
13✔
2865
                            # No primary.  Whatever we would choose for id1 should become the new one.
2866
                            $pemail = $u1->getEmailPreferred();
4✔
2867
                            $emails = $this->dbhr->preQuery("SELECT * FROM users_emails WHERE email LIKE ?;", [
4✔
2868
                                $pemail
4✔
2869
                            ]);
4✔
2870

2871
                            foreach ($emails as $email) {
4✔
2872
                                $primary = $email['id'];
4✔
2873
                            }
2874
                        }
2875

2876
                        #error_log("Merge emails");
2877
                        $sql = "UPDATE users_emails SET userid = $id1, preferred = 0 WHERE userid = $id2;";
13✔
2878
                        $rc = $this->dbhm->preExec($sql);
13✔
2879

2880
                        if ($primary) {
13✔
2881
                            $sql = "UPDATE users_emails SET preferred = 1 WHERE id = $primary;";
13✔
2882
                            $rc = $this->dbhm->preExec($sql);
13✔
2883
                        }
2884

2885
                        #error_log("Emails now " . var_export($this->dbhm->preQuery("SELECT * FROM users_emails WHERE userid = $id1;"), true));
2886
                        #error_log("Email merge returned $rc");
2887
                    }
2888

2889
                    if ($rc) {
13✔
2890
                        # Merge other foreign keys where success is less important.  For some of these there might already
2891
                        # be entries, so we do an IGNORE.
2892
                        $this->dbhm->preExec("UPDATE locations_excluded SET userid = $id1 WHERE userid = $id2;");
13✔
2893
                        $this->dbhm->preExec("UPDATE IGNORE chat_roster SET userid = $id1 WHERE userid = $id2;");
13✔
2894
                        $this->dbhm->preExec("UPDATE IGNORE sessions SET userid = $id1 WHERE userid = $id2;");
13✔
2895
                        $this->dbhm->preExec("UPDATE IGNORE spam_users SET userid = $id1 WHERE userid = $id2;");
13✔
2896
                        $this->dbhm->preExec("UPDATE IGNORE spam_users SET byuserid = $id1 WHERE byuserid = $id2;");
13✔
2897
                        $this->dbhm->preExec("UPDATE IGNORE users_addresses SET userid = $id1 WHERE userid = $id2;");
13✔
2898
                        $this->dbhm->preExec("UPDATE users_comments SET userid = $id1 WHERE userid = $id2;");
13✔
2899
                        $this->dbhm->preExec("UPDATE users_comments SET byuserid = $id1 WHERE byuserid = $id2;");
13✔
2900
                        $this->dbhm->preExec("UPDATE IGNORE users_donations SET userid = $id1 WHERE userid = $id2;");
13✔
2901
                        $this->dbhm->preExec("UPDATE IGNORE users_images SET userid = $id1 WHERE userid = $id2;");
13✔
2902
                        $this->dbhm->preExec("UPDATE IGNORE users_invitations SET userid = $id1 WHERE userid = $id2;");
13✔
2903
                        $this->dbhm->preExec("UPDATE users_logins SET userid = $id1 WHERE userid = $id2;");
13✔
2904
                        $this->dbhm->preExec("UPDATE IGNORE users_logins SET uid = $id1 WHERE userid = $id1 AND `type` = ?;", [
13✔
2905
                            User::LOGIN_NATIVE
13✔
2906
                        ]);
13✔
2907
                        $this->dbhm->preExec("UPDATE IGNORE users_nearby SET userid = $id1 WHERE userid = $id2;");
13✔
2908
                        $this->dbhm->preExec("UPDATE IGNORE users_notifications SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
2909
                        $this->dbhm->preExec("UPDATE IGNORE users_notifications SET touser = $id1 WHERE touser = $id2;");
13✔
2910
                        $this->dbhm->preExec("UPDATE IGNORE users_nudges SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
2911
                        $this->dbhm->preExec("UPDATE IGNORE users_nudges SET touser = $id1 WHERE touser = $id2;");
13✔
2912
                        $this->dbhm->preExec("UPDATE IGNORE users_phones SET userid = $id1 WHERE userid = $id2;");
13✔
2913
                        $this->dbhm->preExec("UPDATE IGNORE users_push_notifications SET userid = $id1 WHERE userid = $id2;");
13✔
2914
                        $this->dbhm->preExec("UPDATE IGNORE users_requests SET userid = $id1 WHERE userid = $id2;");
13✔
2915
                        $this->dbhm->preExec("UPDATE IGNORE users_requests SET completedby = $id1 WHERE completedby = $id2;");
13✔
2916
                        $this->dbhm->preExec("UPDATE IGNORE users_searches SET userid = $id1 WHERE userid = $id2;");
13✔
2917
                        $this->dbhm->preExec("UPDATE IGNORE newsfeed SET userid = $id1 WHERE userid = $id2;");
13✔
2918
                        $this->dbhm->preExec("UPDATE IGNORE messages_reneged SET userid = $id1 WHERE userid = $id2;");
13✔
2919
                        $this->dbhm->preExec("UPDATE IGNORE users_stories SET userid = $id1 WHERE userid = $id2;");
13✔
2920
                        $this->dbhm->preExec("UPDATE IGNORE users_stories_likes SET userid = $id1 WHERE userid = $id2;");
13✔
2921
                        $this->dbhm->preExec("UPDATE IGNORE users_stories_requested SET userid = $id1 WHERE userid = $id2;");
13✔
2922
                        $this->dbhm->preExec("UPDATE IGNORE users_thanks SET userid = $id1 WHERE userid = $id2;");
13✔
2923
                        $this->dbhm->preExec("UPDATE IGNORE modnotifs SET userid = $id1 WHERE userid = $id2;");
13✔
2924
                        $this->dbhm->preExec("UPDATE IGNORE teams_members SET userid = $id1 WHERE userid = $id2;");
13✔
2925
                        $this->dbhm->preExec("UPDATE IGNORE users_aboutme SET userid = $id1 WHERE userid = $id2;");
13✔
2926
                        $this->dbhm->preExec("UPDATE IGNORE ratings SET rater = $id1 WHERE rater = $id2;");
13✔
2927
                        $this->dbhm->preExec("UPDATE IGNORE ratings SET ratee = $id1 WHERE ratee = $id2;");
13✔
2928
                        $this->dbhm->preExec("UPDATE IGNORE users_replytime SET userid = $id1 WHERE userid = $id2;");
13✔
2929
                        $this->dbhm->preExec("UPDATE IGNORE messages_promises SET userid = $id1 WHERE userid = $id2;");
13✔
2930
                        $this->dbhm->preExec("UPDATE IGNORE messages_by SET userid = $id1 WHERE userid = $id2;");
13✔
2931
                        $this->dbhm->preExec("UPDATE IGNORE trysts SET user1 = $id1 WHERE user1 = $id2;");
13✔
2932
                        $this->dbhm->preExec("UPDATE IGNORE trysts SET user2 = $id1 WHERE user2 = $id2;");
13✔
2933
                        $this->dbhm->preExec("UPDATE IGNORE isochrones_users SET userid = $id1 WHERE userid = $id2;");
13✔
2934
                        $this->dbhm->preExec("UPDATE IGNORE microactions SET userid = $id1 WHERE userid = $id2;");
13✔
2935

2936
                        # Handle the bans.
2937
                        $this->dbhm->preExec("UPDATE IGNORE users_banned SET userid = $id1 WHERE userid = $id2;");
13✔
2938
                        $this->dbhm->preExec("UPDATE IGNORE users_banned SET byuser = $id1 WHERE byuser = $id2;");
13✔
2939

2940

2941
                        $bans = $this->dbhm->preQuery("SELECT * FROM users_banned WHERE userid = $id1");
13✔
2942
                        foreach ($bans as $ban) {
13✔
2943
                            # Make sure we are not a member; this could happen if one of the users is banned and
2944
                            # the other is not.
2945
                            $this->dbhm->preExec("DELETE FROM memberships WHERE userid = ? AND groupid = ?", [
1✔
2946
                                $id1,
1✔
2947
                                $ban['groupid']
1✔
2948
                            ]);
1✔
2949
                        }
2950

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

2958
                        foreach ($rooms as $room) {
13✔
2959
                            # Now see if there is already a chat room between the destination user and whatever this
2960
                            # one is.
2961
                            switch ($room['chattype']) {
1✔
2962
                                case ChatRoom::TYPE_USER2MOD;
1✔
2963
                                    $sql = "SELECT id FROM chat_rooms WHERE user1 = $id1 AND groupid = {$room['groupid']};";
1✔
2964
                                    break;
1✔
2965
                                case ChatRoom::TYPE_USER2USER;
1✔
2966
                                    $other = $room['user1'] == $id2 ? $room['user2'] : $room['user1'];
1✔
2967
                                    $sql = "SELECT id FROM chat_rooms WHERE (user1 = $id1 AND user2 = $other) OR (user2 = $id1 AND user1 = $other);";
1✔
2968
                                    break;
1✔
2969
                            }
2970

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

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

2978
                                # Make sure latestmessage is set correctly.
2979
                                $this->dbhm->preExec("UPDATE chat_rooms SET latestmessage = GREATEST(latestmessage, ?) WHERE id = ?", [
1✔
2980
                                    $room['latestmessage'],
1✔
2981
                                    $alreadys[0]['id']
1✔
2982
                                ]);
1✔
2983
                            } else {
2984
                                # No, there isn't, so we can update our old one.
2985
                                $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✔
2986
                                $this->dbhm->preExec($sql);
1✔
2987
                            }
2988
                        }
2989

2990
                        $this->dbhm->preExec("UPDATE chat_messages SET userid = $id1 WHERE userid = $id2;");
13✔
2991
                    }
2992

2993
                    # Merge attributes we want to keep if we have them in id2 but not id1.  Some will have unique
2994
                    # keys, so update to delete them.
2995
                    foreach (['fullname', 'firstname', 'lastname', 'yahooid'] as $att) {
13✔
2996
                        $users = $this->dbhm->preQuery("SELECT $att FROM users WHERE id = $id2;");
13✔
2997
                        foreach ($users as $user) {
13✔
2998
                            $this->dbhm->preExec("UPDATE users SET $att = NULL WHERE id = $id2;");
13✔
2999
                            User::clearCache($id1);
13✔
3000
                            User::clearCache($id2);
13✔
3001

3002
                            if (!$u1->getPrivate($att)) {
13✔
3003
                                if ($att != 'fullname') {
13✔
3004
                                    $this->dbhm->preExec("UPDATE users SET $att = ? WHERE id = $id1 AND $att IS NULL;", [$user[$att]]);
13✔
3005
                                } else if (stripos($user[$att], 'fbuser') === FALSE && stripos($user[$att], '-owner') === FALSE) {
4✔
3006
                                    # We don't want to overwrite a name with FBUser or a -owner address.
3007
                                    $this->dbhm->preExec("UPDATE users SET $att = ? WHERE id = $id1;", [$user[$att]]);
4✔
3008
                                }
3009
                            }
3010
                        }
3011
                    }
3012

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

3017
                        #error_log("Log merge 1 returned $rc");
3018
                    }
3019

3020
                    if ($rc) {
13✔
3021
                        $rc = $this->dbhm->preExec("UPDATE logs SET byuser = $id1 WHERE byuser = $id2;");
13✔
3022

3023
                        #error_log("Log merge 2 returned $rc");
3024
                    }
3025

3026
                    # Merge the fromuser in messages.  There might not be any, and it's not the end of the world
3027
                    # if this info isn't correct, so ignore the rc.
3028
                    #error_log("Merge messages, current rc $rc");
3029
                    if ($rc) {
13✔
3030
                        $this->dbhm->preExec("UPDATE messages SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
3031
                    }
3032

3033
                    # Merge the history
3034
                    #error_log("Merge history, current rc $rc");
3035
                    if ($rc) {
13✔
3036
                        $this->dbhm->preExec("UPDATE messages_history SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
3037
                        $this->dbhm->preExec("UPDATE memberships_history SET userid = $id1 WHERE userid = $id2;");
13✔
3038
                    }
3039

3040
                    # Merge the systemrole.
3041
                    $u1s = $this->dbhr->preQuery("SELECT systemrole FROM users WHERE id = $id1;");
13✔
3042
                    foreach ($u1s as $u1) {
13✔
3043
                        $u2s = $this->dbhr->preQuery("SELECT systemrole FROM users WHERE id = $id2;");
13✔
3044
                        foreach ($u2s as $u2) {
13✔
3045
                            $rc = $this->dbhm->preExec("UPDATE users SET systemrole = ? WHERE id = $id1;", [
13✔
3046
                                $this->systemRoleMax($u1['systemrole'], $u2['systemrole'])
13✔
3047
                            ]);
13✔
3048
                        }
3049
                        User::clearCache($id1);
13✔
3050
                    }
3051

3052
                    # Merge the add date.
3053
                    $u1 = User::get($this->dbhr, $this->dbhm, $id1);
13✔
3054
                    $u2 = User::get($this->dbhr, $this->dbhm, $id2);
13✔
3055
                    $this->dbhm->preExec("UPDATE users SET added = ? WHERE id = $id1;", [
13✔
3056
                        strtotime($u1->getPrivate('added')) < strtotime($u2->getPrivate('added')) ? $u1->getPrivate('added') : $u2->getPrivate('added')
13✔
3057
                    ]);
13✔
3058

3059
                    $this->dbhm->preExec("UPDATE users SET lastupdated = NOW() WHERE id = ?;", [
13✔
3060
                        $id1
13✔
3061
                    ]);
13✔
3062

3063
                    $tnid1 = $u2->getPrivate('tnuserid');
13✔
3064
                    $tnid2 = $u2->getPrivate('tnuserid');
13✔
3065

3066
                    if (!$tnid1 && $tnid2) {
13✔
3067
                        $u2->setPrivate('tnuserid', NULL);
×
3068
                        $u1->setPrivate('tnuserid', $tnid2);
×
3069
                    }
3070

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

3075
                    if (count($giftaids)) {
13✔
3076
                        $weights = [
1✔
3077
                            Donations::PERIOD_PAST_4_YEARS_AND_FUTURE => 0,
1✔
3078
                            Donations::PERIOD_SINCE => 1,
1✔
3079
                            Donations::PERIOD_FUTURE=> 2,
1✔
3080
                            Donations::PERIOD_THIS => 3,
1✔
3081
                            Donations::PERIOD_DECLINED => 4
1✔
3082
                        ];
1✔
3083

3084
                        $best = NULL;
1✔
3085
                        foreach ($giftaids as $giftaid) {
1✔
3086
                            if ($best == NULL ||
1✔
3087
                                $weights[$giftaid['period']] < $weights[$best['period']]) {
1✔
3088
                                $best = $giftaid;
1✔
3089
                            }
3090
                        }
3091

3092
                        foreach ($giftaids as $giftaid) {
1✔
3093
                            if ($giftaid['id'] != $best['id']) {
1✔
3094
                                $this->dbhm->preExec("DELETE FROM giftaid WHERE id = ?;", [
1✔
3095
                                    $giftaid['id']
1✔
3096
                                ]);
1✔
3097
                            }
3098
                        }
3099

3100
                        $this->dbhm->preExec("UPDATE giftaid SET userid = ? WHERE id = ?;", [
1✔
3101
                            $id1,
1✔
3102
                            $best['id']
1✔
3103
                        ]);
1✔
3104
                    }
3105

3106
                    if ($rc) {
13✔
3107
                        # Log the merge - before the delete otherwise we will fail to log it.
3108
                        $l->log([
13✔
3109
                            'type' => Log::TYPE_USER,
13✔
3110
                            'subtype' => Log::SUBTYPE_MERGED,
13✔
3111
                            'user' => $id2,
13✔
3112
                            'byuser' => $me ? $me->getId() : NULL,
13✔
3113
                            'text' => "Merged $id2 into $id1 ($reason)"
13✔
3114
                        ]);
13✔
3115

3116
                        # Log under both users to make sure we can trace it.
3117
                        $l->log([
13✔
3118
                            'type' => Log::TYPE_USER,
13✔
3119
                            'subtype' => Log::SUBTYPE_MERGED,
13✔
3120
                            'user' => $id1,
13✔
3121
                            'byuser' => $me ? $me->getId() : NULL,
13✔
3122
                            'text' => "Merged $id2 into $id1 ($reason)"
13✔
3123
                        ]);
13✔
3124
                    }
3125

3126
                    if ($rc) {
13✔
3127
                        # Everything worked.
3128
                        $rollback = FALSE;
13✔
3129

3130
                        # We might have merged ourself!
3131
                        if (Utils::pres('id', $_SESSION) == $id2) {
13✔
3132
                            $_SESSION['id'] = $id1;
13✔
3133
                        }
3134
                    }
3135
                } catch (\Exception $e) {
1✔
3136
                    error_log("Merge exception " . $e->getMessage());
1✔
3137
                    $rollback = TRUE;
1✔
3138
                }
3139
            }
3140

3141
            if ($rollback) {
14✔
3142
                # Something went wrong.
3143
                #error_log("Merge failed, rollback");
3144
                $this->dbhm->rollBack();
1✔
3145
                $ret = FALSE;
1✔
3146
            } else {
3147
                #error_log("Merge worked, commit");
3148
                $ret = $this->dbhm->commit();
13✔
3149

3150
                if ($ret) {
13✔
3151
                    # Finally, delete id2.  We used to this inside the transaction, but the result was that
3152
                    # fromuser sometimes got set to NULL on messages owned by id2, despite them having been set to
3153
                    # id1 earlier on.  Either we're dumb, or there's a subtle interaction between transactions,
3154
                    # foreign keys and Percona clusters.  This is safer and proves to be more reliable.
3155
                    #
3156
                    # Make sure we don't pick up an old cached version, as we've just changed it quite a bit.
3157
                    error_log("Merged $id1 < $id2, $reason");
13✔
3158
                    $deleteme = new User($this->dbhm, $this->dbhm, $id2);
13✔
3159
                    $rc = $deleteme->delete(NULL, NULL, NULL, FALSE);
13✔
3160
                }
3161
            }
3162
        }
3163

3164
        return ($ret);
16✔
3165
    }
3166

3167
    public function mailer($user, $modmail, $toname, $to, $bcc, $fromname, $from, $subject, $text) {
3168
        try {
3169
            #error_log(session_id() . " mail " . microtime(true));
3170

3171
            list ($transport, $mailer) = Mail::getMailer();
4✔
3172

3173
            $message = \Swift_Message::newInstance()
4✔
3174
                ->setSubject($subject)
4✔
3175
                ->setFrom([$from => $fromname])
4✔
3176
                ->setTo([$to => $toname])
4✔
3177
                ->setBody($text);
4✔
3178

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

3182
            if ($user) {
3✔
3183
                $headers->addTextHeader('X-Iznik-From-User', $user->getId());
3✔
3184
            }
3185

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

3188
            if ($bcc) {
3✔
3189
                $message->setBcc(explode(',', $bcc));
1✔
3190
            }
3191

3192
            Mail::addHeaders($this->dbhr, $this->dbhm, $message,Mail::MODMAIL, $user->getId());
3✔
3193

3194
            $this->sendIt($mailer, $message);
3✔
3195

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

3199
            #error_log(session_id() . " mailed " . microtime(true));
3200
        } catch (\Exception $e) {
4✔
3201
            # Not much we can do - shouldn't really happen given the failover transport.
3202
            // @codeCoverageIgnoreStart
3203
            error_log("Send failed with " . $e->getMessage());
3204
            // @codeCoverageIgnoreEnd
3205
        }
3206
    }
3207

3208
    private function maybeMail($groupid, $subject, $body, $action)
3209
    {
3210
        if ($body) {
4✔
3211
            # We have a mail to send.
3212
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3213
            $myid = $me->getId();
4✔
3214

3215
            $g = Group::get($this->dbhr, $this->dbhm, $groupid);
4✔
3216
            $atts = $g->getPublic();
4✔
3217

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

3220
            # Find who to send it from.  If we have a config to use for this group then it will tell us.
3221
            $name = $me->getName();
4✔
3222
            $c = new ModConfig($this->dbhr, $this->dbhm);
4✔
3223
            $cid = $c->getForGroup($me->getId(), $groupid);
4✔
3224
            $c = new ModConfig($this->dbhr, $this->dbhm, $cid);
4✔
3225
            $fromname = $c->getPrivate('fromname');
4✔
3226
            $name = ($fromname == 'Groupname Moderator') ? '$groupname Moderator' : $name;
4✔
3227

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

3231
            $bcc = $c->getBcc($action);
4✔
3232

3233
            if ($bcc) {
4✔
3234
                $bcc = str_replace('$groupname', $atts['nameshort'], $bcc);
1✔
3235
            }
3236

3237
            # We add the message into chat.
3238
            $r = new ChatRoom($this->dbhr, $this->dbhm);
4✔
3239
            $rid = $r->createUser2Mod($this->id, $groupid);
4✔
3240
            $m = NULL;
4✔
3241

3242
            $to = $this->getEmailPreferred();
4✔
3243

3244
            if ($rid) {
4✔
3245
                # Create the message.  Mark it as needing review to prevent timing window.
3246
                $m = new ChatMessage($this->dbhr, $this->dbhm);
4✔
3247
                list ($mid, $banned) = $m->create($rid,
4✔
3248
                    $myid,
4✔
3249
                    "$subject\r\n\r\n$body",
4✔
3250
                    ChatMessage::TYPE_MODMAIL,
4✔
3251
                    NULL,
4✔
3252
                    TRUE,
4✔
3253
                    NULL,
4✔
3254
                    NULL,
4✔
3255
                    NULL,
4✔
3256
                    NULL,
4✔
3257
                    NULL,
4✔
3258
                    TRUE,
4✔
3259
                    TRUE);
4✔
3260

3261
                $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✔
3262
            }
3263

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

3270
                # We've mailed the message out so they are up to date with this chat.
3271
                $r->upToDate($this->id);
3✔
3272
            }
3273

3274
            if ($m) {
4✔
3275
                # We, as a mod, have seen this message - update the roster to show that.  This avoids this message
3276
                # appearing as unread to us.
3277
                $r->updateRoster($myid, $mid);
4✔
3278

3279
                # Ensure that the other mods are present in the roster with the message seen/unseen depending on
3280
                # whether that's what we want.
3281
                $mods = $g->getMods();
4✔
3282
                foreach ($mods as $mod) {
4✔
3283
                    if ($mod != $myid) {
3✔
3284
                        if ($c->getPrivate('chatread')) {
2✔
3285
                            # We want to mark it as seen for all mods.
3286
                            $r->updateRoster($mod, $mid, ChatRoom::STATUS_AWAY);
1✔
3287
                        } else {
3288
                            # Leave it unseen, but make sure they're in the roster.
3289
                            $r->updateRoster($mod, NULL, ChatRoom::STATUS_AWAY);
1✔
3290
                        }
3291
                    }
3292
                }
3293

3294
                if ($c->getPrivate('chatread')) {
4✔
3295
                    $m->setPrivate('mailedtoall', 1);
1✔
3296
                    $m->setPrivate('seenbyall', 1);
1✔
3297
                }
3298

3299
                # Allow mailing to happen.
3300
                $m->setPrivate('reviewrequired', 0);
4✔
3301
            }
3302
        }
3303
    }
3304

3305
    public function mail($groupid, $subject, $body, $stdmsgid, $action = NULL)
3306
    {
3307
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3308

3309
        $this->log->log([
4✔
3310
            'type' => Log::TYPE_USER,
4✔
3311
            'subtype' => Log::SUBTYPE_MAILED,
4✔
3312
            'user' => $this->id,
4✔
3313
            'byuser' => $me ? $me->getId() : NULL,
4✔
3314
            'text' => $subject,
4✔
3315
            'groupid' => $groupid,
4✔
3316
            'stdmsgid' => $stdmsgid
4✔
3317
        ]);
4✔
3318

3319
        $this->maybeMail($groupid, $subject, $body, $action);
4✔
3320
    }
3321

3322
    public function happinessReviewed($happinessid) {
3323
        $this->dbhm->preExec("UPDATE messages_outcomes SET reviewed = 1 WHERE id = ?", [
1✔
3324
            $happinessid
1✔
3325
        ]);
1✔
3326
    }
3327

3328
    public function getCommentsForSingleUser($userid) {
3329
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3330
        $rets = [
4✔
3331
            $userid => [
4✔
3332
                'id' => $userid
4✔
3333
            ]
4✔
3334
        ];
4✔
3335

3336
        $this->getComments($me, $rets);
4✔
3337

3338
        return Utils::presdef('comments', $rets[$userid], NULL);
4✔
3339
    }
3340

3341
    public function getComments($me, &$rets)
3342
    {
3343
        $userids = array_keys($rets);
191✔
3344

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

3353
            $commentuids = [];
76✔
3354
            foreach ($comments as $comment) {
76✔
3355
                if (Utils::pres('byuserid', $comment)) {
2✔
3356
                    $commentuids[] = $comment['byuserid'];
2✔
3357
                }
3358
            }
3359

3360
            $commentusers = [];
76✔
3361

3362
            if ($commentuids && count($commentuids)) {
76✔
3363
                $commentusers = $this->getPublicsById($commentuids, NULL, FALSE, FALSE, FALSE, FALSE);
2✔
3364

3365
                foreach ($commentusers as &$commentuser) {
2✔
3366
                    $commentuser['settings'] = NULL;
2✔
3367
                }
3368
            }
3369

3370
            foreach ($rets as $retind => $ret) {
76✔
3371
                $rets[$retind]['comments'] = [];
76✔
3372

3373
                for ($commentind = 0; $commentind < count($comments); $commentind++) {
76✔
3374
                    if ($comments[$commentind]['userid'] == $rets[$retind]['id']) {
2✔
3375
                        $comments[$commentind]['date'] = Utils::ISODate($comments[$commentind]['date']);
2✔
3376
                        $comments[$commentind]['reviewed'] = Utils::ISODate($comments[$commentind]['reviewed']);
2✔
3377

3378
                        if (Utils::pres('byuserid', $comments[$commentind])) {
2✔
3379
                            $comments[$commentind]['byuser'] = $commentusers[$comments[$commentind]['byuserid']];
2✔
3380
                        }
3381

3382
                        $rets[$retind]['comments'][] = $comments[$commentind];
2✔
3383
                    }
3384
                }
3385
            }
3386
        }
3387
    }
3388

3389
    public function listComments(&$ctx, $groupid = NULL) {
3390
        $comments = [];
1✔
3391
        $ctxq = '';
1✔
3392

3393
        if ($ctx && Utils::pres('reviewed', $ctx)) {
1✔
3394
            $ctxq = "users_comments.reviewed < " . $this->dbhr->quote($ctx['reviewed']) . " AND ";
1✔
3395
        }
3396

3397
        $groupq = $groupid ? " groupid = $groupid AND " : '';
1✔
3398

3399
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
3400
        $groupids = $me ? $me->getModeratorships() : [];
1✔
3401

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

3407
            $uids = array_unique(array_merge(array_column($comments, 'byuserid'), array_column($comments, 'userid')));
1✔
3408
            $u = new User($this->dbhr, $this->dbhm);
1✔
3409
            $users = $u->getPublicsById($uids, NULL, FALSE, FALSE, FALSE, FALSE);
1✔
3410

3411
            foreach ($comments as &$comment) {
1✔
3412
                $comment['date'] = Utils::ISODate($comment['date']);
1✔
3413
                $comment['reviewed'] = Utils::ISODate($comment['reviewed']);
1✔
3414

3415
                if (Utils::pres('userid', $comment)) {
1✔
3416
                    $comment['user'] = $users[$comment['userid']];
1✔
3417
                    unset($comment['userid']);
1✔
3418
                }
3419

3420
                if (Utils::pres('byuserid', $comment)) {
1✔
3421
                    $comment['byuser'] = $users[$comment['byuserid']];
1✔
3422
                    unset($comment['byuserid']);
1✔
3423
                }
3424

3425
                $ctx['reviewed'] = $comment['reviewed'];
1✔
3426
            }
3427
        }
3428

3429
        return $comments;
1✔
3430
    }
3431

3432
    public function getComment($id)
3433
    {
3434
        # We can only see comments on groups on which we have mod status.
3435
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3436
        $groupids = $me ? $me->getModeratorships() : [];
2✔
3437
        $groupids = count($groupids) == 0 ? [0] : $groupids;
2✔
3438

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

3442
        foreach ($comments as &$comment) {
2✔
3443
            $comment['date'] = Utils::ISODate($comment['date']);
2✔
3444
            $comment['reviewed'] = Utils::ISODate($comment['reviewed']);
2✔
3445

3446
            if (Utils::pres('byuserid', $comment)) {
2✔
3447
                $u = User::get($this->dbhr, $this->dbhm, $comment['byuserid']);
2✔
3448
                $comment['byuser'] = $u->getPublic();
2✔
3449
            }
3450

3451
            return ($comment);
2✔
3452
        }
3453

3454
        return (NULL);
1✔
3455
    }
3456

3457
    public function addComment($groupid, $user1 = NULL, $user2 = NULL, $user3 = NULL, $user4 = NULL, $user5 = NULL,
3458
                               $user6 = NULL, $user7 = NULL, $user8 = NULL, $user9 = NULL, $user10 = NULL,
3459
                               $user11 = NULL, $byuserid = NULL, $checkperms = TRUE, $flag = FALSE)
3460
    {
3461
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
7✔
3462

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

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

3471
        foreach ($groups as $modgroupid) {
7✔
3472
            if ($groupid == $modgroupid) {
7✔
3473
                $sql = "INSERT INTO users_comments (userid, groupid, byuserid, user1, user2, user3, user4, user5, user6, user7, user8, user9, user10, user11, flag) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
6✔
3474
                $this->dbhm->preExec($sql, [
6✔
3475
                    $this->id,
6✔
3476
                    $groupid,
6✔
3477
                    $byuserid,
6✔
3478
                    $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
6✔
3479
                    $flag ? 1 : 0
6✔
3480
                ]);
6✔
3481

3482
                $rc = $this->dbhm->lastInsertId();
6✔
3483

3484
                $added = TRUE;
6✔
3485
            }
3486
        }
3487

3488
        if (!$added && $me && $me->isAdminOrSupport()) {
7✔
3489
            $sql = "INSERT INTO users_comments (userid, groupid, byuserid, user1, user2, user3, user4, user5, user6, user7, user8, user9, user10, user11, flag) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
1✔
3490
            $this->dbhm->preExec($sql, [
1✔
3491
                $this->id,
1✔
3492
                NULL,
1✔
3493
                $byuserid,
1✔
3494
                $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
1✔
3495
                $flag ? 1 : 0
1✔
3496
            ]);
1✔
3497

3498
            $rc = $this->dbhm->lastInsertId();
1✔
3499
        }
3500

3501
        if ($rc && $flag) {
7✔
3502
            $this->flagOthers($groupid);
1✔
3503
        }
3504

3505
        return ($rc);
7✔
3506
    }
3507

3508
    private function flagOthers($groupid) {
3509
        # We want to flag this to any other groups that the member is on.
3510
        $membs = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ? AND groupid != ?;", [
2✔
3511
            $this->id,
2✔
3512
            $groupid
2✔
3513
        ]);
2✔
3514

3515
        foreach ($membs as $memb) {
2✔
3516
            $this->memberReview($memb['groupid'], TRUE, 'Note flagged to other groups');
2✔
3517
        }
3518
    }
3519

3520
    public function editComment($id, $user1 = NULL, $user2 = NULL, $user3 = NULL, $user4 = NULL, $user5 = NULL,
3521
                                $user6 = NULL, $user7 = NULL, $user8 = NULL, $user9 = NULL, $user10 = NULL,
3522
                                $user11 = NULL, $flag = FALSE)
3523
    {
3524
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
3525

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

3529
        # Can only edit comments for a group on which we're a mod.  This code isn't that efficient but it doesn't
3530
        # happen often.
3531
        $rc = NULL;
3✔
3532
        $comments = $this->dbhr->preQuery("SELECT id, groupid FROM users_comments WHERE id = ?;", [
3✔
3533
            $id
3✔
3534
        ]);
3✔
3535

3536
        foreach ($comments as $comment) {
3✔
3537
            if ($me && ($me->isAdminOrSupport() || $me->isModOrOwner($comment['groupid']))) {
3✔
3538
                $sql = "UPDATE users_comments SET byuserid = ?, user1 = ?, user2 = ?, user3 = ?, user4 = ?, user5 = ?, user6 = ?, user7 = ?, user8 = ?, user9 = ?, user10 = ?, user11 = ?, reviewed = NOW(), flag = ? WHERE id = ?;";
3✔
3539
                $rc = $this->dbhm->preExec($sql, [
3✔
3540
                    $byuserid,
3✔
3541
                    $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
3✔
3542
                    $flag,
3✔
3543
                    $comment['id']
3✔
3544
                ]);
3✔
3545

3546
                if ($rc && $flag) {
3✔
3547
                    $this->flagOthers($comment['groupid']);
1✔
3548
                }
3549
            }
3550
        }
3551

3552
        return ($rc);
3✔
3553
    }
3554

3555
    public function deleteComment($id)
3556
    {
3557
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3558

3559
        # Can only delete comments for a group on which we're a mod.
3560
        $rc = FALSE;
2✔
3561

3562
        $comments = $this->dbhr->preQuery("SELECT id, groupid FROM users_comments WHERE id = ?;", [
2✔
3563
            $id
2✔
3564
        ]);
2✔
3565

3566
        foreach ($comments as $comment) {
2✔
3567
            if ($me && ($me->isAdminOrSupport() || $me->isModOrOwner($comment['groupid']))) {
2✔
3568
                $rc = $this->dbhm->preExec("DELETE FROM users_comments WHERE id = ?;", [$id]);
2✔
3569
            }
3570
        }
3571

3572
        return ($rc);
2✔
3573
    }
3574

3575
    public function deleteComments()
3576
    {
3577
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
3578

3579
        # Can only delete comments for a group on which we're a mod.
3580
        $rc = FALSE;
1✔
3581
        $groups = $me ? $me->getModeratorships() : [];
1✔
3582
        foreach ($groups as $modgroupid) {
1✔
3583
            $rc = $this->dbhm->preExec("DELETE FROM users_comments WHERE userid = ? AND groupid = ?;", [$this->id, $modgroupid]);
1✔
3584
        }
3585

3586
        return ($rc);
1✔
3587
    }
3588

3589
    public function split($email, $name = NULL)
3590
    {
3591
        # We want to ensure that the current user has no reference to these values.
3592
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3593
        $l = new Log($this->dbhr, $this->dbhm);
2✔
3594
        if ($email) {
2✔
3595
            $this->removeEmail($email);
2✔
3596
        }
3597

3598
        $l->log([
2✔
3599
            'type' => Log::TYPE_USER,
2✔
3600
            'subtype' => Log::SUBTYPE_SPLIT,
2✔
3601
            'user' => $this->id,
2✔
3602
            'byuser' => $me ? $me->getId() : NULL,
2✔
3603
            'text' => "Split out $email"
2✔
3604
        ]);
2✔
3605

3606
        $u = new User($this->dbhr, $this->dbhm);
2✔
3607
        $uid2 = $u->create(NULL, NULL, $name);
2✔
3608
        $u->addEmail($email);
2✔
3609

3610
        # We might be able to move some messages over.
3611
        $this->dbhm->preExec("UPDATE messages SET fromuser = ? WHERE fromaddr = ?;", [
2✔
3612
            $uid2,
2✔
3613
            $email
2✔
3614
        ]);
2✔
3615
        $this->dbhm->preExec("UPDATE messages_history SET fromuser = ? WHERE fromaddr = ?;", [
2✔
3616
            $uid2,
2✔
3617
            $email
2✔
3618
        ]);
2✔
3619

3620
        # Chats which reference the messages sent from that email must also be intended for the split user.
3621
        $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✔
3622
            $email
2✔
3623
        ]);
2✔
3624

3625
        foreach ($chats as $chat) {
2✔
3626
            if ($chat['user1'] == $this->id) {
1✔
3627
                $this->dbhm->preExec("UPDATE chat_rooms SET user1 = ? WHERE id = ?;", [
1✔
3628
                    $uid2,
1✔
3629
                    $chat['id']
1✔
3630
                ]);
1✔
3631
            }
3632

3633
            if ($chat['user2'] == $this->id) {
1✔
3634
                $this->dbhm->preExec("UPDATE chat_rooms SET user2 = ? WHERE id = ?;", [
1✔
3635
                    $uid2,
1✔
3636
                    $chat['id']
1✔
3637
                ]);
1✔
3638
            }
3639
        }
3640

3641
        # We might have a name.
3642
        $this->dbhm->preExec("UPDATE users SET fullname = (SELECT fromname FROM messages WHERE fromaddr = ? LIMIT 1) WHERE id = ?;", [
2✔
3643
            $email,
2✔
3644
            $uid2
2✔
3645
        ]);
2✔
3646

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

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

3655
        return ($uid2);
2✔
3656
    }
3657

3658
    public function welcome($email, $password)
3659
    {
3660
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
5✔
3661
        $twig = new \Twig_Environment($loader);
5✔
3662

3663
        $html = $twig->render('welcome/welcome.html', [
5✔
3664
            'email' => $email,
5✔
3665
            'password' => $password
5✔
3666
        ]);
5✔
3667

3668
        $message = \Swift_Message::newInstance()
5✔
3669
            ->setSubject("Welcome to " . SITE_NAME . "!")
5✔
3670
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
5✔
3671
            ->setTo($email)
5✔
3672
            ->setBody("Thanks for joining" . SITE_NAME . "!" . ($password ? "  Here's your password: $password." : ''));
5✔
3673

3674
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3675
        # Outlook.
3676
        $htmlPart = \Swift_MimePart::newInstance();
5✔
3677
        $htmlPart->setCharset('utf-8');
5✔
3678
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
5✔
3679
        $htmlPart->setContentType('text/html');
5✔
3680
        $htmlPart->setBody($html);
5✔
3681
        $message->attach($htmlPart);
5✔
3682

3683
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::WELCOME, $this->getId());
5✔
3684

3685
        list ($transport, $mailer) = Mail::getMailer();
5✔
3686
        $this->sendIt($mailer, $message);
5✔
3687
    }
3688

3689
    public function FBL()
3690
    {
3691
        $settings = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FBL, TRUE);
1✔
3692
        $unsubscribe = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FBL, TRUE);
1✔
3693

3694
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
1✔
3695
        $twig = new \Twig_Environment($loader);
1✔
3696

3697
        $html = $twig->render('fbl.html', [
1✔
3698
            'email' => $this->getEmailPreferred(),
1✔
3699
            'unsubscribe' => $unsubscribe,
1✔
3700
            'settings' => $settings
1✔
3701
        ]);
1✔
3702

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

3710
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3711
        # Outlook.
3712
        $htmlPart = \Swift_MimePart::newInstance();
1✔
3713
        $htmlPart->setCharset('utf-8');
1✔
3714
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
3715
        $htmlPart->setContentType('text/html');
1✔
3716
        $htmlPart->setBody($html);
1✔
3717
        $message->attach($htmlPart);
1✔
3718

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

3721
        list ($transport, $mailer) = Mail::getMailer();
1✔
3722
        $this->sendIt($mailer, $message);
1✔
3723
    }
3724

3725
    public function forgotPassword($email)
3726
    {
3727
        $link = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FORGOT_PASSWORD, TRUE);
1✔
3728

3729
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/welcome');
1✔
3730
        $twig = new \Twig_Environment($loader);
1✔
3731

3732
        $html = $twig->render('forgotpassword.html', [
1✔
3733
            'email' => $this->getEmailPreferred(),
1✔
3734
            'url' => $link,
1✔
3735
        ]);
1✔
3736

3737
        $message = \Swift_Message::newInstance()
1✔
3738
            ->setSubject("Forgot your password?")
1✔
3739
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
1✔
3740
            ->setTo($email)
1✔
3741
            ->setBody("To set a new password, just log in here: $link");
1✔
3742

3743
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3744
        # Outlook.
3745
        $htmlPart = \Swift_MimePart::newInstance();
1✔
3746
        $htmlPart->setCharset('utf-8');
1✔
3747
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
3748
        $htmlPart->setContentType('text/html');
1✔
3749
        $htmlPart->setBody($html);
1✔
3750
        $message->attach($htmlPart);
1✔
3751

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

3754
        list ($transport, $mailer) = Mail::getMailer();
1✔
3755
        $this->sendIt($mailer, $message);
1✔
3756
    }
3757

3758
    public function verifyEmail($email, $force = false)
3759
    {
3760
        # If this is one of our current emails, then we can just make it the primary.
3761
        $emails = $this->getEmails();
6✔
3762
        $handled = FALSE;
6✔
3763

3764
        if (!$force) {
6✔
3765
            foreach ($emails as $anemail) {
6✔
3766
                if (User::canonMail($anemail['email']) == User::canonMail($email)) {
6✔
3767
                    # It's one of ours already; make sure it's flagged as primary.
3768
                    $this->addEmail($email, 1);
3✔
3769
                    $handled = TRUE;
3✔
3770
                }
3771
            }
3772
        }
3773

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

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

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

3789
            if (!$key) {
5✔
3790
                do {
3791
                    # Loop in case of clash on the key we happen to invent.
3792
                    $key = uniqid();
5✔
3793
                    $sql = "INSERT INTO users_emails (email, canon, validatekey, backwards) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE validatekey = ?;";
5✔
3794
                    $this->dbhm->preExec($sql,
5✔
3795
                                         [$email, $canon, $key, strrev($canon), $key]);
5✔
3796
                } while (!$this->dbhm->rowsAffected());
5✔
3797
            }
3798

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

3801
            list ($transport, $mailer) = Mail::getMailer();
5✔
3802
            $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
5✔
3803
            $twig = new \Twig_Environment($loader);
5✔
3804

3805
            $html = $twig->render('verifymail.html', [
5✔
3806
                'email' => $email,
5✔
3807
                'confirm' => $confirm
5✔
3808
            ]);
5✔
3809

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

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

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

3828
            $this->sendIt($mailer, $message);
5✔
3829
        }
3830

3831
        return ($handled);
6✔
3832
    }
3833

3834
    public function confirmEmail($key)
3835
    {
3836
        $rc = FALSE;
2✔
3837
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3838

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

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

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

3852
            $rc = $this->id;
2✔
3853
        }
3854

3855
        return ($rc);
2✔
3856
    }
3857

3858
    public function confirmUnsubscribe()
3859
    {
3860
        list ($transport, $mailer) = Mail::getMailer();
2✔
3861

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

3864
        $message = \Swift_Message::newInstance()
2✔
3865
            ->setSubject("Please confirm you want to leave Freegle")
2✔
3866
            ->setFrom(NOREPLY_ADDR)
2✔
3867
            ->setReplyTo(SUPPORT_ADDR)
2✔
3868
            ->setTo($this->getEmailPreferred())
2✔
3869
            ->setDate(time())
2✔
3870
            ->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✔
3871

3872
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::UNSUBSCRIBE);
2✔
3873
        $this->sendIt($mailer, $message);
2✔
3874
    }
3875

3876
    public function inventEmail($force = FALSE)
3877
    {
3878
        # An invented email is one on our domain that doesn't give away too much detail, but isn't just a string of
3879
        # numbers (ideally).  We may already have one.
3880
        $email = NULL;
44✔
3881

3882
        if (!$force) {
44✔
3883
            # We want the most recent of our own emails.
3884
            $emails = $this->getEmails(TRUE);
44✔
3885
            foreach ($emails as $thisemail) {
44✔
3886
                if (strpos($thisemail['email'], USER_DOMAIN) !== FALSE) {
27✔
3887
                    $email = $thisemail['email'];
14✔
3888
                    break;
14✔
3889
                }
3890
            }
3891
        }
3892

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

3898
            if (!$force && strlen(str_replace(' ', '', $yahooid)) && strpos($yahooid, '@') === FALSE && strlen($yahooid) <= 16) {
31✔
3899
                $email = str_replace(' ', '', $yahooid) . '-' . $this->id . '@' . USER_DOMAIN;
1✔
3900
            } else {
3901
                # Their own email might already be of that nature, which would be lovely.
3902
                if (!$force) {
31✔
3903
                    $email = $this->getEmailPreferred();
31✔
3904

3905
                    if ($email) {
31✔
3906
                        foreach (['firstname', 'lastname', 'fullname'] as $att) {
14✔
3907
                            $words = explode(' ', $this->user[$att]);
14✔
3908
                            foreach ($words as $word) {
14✔
3909
                                if (strlen($word) && stripos($email, $word) !== FALSE) {
14✔
3910
                                    # Unfortunately not - it has some personal info in it.
3911
                                    $email = NULL;
14✔
3912
                                }
3913
                            }
3914
                        }
3915

3916
                        if (stripos($email, '%') !== FALSE) {
14✔
3917
                            # This may indicate a case where the real email is encoded on the LHS, eg gtempaccount.com
3918
                            $email = NULL;
1✔
3919
                        }
3920
                    }
3921
                }
3922

3923
                if ($email) {
31✔
3924
                    # We have an email which is fairly anonymous.  Use the LHS.
3925
                    $p = strpos($email, '@');
2✔
3926
                    $email = str_replace(' ', '', $p > 0 ? substr($email, 0, $p) : $email) . '-' . $this->id . '@' . USER_DOMAIN;
2✔
3927
                } else {
3928
                    # We can't make up something similar to their existing email address so invent from scratch.
3929
                    $lengths = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/distinct_word_lengths.json'), true);
31✔
3930
                    $bigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/word_start_bigrams.json'), true);
31✔
3931
                    $trigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/trigrams.json'), true);
31✔
3932

3933
                    do {
3934
                        $length = \Wordle\array_weighted_rand($lengths);
31✔
3935
                        $start = \Wordle\array_weighted_rand($bigrams);
31✔
3936
                        $email = strtolower(\Wordle\fill_word($start, $length, $trigrams)) . '-' . $this->id . '@' . USER_DOMAIN;
31✔
3937

3938
                        # We might just happen to have invented an email with their personal information in it.  This
3939
                        # actually happened in the UT with "test".
3940
                        foreach (['firstname', 'lastname', 'fullname'] as $att) {
31✔
3941
                            $words = explode(' ', $this->user[$att]);
31✔
3942
                            foreach ($words as $word) {
31✔
3943
                                $word = trim($word);
31✔
3944
                                if (strlen($word)) {
31✔
3945
                                    $p = stripos($email, $word);
26✔
3946
                                    $q = strpos($email, '-');
26✔
3947

3948
                                    if ($word !== '-') {
26✔
3949
                                        # Dash is always present, which is fine.
3950
                                        $email = ($p !== FALSE && $p < $q) ? NULL : $email;
26✔
3951
                                    }
3952
                                }
3953
                            }
3954
                        }
3955
                    } while (!$email);
31✔
3956
                }
3957
            }
3958
        }
3959

3960
        return ($email);
44✔
3961
    }
3962

3963
    public function delete($groupid = NULL, $subject = NULL, $body = NULL, $log = TRUE)
3964
    {
3965
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
18✔
3966

3967
        # Delete memberships.  This will remove any Yahoo memberships.
3968
        $membs = $this->getMemberships();
18✔
3969
        #error_log("Members in delete " . var_export($membs, TRUE));
3970
        foreach ($membs as $memb) {
18✔
3971
            $this->removeMembership($memb['id']);
8✔
3972
        }
3973

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

3976
        if ($rc && $log) {
18✔
3977
            $this->log->log([
5✔
3978
                'type' => Log::TYPE_USER,
5✔
3979
                'subtype' => Log::SUBTYPE_DELETED,
5✔
3980
                'user' => $this->id,
5✔
3981
                'byuser' => $me ? $me->getId() : NULL,
5✔
3982
                'text' => $this->getName()
5✔
3983
            ]);
5✔
3984
        }
3985

3986
        return ($rc);
18✔
3987
    }
3988

3989
    public function getUnsubLink($domain, $id, $type = NULL, $auto = FALSE)
3990
    {
3991
        return (User::loginLink($domain, $id, "/unsubscribe/$id", $type, $auto));
23✔
3992
    }
3993

3994
    public function listUnsubscribe($id, $type = NULL) {
3995
        # These are links which will completely unsubscribe the user.  This is necessary because of Yahoo and Gmail
3996
        # changes in 2024, and also useful for CAN-SPAM.  We want them to involve the key to prevent spoof unsubscribes.
3997
        #
3998
        # We only include the web link, because this providers a better user experience - we can tell them
3999
        # things afterwards.  This is valid - RFC8058 the RFC says you MUST include an HTTPS link, and you MAY
4000
        # include others.
4001
        $key = $id ? $this->getUserKey($id) : '1234';
104✔
4002
        $key = $key ? $key : '1234';
104✔
4003
        #$ret = "<mailto:unsubscribe-$id-$key-$type@" . USER_DOMAIN . "?subject=unsubscribe>, <https://" . USER_SITE . "/one-click-unsubscribe/$id/$key>";
4004
        $ret = "<https://" . USER_SITE . "/one-click-unsubscribe/$id/$key>";
104✔
4005
        #$ret = "<http://localhost:3002/one-click-unsubscribe/$id/$key>";
4006
        return $ret;
104✔
4007
    }
4008

4009
    public function loginLink($domain, $id, $url = '/', $type = NULL, $auto = FALSE)
4010
    {
4011
        $p = strpos($url, '?');
57✔
4012
        $ret = $p === FALSE ? "https://$domain$url?u=$id&src=$type" : "https://$domain$url&u=$id&src=$type";
57✔
4013

4014
        if ($auto) {
57✔
4015
            # Get a per-user link we can use to log in without a password.
4016
            $key = $this->getUserKey($id);
11✔
4017

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

4021
            $p = strpos($url, '?');
11✔
4022
            $src = $type ? "&src=$type" : "";
11✔
4023
            $ret = $p === FALSE ? ("https://$domain$url?u=$id&k=$key$src") : ("https://$domain$url&u=$id&k=$key$src");
11✔
4024
        }
4025

4026
        return ($ret);
57✔
4027
    }
4028

4029
    public function sendOurMails($g = NULL, $checkholiday = TRUE, $checkbouncing = TRUE)
4030
    {
4031
        if ($this->getPrivate('deleted')) {
98✔
4032
            return FALSE;
1✔
4033
        }
4034

4035
        # We don't want to send emails to people who haven't been active for more than six months.  This improves
4036
        # our spam reputation, by avoiding honeytraps.
4037
        $sendit = FALSE;
98✔
4038
        $lastaccess = strtotime($this->getPrivate('lastaccess'));
98✔
4039

4040
        // This time is also present on the client in ModMember, and in Engage.
4041
        if (time() - $lastaccess <= Engage::USER_INACTIVE) {
98✔
4042
            $sendit = TRUE;
98✔
4043

4044
            if ($sendit && $checkholiday) {
98✔
4045
                # We might be on holiday.
4046
                $hol = $this->getPrivate('onholidaytill');
22✔
4047
                $till = $hol ? strtotime($hol) : 0;
22✔
4048
                #error_log("Holiday $till vs " . time());
4049

4050
                $sendit = time() > $till;
22✔
4051
            }
4052

4053
            if ($sendit && $checkbouncing) {
98✔
4054
                # And don't send if we're bouncing.
4055
                $sendit = !$this->getPrivate('bouncing');
22✔
4056
                #error_log("After bouncing $sendit");
4057
            }
4058
        }
4059

4060
        #error_log("Sendit? $sendit");
4061
        return ($sendit);
98✔
4062
    }
4063

4064
    public function getMembershipHistory()
4065
    {
4066
        # We get this from our logs.
4067
        $sql = "SELECT * FROM logs WHERE user = ? AND `type` = ? ORDER BY id DESC;";
5✔
4068
        $logs = $this->dbhr->preQuery($sql, [$this->id, Log::TYPE_GROUP]);
5✔
4069

4070
        $ret = [];
5✔
4071
        foreach ($logs as $log) {
5✔
4072
            $thisone = NULL;
3✔
4073
            switch ($log['subtype']) {
3✔
4074
                case Log::SUBTYPE_JOINED:
3✔
4075
                case Log::SUBTYPE_APPROVED:
1✔
4076
                case Log::SUBTYPE_REJECTED:
1✔
4077
                case Log::SUBTYPE_APPLIED:
1✔
4078
                case Log::SUBTYPE_LEFT:
1✔
4079
                    {
3✔
4080
                        $thisone = $log['subtype'];
3✔
4081
                        break;
3✔
4082
                    }
3✔
4083
            }
4084

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

4089
                if ($g->getId() ==  $log['groupid']) {
3✔
4090
                    $ret[] = [
3✔
4091
                        'timestamp' => Utils::ISODate($log['timestamp']),
3✔
4092
                        'type' => $thisone,
3✔
4093
                        'group' => [
3✔
4094
                            'id' => $log['groupid'],
3✔
4095
                            'nameshort' => $g->getPrivate('nameshort'),
3✔
4096
                            'namedisplay' => $g->getName()
3✔
4097
                        ],
3✔
4098
                        'text' => $log['text']
3✔
4099
                    ];
3✔
4100
                }
4101
            }
4102
        }
4103

4104
        return ($ret);
5✔
4105
    }
4106

4107
    public function search($search, $ctx)
4108
    {
4109
        if (preg_replace('/\-|\~/', '', $search) ==  '') {
5✔
4110
            # Most likely an encoded id.
4111
            $search = User::decodeId($search);
×
4112
        }
4113

4114
        if (preg_match('/story-(.*)/', $search, $matches)) {
5✔
4115
            # Story.
4116
            $s = new Story($this->dbhr, $this->dbhm, $matches[1]);
×
4117
            $search = $s->getPrivate('userid');
×
4118
        }
4119

4120
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
5✔
4121
        $id = intval(Utils::presdef('id', $ctx, 0));
5✔
4122
        $ctx = $ctx ? $ctx : [];
5✔
4123
        $q = $this->dbhr->quote("$search%");
5✔
4124
        $backwards = strrev($search);
5✔
4125
        $qb = $this->dbhr->quote("$backwards%");
5✔
4126

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

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

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

4136
        $sql = "SELECT DISTINCT userid FROM
5✔
4137
                ((SELECT userid FROM users_emails WHERE canon LIKE $canon OR backwards LIKE $qb) UNION
5✔
4138
                (SELECT userid FROM users_emails WHERE canon LIKE $canon2) UNION
5✔
4139
                (SELECT id AS userid FROM users WHERE fullname LIKE $q) UNION
5✔
4140
                (SELECT id AS userid FROM users WHERE yahooid LIKE $q) UNION
5✔
4141
                (SELECT id AS userid FROM users WHERE id = ?) UNION
4142
                (SELECT userid FROM users_logins WHERE uid LIKE $q)) t WHERE userid > ? ORDER BY userid ASC";
5✔
4143
        $users = $this->dbhr->preQuery($sql, [$search, $id]);
5✔
4144

4145
        $ret = [];
5✔
4146

4147
        foreach ($users as $user) {
5✔
4148
            $ctx['id'] = $user['userid'];
4✔
4149

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

4152
            $thisone = $u->getPublic(NULL, TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, [
4✔
4153
                MessageCollection::PENDING,
4✔
4154
                MessageCollection::APPROVED
4✔
4155
            ], TRUE);
4✔
4156

4157
            # We might not have the emails.
4158
            $thisone['email'] = $u->getEmailPreferred();
4✔
4159
            $thisone['emails'] = $u->getEmails();
4✔
4160

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

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

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

4170
            # Also return the chats for this user.  Can't use ChatRooms::listForUser because that would exclude any
4171
            # chats on groups where we were no longer a member.
4172
            $rooms = array_filter(array_column($this->dbhr->preQuery("SELECT id FROM chat_rooms WHERE user1 = ? UNION SELECT id FROM chat_rooms WHERE chattype = ? AND user2 = ?;", [
4✔
4173
                $user['userid'],
4✔
4174
                ChatRoom::TYPE_USER2USER,
4✔
4175
                $user['userid'],
4✔
4176
            ]), 'id'));
4✔
4177

4178
            $thisone['chatrooms'] = [];
4✔
4179

4180
            if ($rooms) {
4✔
4181
                $r = new ChatRoom($this->dbhr, $this->dbhm);
×
4182
                $thisone['chatrooms'] = $r->fetchRooms($rooms, $user['userid'], FALSE);
×
4183
            }
4184

4185
            # Add the public location and best guess lat/lng
4186
            $thisone['info']['publiclocation'] = $u->getPublicLocation();
4✔
4187
            $latlng = $u->getLatLng(FALSE, TRUE);
4✔
4188
            $thisone['privateposition'] = [
4✔
4189
                'lat' => $latlng[0],
4✔
4190
                'lng' => $latlng[1],
4✔
4191
                'name' => $latlng[2]
4✔
4192
            ];
4✔
4193

4194
            $thisone['comments'] = $this->getCommentsForSingleUser($user['userid']);
4✔
4195
            $thisone['tnuserid'] = $u->getPrivate('tnuserid');
4✔
4196

4197
            $push = $this->dbhr->preQuery("SELECT MAX(lastsent) AS lastpush FROM users_push_notifications WHERE userid = ?;", [
4✔
4198
                $user['userid']
4✔
4199
            ]);
4✔
4200

4201
            foreach ($push as $p) {
4✔
4202
                $thisone['lastpush'] = Utils::ISODate($p['lastpush']);
4✔
4203
            }
4204

4205
            $thisone['info'] = $u->getInfo();
4✔
4206
            $thisone['trustlevel'] = $u->getPrivate('trustlevel');
4✔
4207

4208
            $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE userid = ?;", [
4✔
4209
                $u->getId()
4✔
4210
            ]);
4✔
4211

4212
            $thisone['bans'] = [];
4✔
4213

4214
            foreach ($bans as $ban) {
4✔
4215
                $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
4216
                $banner = User::get($this->dbhr, $this->dbhm, $ban['byuser']);
1✔
4217
                $thisone['bans'][] = [
1✔
4218
                    'date' => Utils::ISODate($ban['date']),
1✔
4219
                    'group' => $g->getName(),
1✔
4220
                    'byemail' => $banner->getEmailPreferred(),
1✔
4221
                    'byuserid' => $ban['byuser']
1✔
4222
                ];
1✔
4223
            }
4224

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

4228
            if ($me->hasPermission(User::PERM_GIFTAID)) {
4✔
4229
                $thisone['donations'] = $d->listByUser($user['userid']);
2✔
4230
            }
4231

4232
            $thisone['newsfeedmodstatus'] = $u->getPrivate('newsfeedmodstatus');
4✔
4233
            $thisone['newsfeed'] = $this->dbhr->preQuery("SELECT id, message, timestamp, hidden, hiddenby, deleted, deletedby FROM newsfeed WHERE userid = ? ORDER BY id DESC;", [
4✔
4234
                $user['userid']
4✔
4235
            ]);
4✔
4236

4237
            foreach ($thisone['newsfeed'] as &$nf) {
4✔
4238
                $nf['timestamp'] = Utils::ISODate($nf['timestamp']);
×
4239
                $nf['deleted'] = Utils::ISODate($nf['deleted']);
×
4240
                $nf['hidden'] = Utils::ISODate($nf['hidden']);
×
4241
            }
4242

4243
            $ret[] = $thisone;
4✔
4244
        }
4245

4246
        return ($ret);
5✔
4247
    }
4248

4249
    private function safeGetPostcode($val) {
4250
        $ret = [ NULL, NULL ];
55✔
4251

4252
        $settings = json_decode($val, TRUE);
55✔
4253

4254
        if (Utils::pres('mylocation', $settings) &&
55✔
4255
            Utils::presdef('type', $settings['mylocation'], NULL) == 'Postcode') {
55✔
4256
            $ret = [
14✔
4257
                Utils::presdef('id', $settings['mylocation'], NULL),
14✔
4258
                Utils::presdef('name', $settings['mylocation'], NULL)
14✔
4259
            ];
14✔
4260
        }
4261

4262
        return $ret;
55✔
4263
    }
4264

4265
    public function setPrivate($att, $val)
4266
    {
4267
        if (!strcmp($att, 'settings') && $val) {
173✔
4268
            # Possible location change.
4269
            list ($oldid, $oldloc) = $this->safeGetPostcode($this->getPrivate('settings'));
55✔
4270
            list ($newid, $newloc) = $this->safeGetPostcode($val);
55✔
4271

4272
            if ($oldloc !== $newloc) {
55✔
4273
                # We have changed our location.
4274
                parent::setPrivate('lastlocation', $newid);
14✔
4275
                $i = new Isochrone($this->dbhr, $this->dbhm);
14✔
4276
                $i->deleteForUser($this->id);
14✔
4277

4278
                $this->log->log([
14✔
4279
                            'type' => Log::TYPE_USER,
14✔
4280
                            'subtype' => Log::SUBTYPE_POSTCODECHANGE,
14✔
4281
                            'user' => $this->id,
14✔
4282
                            'text' => $newloc
14✔
4283
                        ]);
14✔
4284
            }
4285

4286
            // Prune the info in the settings to remove any groupsnear info, which would use space and is not needed.
4287
            $val = User::pruneSettings($val);
55✔
4288
        }
4289

4290
        User::clearCache($this->id);
173✔
4291
        parent::setPrivate($att, $val);
173✔
4292
    }
4293

4294
    public static function pruneSettings($val) {
4295
        // Prune info from what we store in the user table to keep it smaller.
4296
        if (strpos($val, 'groupsnear') !== FALSE) {
55✔
4297
            $decoded = json_decode($val, TRUE);
×
4298
            if (Utils::pres('mylocation', $decoded) && Utils::pres('groupsnear', $decoded['mylocation'])) {
×
4299
                unset($decoded['mylocation']['groupsnear']);
×
4300
                $val = json_encode($decoded);
×
4301
            }
4302
        }
4303

4304
        return $val;
55✔
4305
    }
4306

4307
    public function canMerge()
4308
    {
4309
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
16✔
4310
        return (array_key_exists('canmerge', $settings) ? $settings['canmerge'] : TRUE);
16✔
4311
    }
4312

4313
    public function notifsOn($type, $groupid = NULL)
4314
    {
4315
        if ($this->getPrivate('deleted')) {
561✔
4316
            return FALSE;
1✔
4317
        }
4318

4319
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
561✔
4320
        $notifs = Utils::pres('notifications', $settings);
561✔
4321

4322
        $defs = [
561✔
4323
            self::NOTIFS_EMAIL => TRUE,
561✔
4324
            self::NOTIFS_EMAIL_MINE => FALSE,
561✔
4325
            self::NOTIFS_PUSH => TRUE,
561✔
4326
            self::NOTIFS_APP => TRUE
561✔
4327
        ];
561✔
4328

4329
        $ret = ($notifs && array_key_exists($type, $notifs)) ? $notifs[$type] : $defs[$type];
561✔
4330

4331
        if ($ret && $groupid) {
561✔
4332
            # Check we're an active mod on this group - if not then we don't want the notifications.
4333
            $ret = $this->activeModForGroup($groupid);
5✔
4334
        }
4335

4336
        #error_log("Notifs on for user #{$this->id} type $type ? $ret from " . var_export($notifs, TRUE));
4337
        return ($ret);
561✔
4338
    }
4339

4340
    public function getNotificationPayload($modtools)
4341
    {
4342
        # This gets a notification count/title/message for this user.
4343
        $notifcount = 0;
8✔
4344
        $title = '';
8✔
4345
        $message = NULL;
8✔
4346
        $chatids = [];
8✔
4347
        $route = NULL;
8✔
4348

4349
        if (!$modtools) {
8✔
4350
            # User notification.  We want to show a count of chat messages, or some of the message if there is just one.
4351
            $r = new ChatRoom($this->dbhr, $this->dbhm);
5✔
4352
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_USER2USER, ChatRoom::TYPE_USER2MOD], $modtools);
5✔
4353
            $chatcount = count($unseen);
5✔
4354
            $total = $chatcount;
5✔
4355
            foreach ($unseen as $un) {
5✔
4356
                $chatids[] = $un['chatid'];
3✔
4357
            };
4358

4359
            #error_log("Chats with unseen " . var_export($chatids, TRUE));
4360
            $n = new Notifications($this->dbhr, $this->dbhm);
5✔
4361
            $notifcount = $n->countUnseen($this->id);
5✔
4362

4363
            if ($total ==  1) {
5✔
4364
                $r = new ChatRoom($this->dbhr, $this->dbhm, $unseen[0]['chatid']);
2✔
4365
                $atts = $r->getPublic($this);
2✔
4366
                $title = $atts['name'];
2✔
4367
                list($msgs, $users) = $r->getMessages(100, 0);
2✔
4368

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

4372
                    # Convert all emojis to smilies.  Obviously that's not right, but most of them are, and we want
4373
                    # to get rid of the unicode.
4374
                    $message = preg_replace('/\\\\u.*?\\\\u/', ':-)', $message);
2✔
4375

4376
                    $message = strlen($message) > 256 ? (substr($message, 0, 256) . "...") : $message;
2✔
4377
                }
4378

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

4381
                if ($notifcount) {
2✔
4382
                    $total += $notifcount;
2✔
4383
                }
4384
            } else if ($total > 1) {
3✔
4385
                $title = "You have $total new messages";
1✔
4386
                $route = "/chats";
1✔
4387

4388
                if ($notifcount) {
1✔
4389
                    $total += $notifcount;
1✔
4390
                    $title .= " and $notifcount notification" . ($notifcount == 1 ? '' : 's');
1✔
4391
                }
4392
            } else {
4393
                # Add in the notifications you see primarily from the newsfeed.
4394
                if ($notifcount) {
3✔
4395
                    $total += $notifcount;
3✔
4396
                    $ctx = NULL;
3✔
4397
                    $notifs = $n->get($this->id, $ctx);
3✔
4398
                    $title = $n->getNotifTitle($notifs);
3✔
4399
                    $route = '/';
3✔
4400

4401
                    if (count($notifs) > 0) {
3✔
4402
                        # For newsfeed notifications sent a route to the right place.
4403
                        switch ($notifs[0]['type']) {
3✔
4404
                            case Notifications::TYPE_COMMENT_ON_COMMENT:
3✔
4405
                            case Notifications::TYPE_COMMENT_ON_YOUR_POST:
3✔
4406
                            case Notifications::TYPE_LOVED_COMMENT:
3✔
4407
                            case Notifications::TYPE_LOVED_POST:
3✔
4408
                                $route = '/chitchat/' . $notifs[0]['newsfeedid'];
1✔
4409
                                break;
5✔
4410
                        }
4411
                    }
4412
                }
4413
            }
4414
        } else {
4415
            # ModTools notification.  We show the count of work + chats.
4416
            $r = new ChatRoom($this->dbhr, $this->dbhm);
4✔
4417
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_MOD2MOD, ChatRoom::TYPE_USER2MOD], $modtools);
4✔
4418
            $chatcount = count($unseen);
4✔
4419

4420
            $work = $this->getWorkCounts();
4✔
4421
            $total = $work['total'] + $chatcount;
4✔
4422

4423
            // The order of these is important as the route will be the last matching.
4424
            $types = [
4✔
4425
                'pendingvolunteering' => [ 'volunteer op', 'volunteerops', '/modtools/volunteering' ],
4✔
4426
                'pendingevents' => [ 'event', 'events', '/modtools/communityevents' ],
4✔
4427
                'socialactions' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4✔
4428
                'popularposts' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4✔
4429
                'stories' => [ 'story', 'stories', '/modtools/members/stories' ],
4✔
4430
                'newsletterstories' => [ 'newsletter story', 'newsletter stories', '/modtools/members/newsletter' ],
4✔
4431
                'chatreview' => [ 'chat message to review', 'chat messages to review', '/modtools/chats/review' ],
4✔
4432
                'pendingadmins' => [ 'admin', 'admins', '/modtools/admins' ],
4✔
4433
                'spammembers' => [ 'member to review', 'members to review', '/modtools/members/review' ],
4✔
4434
                'relatedmembers' => [ 'related member to review', 'related members to review', '/modtools/members/related' ],
4✔
4435
                'editreview' => [ 'edit', 'edits', '/modtools/messages/edits' ],
4✔
4436
                'spam' => [ 'message to review', 'messages to review', '/modtools/messages/pending' ],
4✔
4437
                'pending' => [ 'pending message', 'pending messages', '/modtools/messages/pending' ]
4✔
4438
            ];
4✔
4439

4440
            $title = $chatcount ? ("$chatcount chat message" . ($chatcount != 1 ? 's' : '') . "\n") : '';
4✔
4441
            $route = NULL;
4✔
4442

4443
            foreach ($types as $type => $vals) {
4✔
4444
                if (Utils::presdef($type, $work, 0) > 0) {
4✔
4445
                    $title .= $work[$type] . ' ' . ($work[$type] != 1 ? $vals[1] : $vals[0] ) . "\n";
1✔
4446
                    $route = $vals[2];
1✔
4447
                }
4448
            }
4449

4450
            $title = $title == '' ? NULL : $title;
4✔
4451
        }
4452

4453

4454
        return ([$total, $chatcount, $notifcount, $title, $message, array_unique($chatids), $route]);
8✔
4455
    }
4456

4457
    public function hasPermission($perm)
4458
    {
4459
        $perms = $this->user['permissions'];
39✔
4460
        return ($perms && stripos($perms, $perm) !== FALSE);
39✔
4461
    }
4462

4463
    public function sendIt($mailer, $message)
4464
    {
4465
        $mailer->send($message);
28✔
4466
    }
4467

4468
    public function thankDonation()
4469
    {
4470
        try {
4471
            $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/donations');
1✔
4472
            $twig = new \Twig_Environment($loader);
1✔
4473
            list ($transport, $mailer) = Mail::getMailer();
1✔
4474

4475
            $message = \Swift_Message::newInstance()
1✔
4476
                ->setSubject("Thank you for supporting Freegle!")
1✔
4477
                ->setFrom(PAYPAL_THANKS_FROM)
1✔
4478
                ->setReplyTo(PAYPAL_THANKS_FROM)
1✔
4479
                ->setTo($this->getEmailPreferred())
1✔
4480
                ->setBody("Thank you for supporting Freegle!");
1✔
4481

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

4484
            $html = $twig->render('thank.html', [
1✔
4485
                'name' => $this->getName(),
1✔
4486
                'email' => $this->getEmailPreferred(),
1✔
4487
                'unsubscribe' => $this->loginLink(USER_SITE, $this->getId(), "/unsubscribe", NULL)
1✔
4488
            ]);
1✔
4489

4490
            # Add HTML in base-64 as default quoted-printable encoding leads to problems on
4491
            # Outlook.
4492
            $htmlPart = \Swift_MimePart::newInstance();
1✔
4493
            $htmlPart->setCharset('utf-8');
1✔
4494
            $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
4495
            $htmlPart->setContentType('text/html');
1✔
4496
            $htmlPart->setBody($html);
1✔
4497
            $message->attach($htmlPart);
1✔
4498

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

4501
            $this->sendIt($mailer, $message);
1✔
4502
        } catch (\Exception $e) { error_log("Failed " . $e->getMessage()); };
×
4503
    }
4504

4505
    public function invite($email)
4506
    {
4507
        $ret = FALSE;
9✔
4508

4509
        # We can only invite logged in.
4510
        if ($this->id) {
9✔
4511
            # ...and only if we have spare.
4512
            if ($this->user['invitesleft'] > 0) {
9✔
4513
                # They might already be using us - but they might also have forgotten.  So allow that case.  However if
4514
                # they have actively declined a previous invitation we suppress this one.
4515
                $previous = $this->dbhr->preQuery("SELECT id FROM users_invitations WHERE email = ? AND outcome = ?;", [
9✔
4516
                    $email,
9✔
4517
                    User::INVITE_DECLINED
9✔
4518
                ]);
9✔
4519

4520
                if (count($previous) == 0) {
9✔
4521
                    # The table has a unique key on userid and email, so that means we can only invite the same person
4522
                    # once.  That avoids us pestering them.
4523
                    try {
4524
                        $this->dbhm->preExec("INSERT INTO users_invitations (userid, email) VALUES (?,?);", [
9✔
4525
                            $this->id,
9✔
4526
                            $email
9✔
4527
                        ]);
9✔
4528

4529
                        # We're ok to invite.
4530
                        $fromname = $this->getName();
9✔
4531
                        $frommail = $this->getEmailPreferred();
9✔
4532
                        $url = "https://" . USER_SITE . "/invite/" . $this->dbhm->lastInsertId();
9✔
4533

4534
                        list ($transport, $mailer) = Mail::getMailer();
9✔
4535
                        $message = \Swift_Message::newInstance()
9✔
4536
                            ->setSubject("$fromname has invited you to try Freegle!")
9✔
4537
                            ->setFrom([NOREPLY_ADDR => SITE_NAME])
9✔
4538
                            ->setReplyTo($frommail)
9✔
4539
                            ->setTo($email)
9✔
4540
                            ->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✔
4541

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

4544
                        $html = invite($fromname, $frommail, $url);
9✔
4545

4546
                        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
4547
                        # Outlook.
4548
                        $htmlPart = \Swift_MimePart::newInstance();
9✔
4549
                        $htmlPart->setCharset('utf-8');
9✔
4550
                        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
9✔
4551
                        $htmlPart->setContentType('text/html');
9✔
4552
                        $htmlPart->setBody($html);
9✔
4553
                        $message->attach($htmlPart);
9✔
4554

4555
                        $this->sendIt($mailer, $message);
9✔
4556
                        $ret = TRUE;
9✔
4557

4558
                        $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft - 1 WHERE id = ?;", [
9✔
4559
                            $this->id
9✔
4560
                        ]);
9✔
4561
                    } catch (\Exception $e) {
1✔
4562
                        # Probably a duplicate.
4563
                    }
4564
                }
4565
            }
4566
        }
4567

4568
        return ($ret);
9✔
4569
    }
4570

4571
    public function inviteOutcome($id, $outcome)
4572
    {
4573
        $invites = $this->dbhm->preQuery("SELECT * FROM users_invitations WHERE id = ?;", [
1✔
4574
            $id
1✔
4575
        ]);
1✔
4576

4577
        foreach ($invites as $invite) {
1✔
4578
            if ($invite['outcome'] == User::INVITE_PENDING) {
1✔
4579
                $this->dbhm->preExec("UPDATE users_invitations SET outcome = ?, outcometimestamp = NOW() WHERE id = ?;", [
1✔
4580
                    $outcome,
1✔
4581
                    $id
1✔
4582
                ]);
1✔
4583

4584
                if ($outcome == User::INVITE_ACCEPTED) {
1✔
4585
                    # Give the sender two more invites.  This means that if their invitations are unsuccessful, they will
4586
                    # stall, but if they do ok, they won't.  This isn't perfect - someone could fake up emails and do
4587
                    # successful invitations that way.
4588
                    $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft + 2 WHERE id = ?;", [
1✔
4589
                        $invite['userid']
1✔
4590
                    ]);
1✔
4591
                }
4592
            }
4593
        }
4594
    }
4595

4596
    public function listInvitations($since = "30 days ago")
4597
    {
4598
        $ret = [];
8✔
4599

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

4606
        foreach ($invites as $invite) {
8✔
4607
            # Check if this email is now on the platform.
4608
            $invite['date'] = Utils::ISODate($invite['date']);
7✔
4609
            $invite['outcometimestamp'] = $invite['outcometimestamp'] ? Utils::ISODate($invite['outcometimestamp']) : NULL;
7✔
4610
            $ret[] = $invite;
7✔
4611
        }
4612

4613
        return ($ret);
8✔
4614
    }
4615

4616
    public function getLatLng($usedef = TRUE, $usegroup = TRUE, $blur = Utils::BLUR_NONE)
4617
    {
4618
        $ret = [ 0, 0, NULL ];
170✔
4619

4620
        if ($this->id) {
170✔
4621
            $locs = $this->getLatLngs([ $this->user ], $usedef, $usegroup, FALSE, [ $this->user ]);
170✔
4622
            $loc = $locs[$this->id];
170✔
4623

4624
            if ($loc) {
170✔
4625
                if ($blur && ($loc['lat'] || $loc['lng'])) {
169✔
4626
                    list ($loc['lat'], $loc['lng']) = Utils::blur($loc['lat'], $loc['lng'], $blur);
4✔
4627
                }
4628

4629
                $ret = [ $loc['lat'], $loc['lng'], Utils::presdef('loc', $loc, NULL) ];
169✔
4630
            }
4631
        }
4632

4633
        return $ret;
170✔
4634
    }
4635

4636
    public function getPublicLocations(&$users, $atts = NULL)
4637
    {
4638
        $idsleft = [];
105✔
4639
        
4640
        foreach ($users as $userid => $user) {
105✔
4641
            if (!Utils::pres('info', $user) || !Utils::pres('publiclocation', $user['info'])) {
105✔
4642
                $idsleft[] = $userid;
105✔
4643
            }
4644
        }
4645
        
4646
        $areas = NULL;
105✔
4647
        $membs = NULL;
105✔
4648

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

4653
            foreach ($atts as $att) {
105✔
4654
                $loc = NULL;
105✔
4655
                $grp = NULL;
105✔
4656

4657
                $aid = NULL;
105✔
4658
                $lid = NULL;
105✔
4659
                $lat = NULL;
105✔
4660
                $lng = NULL;
105✔
4661

4662
                # Default to nowhere.
4663
                $users[$att['id']]['info']['publiclocation'] = [
105✔
4664
                    'display' => '',
105✔
4665
                    'location' => NULL,
105✔
4666
                    'groupname' => NULL
105✔
4667
                ];
105✔
4668

4669
                if (Utils::pres('settings', $att)) {
105✔
4670
                    $settings = $att['settings'];
22✔
4671
                    $settings = json_decode($settings, TRUE);
22✔
4672

4673
                    if (Utils::pres('mylocation', $settings) && Utils::pres('area', $settings['mylocation'])) {
22✔
4674
                        $loc = $settings['mylocation']['area']['name'];
7✔
4675
                        $lid = $settings['mylocation']['id'];
7✔
4676
                        $lat = $settings['mylocation']['lat'];
7✔
4677
                        $lng = $settings['mylocation']['lng'];
7✔
4678
                    }
4679
                }
4680

4681
                if (!$loc) {
105✔
4682
                    # Get the name of the last area we used.
4683
                    if (is_null($areas)) {
98✔
4684
                        $areas = $this->dbhr->preQuery("SELECT l2.id, l2.name, l2.lat, l2.lng, users.id AS userid FROM locations l1 
98✔
4685
                            INNER JOIN users ON users.lastlocation = l1.id
4686
                            INNER JOIN locations l2 ON l2.id = l1.areaid
4687
                            WHERE users.id IN (" . implode(',', $idsleft) . ");", NULL, FALSE, FALSE);
98✔
4688
                    }
4689

4690
                    foreach ($areas as $area) {
98✔
4691
                        if ($att['id'] ==  $area['userid']) {
25✔
4692
                            $loc = $area['name'];
25✔
4693
                            $lid = $area['id'];
25✔
4694
                            $lat = $area['lat'];
25✔
4695
                            $lng = $area['lng'];
25✔
4696
                        }
4697
                    }
4698
                }
4699

4700
                if (!$lid) {
105✔
4701
                    # Find the group of which we are a member which is closest to our location.  We do this because generally
4702
                    # the number of groups we're in is small and therefore this will be quick, whereas the groupsNear call is
4703
                    # fairly slow.
4704
                    $closestdist = PHP_INT_MAX;
98✔
4705
                    $closestname = NULL;
98✔
4706

4707
                    # Get all the memberships.
4708
                    if (!$membs) {
98✔
4709
                        $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(
98✔
4710
                                ',',
98✔
4711
                                $idsleft
98✔
4712
                            ) . ") ORDER BY added ASC;";
98✔
4713
                        $membs = $this->dbhr->preQuery($sql);
98✔
4714
                    }
4715

4716
                    foreach ($membs as $memb) {
98✔
4717
                        if ($memb['userid'] == $att['id']) {
88✔
4718
                            $dist = \GreatCircle::getDistance($lat, $lng, $memb['lat'], $memb['lng']);
88✔
4719

4720
                            if ($dist < $closestdist) {
88✔
4721
                                $closestdist = $dist;
88✔
4722
                                $closestname = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
88✔
4723
                            }
4724
                        }
4725
                    }
4726

4727
                    if (!is_null($closestname)) {
98✔
4728
                        $grp = $closestname;
88✔
4729

4730
                        # The location name might be in the group name, in which case just use the group.
4731
                        $loc = stripos($grp, $loc) !== FALSE ? NULL : $loc;
88✔
4732
                    }
4733
                }
4734

4735
                if ($loc) {
105✔
4736
                    $display = $loc ? ($loc . ($grp ? ", $grp" : "")) : ($grp ? $grp : '');
32✔
4737

4738
                    $users[$att['id']]['info']['publiclocation'] = [
32✔
4739
                        'display' => $display,
32✔
4740
                        'location' => $loc,
32✔
4741
                        'groupname' => $grp
32✔
4742
                    ];
32✔
4743

4744
                    $idsleft = array_filter($idsleft, function($val) use ($att) {
32✔
4745
                        return($val != $att['id']);
32✔
4746
                    });
32✔
4747
                }
4748
            }
4749

4750
            if (count($idsleft) > 0) {
105✔
4751
                # We have some left which don't have explicit postcodes.  Try for a group name.
4752
                #
4753
                # First check the group we used most recently.
4754
                #error_log("Look for group name only for {$att['id']}");
4755
                $found = [];
98✔
4756
                foreach ($idsleft as $userid) {
98✔
4757
                    $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;", [
98✔
4758
                        $userid
98✔
4759
                    ]);
98✔
4760

4761
                    foreach ($messages as $msg) {
98✔
4762
                        list ($type, $item, $location) = Message::parseSubject($msg['subject']);
61✔
4763

4764
                        if ($item) {
61✔
4765
                            $grp = $location;
41✔
4766

4767
                            // Handle some misformed locations which end up with spurious brackets.
4768
                            $grp = preg_replace('/\(|\)/', '', $grp);
41✔
4769

4770
                            $users[$userid]['info']['publiclocation'] = [
41✔
4771
                                'display' => $grp,
41✔
4772
                                'location' => NULL,
41✔
4773
                                'groupname' => $grp
41✔
4774
                            ];
41✔
4775

4776
                            $found[] = $userid;
41✔
4777
                        }
4778
                    }
4779
                }
4780

4781
                $idsleft = array_diff($idsleft, $found);
98✔
4782
                
4783
                # Now check just membership.
4784
                if (count($idsleft)) {
98✔
4785
                    if (!$membs) {
65✔
4786
                        $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(
21✔
4787
                                ',',
21✔
4788
                                $idsleft
21✔
4789
                            ) . ") ORDER BY added ASC;";
21✔
4790
                        $membs = $this->dbhr->preQuery($sql);
21✔
4791
                    }
4792
                    
4793
                    foreach ($idsleft as $userid) {
65✔
4794
                        # Now check the group we joined most recently.
4795
                        foreach ($membs as $memb) {
65✔
4796
                            if ($memb['userid'] == $userid) {
49✔
4797
                                $grp = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
49✔
4798

4799
                                $users[$userid]['info']['publiclocation'] = [
49✔
4800
                                    'display' => $grp,
49✔
4801
                                    'location' => NULL,
49✔
4802
                                    'groupname' => $grp
49✔
4803
                                ];
49✔
4804
                            }
4805
                        }
4806
                    }
4807
                }
4808
            }
4809
        }
4810
    }
4811

4812
    public function getLatLngs($users, $usedef = TRUE, $usegroup = TRUE, $needgroup = FALSE, $atts = NULL, $blur = NULL)
4813
    {
4814
        $userids = array_filter(array_column($users, 'id'));
171✔
4815
        $ret = [];
171✔
4816

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

4820
            foreach ($atts as $att) {
171✔
4821
                $lat = NULL;
171✔
4822
                $lng = NULL;
171✔
4823
                $loc = NULL;
171✔
4824

4825
                if (Utils::pres('settings', $att)) {
171✔
4826
                    $settings = $att['settings'];
36✔
4827
                    $settings = json_decode($settings, TRUE);
36✔
4828

4829
                    if (Utils::pres('mylocation', $settings)) {
36✔
4830
                        $lat = $settings['mylocation']['lat'];
32✔
4831
                        $lng = $settings['mylocation']['lng'];
32✔
4832
                        $loc = Utils::presdef('name', $settings['mylocation'], NULL);
32✔
4833
                        #error_log("Got from mylocation $lat, $lng, $loc");
4834
                    }
4835
                }
4836

4837
                if (is_null($lat)) {
171✔
4838
                    $lid = $att['lastlocation'];
153✔
4839

4840
                    if ($lid) {
153✔
4841
                        $l = new Location($this->dbhr, $this->dbhm, $lid);
23✔
4842
                        $lat = $l->getPrivate('lat');
23✔
4843
                        $lng = $l->getPrivate('lng');
23✔
4844
                        $loc = $l->getPrivate('name');
23✔
4845
                        #error_log("Got from last location $lat, $lng, $loc");
4846
                    }
4847
                }
4848

4849
                if (!is_null($lat)) {
171✔
4850
                    $ret[$att['id']] = [
51✔
4851
                        'lat' => $lat,
51✔
4852
                        'lng' => $lng,
51✔
4853
                        'loc' => $loc,
51✔
4854
                    ];
51✔
4855

4856
                    $userids = array_filter($userids, function($id) use ($att) {
51✔
4857
                        return $id != $att['id'];
51✔
4858
                    });
51✔
4859
                }
4860
            }
4861
        }
4862

4863
        if ($userids && count($userids) && $usegroup) {
171✔
4864
            # Still some we haven't handled.  Get the last message posted on a group with a location, if any.
4865
            $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);
146✔
4866
            foreach ($membs as $memb) {
146✔
4867
                $ret[$memb['userid']] = [
3✔
4868
                    'lat' => $memb['lat'],
3✔
4869
                    'lng' => $memb['lng']
3✔
4870
                ];
3✔
4871

4872
                #error_log("Got from last message posted {$memb['lat']}, {$memb['lng']}");
4873

4874
                $userids = array_filter($userids, function($id) use ($memb) {
3✔
4875
                    return $id != $memb['userid'];
3✔
4876
                });
3✔
4877
            }
4878
        }
4879

4880
        if ($userids && count($userids) && $usegroup) {
171✔
4881
            # Still some we haven't handled.  Get the memberships.  Logic will choose most recently joined.
4882
            $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);
144✔
4883
            foreach ($membs as $memb) {
144✔
4884
                $ret[$memb['userid']] = [
128✔
4885
                    'lat' => $memb['lat'],
128✔
4886
                    'lng' => $memb['lng'],
128✔
4887
                    'group' => Utils::presdef('namefull', $memb, $memb['nameshort'])
128✔
4888
                ];
128✔
4889

4890
                #error_log("Got from membership {$memb['lat']}, {$memb['lng']}, " . Utils::presdef('namefull', $memb, $memb['nameshort']));
4891

4892
                $userids = array_filter($userids, function($id) use ($memb) {
128✔
4893
                    return $id != $memb['userid'];
128✔
4894
                });
128✔
4895
            }
4896
        }
4897

4898
        if ($userids && count($userids)) {
171✔
4899
            # Still some we haven't handled.
4900
            foreach ($userids as $userid) {
23✔
4901
                if ($usedef) {
23✔
4902
                    $ret[$userid] = [
20✔
4903
                        'lat' => 53.9450,
20✔
4904
                        'lng' => -2.5209
20✔
4905
                    ];
20✔
4906
                } else {
4907
                    $ret[$userid] = NULL;
14✔
4908
                }
4909
            }
4910
        }
4911

4912
        if ($needgroup) {
171✔
4913
            # Get a group name.
4914
            $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✔
4915
            foreach ($membs as $memb) {
7✔
4916
                $ret[$memb['userid']]['group'] = Utils::presdef('namefull', $memb, $memb['nameshort']);
7✔
4917
            }
4918
        }
4919

4920
        if ($blur) {
171✔
4921
            foreach ($ret as &$memb) {
7✔
4922
                if ($memb['lat'] || $memb['lng']) {
7✔
4923
                    list ($memb['lat'], $memb['lng']) = Utils::blur($memb['lat'], $memb['lng'], $blur);
7✔
4924
                }
4925
            }
4926
        }
4927

4928
        return ($ret);
171✔
4929
    }
4930

4931
    public function isFreegleMod()
4932
    {
4933
        $ret = FALSE;
168✔
4934

4935
        $this->cacheMemberships();
168✔
4936

4937
        foreach ($this->memberships as $mem) {
168✔
4938
            if ($mem['type'] == Group::GROUP_FREEGLE && ($mem['role'] == User::ROLE_OWNER || $mem['role'] == User::ROLE_MODERATOR)) {
143✔
4939
                $ret = TRUE;
39✔
4940
            }
4941
        }
4942

4943
        return ($ret);
168✔
4944
    }
4945

4946
    public function getKudos($id = NULL)
4947
    {
4948
        $id = $id ? $id : $this->id;
1✔
4949
        $kudos = [
1✔
4950
            'userid' => $id,
1✔
4951
            'posts' => 0,
1✔
4952
            'chats' => 0,
1✔
4953
            'newsfeed' => 0,
1✔
4954
            'events' => 0,
1✔
4955
            'vols' => 0,
1✔
4956
            'facebook' => 0,
1✔
4957
            'platform' => 0,
1✔
4958
            'kudos' => 0,
1✔
4959
        ];
1✔
4960

4961
        $kudi = $this->dbhr->preQuery("SELECT * FROM users_kudos WHERE userid = ?;", [
1✔
4962
            $id
1✔
4963
        ]);
1✔
4964

4965
        foreach ($kudi as $k) {
1✔
4966
            $kudos = $k;
1✔
4967
        }
4968

4969
        return ($kudos);
1✔
4970
    }
4971

4972
    public function updateKudos($id = NULL, $force = FALSE)
4973
    {
4974
        $current = $this->getKudos($id);
1✔
4975

4976
        # Only update if we don't have one or it's older than a day.  This avoids repeatedly updating the entry
4977
        # for the same user in some bulk operations.
4978
        if (!Utils::pres('timestamp', $current) || (time() - strtotime($current['timestamp']) > 24 * 60 * 60)) {
1✔
4979
            # We analyse a user's activity and assign them a level.
4980
            #
4981
            # Only interested in activity in the last year.
4982
            $id = $id ? $id : $this->id;
1✔
4983
            $start = date('Y-m-d', strtotime("365 days ago"));
1✔
4984

4985
            # First, the number of months in which they have posted.
4986
            $posts = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM messages WHERE fromuser = ? AND date >= '$start';", [
1✔
4987
                $id
1✔
4988
            ])[0]['count'];
1✔
4989

4990
            # Ditto communicated with people.
4991
            $chats = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM chat_messages WHERE userid = ? AND date >= '$start';", [
1✔
4992
                $id
1✔
4993
            ])[0]['count'];
1✔
4994

4995
            # Newsfeed posts
4996
            $newsfeed = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(timestamp), '-', MONTH(timestamp)))) AS count FROM newsfeed WHERE userid = ? AND added >= '$start';", [
1✔
4997
                $id
1✔
4998
            ])[0]['count'];
1✔
4999

5000
            # Events
5001
            $events = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM communityevents WHERE userid = ? AND added >= '$start';", [
1✔
5002
                $id
1✔
5003
            ])[0]['count'];
1✔
5004

5005
            # Volunteering
5006
            $vols = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM volunteering WHERE userid = ? AND added >= '$start';", [
1✔
5007
                $id
1✔
5008
            ])[0]['count'];
1✔
5009

5010
            # Do they have a Facebook login?
5011
            $facebook = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_logins WHERE userid = ? AND type = ?", [
1✔
5012
                    $id,
1✔
5013
                    User::LOGIN_FACEBOOK
1✔
5014
                ])[0]['count'] > 0;
1✔
5015

5016
            # Have they posted using the platform?
5017
            $platform = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM messages WHERE fromuser = ? AND arrival >= '$start' AND sourceheader = ?;", [
1✔
5018
                    $id,
1✔
5019
                    Message::PLATFORM
1✔
5020
                ])[0]['count'] > 0;
1✔
5021

5022
            $kudos = $posts + $chats + $newsfeed + $events + $vols;
1✔
5023

5024
            if ($kudos > 0 || $force) {
1✔
5025
                # No sense in creating entries which are blank or the same.
5026
                $current = $this->getKudos($id);
1✔
5027

5028
                if ($current['kudos'] != $kudos || $force) {
1✔
5029
                    $this->dbhm->preExec("REPLACE INTO users_kudos (userid, kudos, posts, chats, newsfeed, events, vols, facebook, platform) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", [
1✔
5030
                        $id,
1✔
5031
                        $kudos,
1✔
5032
                        $posts,
1✔
5033
                        $chats,
1✔
5034
                        $newsfeed,
1✔
5035
                        $events,
1✔
5036
                        $vols,
1✔
5037
                        $facebook,
1✔
5038
                        $platform
1✔
5039
                    ], FALSE);
1✔
5040
                }
5041
            }
5042
        }
5043
    }
5044

5045
    public function topKudos($gid, $limit = 10)
5046
    {
5047
        $limit = intval($limit);
1✔
5048

5049
        $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✔
5050
            $gid,
1✔
5051
            User::ROLE_MEMBER
1✔
5052
        ]);
1✔
5053

5054
        $ret = [];
1✔
5055

5056
        foreach ($kudos as $k) {
1✔
5057
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5058
            $atts = $u->getPublic();
1✔
5059
            $atts['email'] = $u->getEmailPreferred();
1✔
5060

5061
            $thisone = [
1✔
5062
                'user' => $atts,
1✔
5063
                'kudos' => $k
1✔
5064
            ];
1✔
5065

5066
            $ret[] = $thisone;
1✔
5067
        }
5068

5069
        return ($ret);
1✔
5070
    }
5071

5072
    public function possibleMods($gid, $limit = 10)
5073
    {
5074
        # We look for users who are not mods with top kudos who also:
5075
        # - active in last 60 days
5076
        # - not bouncing
5077
        # - using a location which is in the group area
5078
        # - have posted with the platform, as we don't want loyal users of TN or Yahoo.
5079
        # - have a Facebook login, as they are more likely to do publicity.
5080
        $limit = intval($limit);
1✔
5081
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
5082
        $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✔
5083
        $kudos = $this->dbhr->preQuery($sql, [
1✔
5084
            $gid,
1✔
5085
            User::ROLE_MEMBER
1✔
5086
        ]);
1✔
5087

5088
        $ret = [];
1✔
5089

5090
        foreach ($kudos as $k) {
1✔
5091
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5092
            $atts = $u->getPublic();
1✔
5093
            $atts['email'] = $u->getEmailPreferred();
1✔
5094

5095
            $thisone = [
1✔
5096
                'user' => $atts,
1✔
5097
                'kudos' => $k
1✔
5098
            ];
1✔
5099

5100
            $ret[] = $thisone;
1✔
5101
        }
5102

5103
        return ($ret);
1✔
5104
    }
5105

5106
    public function requestExport($sync = FALSE)
5107
    {
5108
        $tag = Utils::randstr(64);
8✔
5109

5110
        # Flag sync ones as started to avoid window with background thread.
5111
        $sync = $sync ? "NOW()" : "NULL";
8✔
5112
        $this->dbhm->preExec("INSERT INTO users_exports (userid, tag, started) VALUES (?, ?, $sync);", [
8✔
5113
            $this->id,
8✔
5114
            $tag
8✔
5115
        ]);
8✔
5116

5117
        return ([$this->dbhm->lastInsertId(), $tag]);
8✔
5118
    }
5119

5120
    public function export($exportid, $tag)
5121
    {
5122
        $this->dbhm->preExec("UPDATE users_exports SET started = NOW() WHERE id = ? AND tag = ?;", [
7✔
5123
            $exportid,
7✔
5124
            $tag
7✔
5125
        ]);
7✔
5126

5127
        # For GDPR we support the ability for a user to export the data we hold about them.  Key points about this:
5128
        #
5129
        # - It needs to be at a high level of abstraction and understandable by the user, not just a cryptic data
5130
        #   dump.
5131
        # - It needs to include data provided by the user and data observed about the user, but not profiling
5132
        #   or categorisation based on that data.  This means that (for example) we need to return which
5133
        #   groups they have joined, but not whether joining those groups has flagged them up as a potential
5134
        #   spammer.
5135
        $ret = [];
7✔
5136
        error_log("...basic info");
7✔
5137

5138
        # Data in user table.
5139
        $d = [];
7✔
5140
        $d['Our_internal_ID_for_you'] = $this->getPrivate('id');
7✔
5141
        $d['Your_full_name'] = $this->getPrivate('fullname');
7✔
5142
        $d['Your_first_name'] = $this->getPrivate('firstname');
7✔
5143
        $d['Your_last_name'] = $this->getPrivate('lastname');
7✔
5144
        $d['Your_Yahoo_ID'] = $this->getPrivate('yahooid');
7✔
5145
        $d['Your_role_on_the_system'] = $this->getPrivate('systemrole');
7✔
5146
        $d['When_you_joined_the_site'] = Utils::ISODate($this->getPrivate('added'));
7✔
5147
        $d['When_you_last_accessed_the_site'] = Utils::ISODate($this->getPrivate('lastaccess'));
7✔
5148
        $d['When_we_last_checked_for_relevant_posts_for_you'] = Utils::ISODate($this->getPrivate('lastrelevantcheck'));
7✔
5149
        $d['Whether_your_email_is_bouncing'] = $this->getPrivate('bouncing') ? 'Yes' : 'No';
7✔
5150
        $d['Permissions_you_have_on_the_site'] = $this->getPrivate('permissions');
7✔
5151
        $d['Number_of_remaining_invitations_you_can_send_to_other_people'] = $this->getPrivate('invitesleft');
7✔
5152

5153
        $lastlocation = $this->user['lastlocation'];
7✔
5154

5155
        if ($lastlocation) {
7✔
5156
            $l = new Location($this->dbhr, $this->dbhm, $lastlocation);
×
5157
            $d['Last_location_you_posted_from'] = $l->getPrivate('name') . " (" . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
×
5158
        }
5159

5160
        $settings = $this->getPrivate('settings');
7✔
5161

5162
        if ($settings) {
7✔
5163
            $settings = json_decode($settings, TRUE);
7✔
5164

5165
            $location = Utils::presdef('id', Utils::presdef('mylocation', $settings, []), NULL);
7✔
5166

5167
            if ($location) {
7✔
5168
                $l = new Location($this->dbhr, $this->dbhm, $location);
6✔
5169
                $d['Last_location_you_entered'] = $l->getPrivate('name') . ' (' . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
6✔
5170
            }
5171

5172
            $notifications = Utils::pres('notifications', $settings);
7✔
5173

5174
            $d['Notifications']['Send_email_notifications_for_chat_messages'] = Utils::presdef('email', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5175
            $d['Notifications']['Send_email_notifications_of_chat_messages_you_send'] = Utils::presdef('emailmine', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5176
            $d['Notifications']['Send_notifications_for_apps'] = Utils::presdef('app', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5177
            $d['Notifications']['Send_push_notifications_to_web_browsers'] = Utils::presdef('push', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5178
            $d['Notifications']['Send_Facebook_notifications'] = Utils::presdef('facebook', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5179
            $d['Notifications']['Send_emails_about_notifications_on_the_site'] = Utils::presdef('notificationmails', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5180

5181
            $d['Hide_profile_picture'] = Utils::presdef('useprofile', $settings, TRUE) ? 'Yes' : 'No';
7✔
5182

5183
            if ($this->isModerator()) {
7✔
5184
                $d['Show_members_that_you_are_a_moderator'] = Utils::pres('showmod', $settings) ? 'Yes' : 'No';
1✔
5185

5186
                switch (Utils::presdef('modnotifs', $settings, 4)) {
1✔
5187
                    case 24:
1✔
5188
                        $d['Send_notifications_of_active_mod_work'] = 'After 24 hours';
×
5189
                        break;
×
5190
                    case 12:
1✔
5191
                        $d['Send_notifications_of_active_mod_work'] = 'After 12 hours';
×
5192
                        break;
×
5193
                    case 4:
1✔
5194
                        $d['Send_notifications_of_active_mod_work'] = 'After 4 hours';
1✔
5195
                        break;
1✔
5196
                    case 2:
×
5197
                        $d['Send_notifications_of_active_mod_work'] = 'After 2 hours';
×
5198
                        break;
×
5199
                    case 1:
×
5200
                        $d['Send_notifications_of_active_mod_work'] = 'After 1 hours';
×
5201
                        break;
×
5202
                    case 0:
×
5203
                        $d['Send_notifications_of_active_mod_work'] = 'Immediately';
×
5204
                        break;
×
5205
                    case -1:
5206
                        $d['Send_notifications_of_active_mod_work'] = 'Never';
×
5207
                        break;
×
5208
                }
5209

5210
                switch (Utils::presdef('backupmodnotifs', $settings, 12)) {
1✔
5211
                    case 24:
1✔
5212
                        $d['Send_notifications_of_backup_mod_work'] = 'After 24 hours';
×
5213
                        break;
×
5214
                    case 12:
1✔
5215
                        $d['Send_notifications_of_backup_mod_work'] = 'After 12 hours';
1✔
5216
                        break;
1✔
5217
                    case 4:
×
5218
                        $d['Send_notifications_of_backup_mod_work'] = 'After 4 hours';
×
5219
                        break;
×
5220
                    case 2:
×
5221
                        $d['Send_notifications_of_backup_mod_work'] = 'After 2 hours';
×
5222
                        break;
×
5223
                    case 1:
×
5224
                        $d['Send_notifications_of_backup_mod_work'] = 'After 1 hours';
×
5225
                        break;
×
5226
                    case 0:
×
5227
                        $d['Send_notifications_of_backup_mod_work'] = 'Immediately';
×
5228
                        break;
×
5229
                    case -1:
5230
                        $d['Send_notifications_of_backup_mod_work'] = 'Never';
×
5231
                        break;
×
5232
                }
5233

5234
                $d['Show_members_that_you_are_a_moderator'] = Utils::presdef('showmod', $settings, TRUE) ? 'Yes' : 'No';
1✔
5235
            }
5236
        }
5237

5238
        # Invitations.  Only show what we sent; the outcome is not this user's business.
5239
        error_log("...invitations");
7✔
5240
        $invites = $this->listInvitations("1970-01-01");
7✔
5241
        $d['invitations'] = [];
7✔
5242

5243
        foreach ($invites as $invite) {
7✔
5244
            $d['invitations'][] = [
6✔
5245
                'email' => $invite['email'],
6✔
5246
                'date' => Utils::ISODate($invite['date'])
6✔
5247
            ];
6✔
5248
        }
5249

5250
        error_log("...emails");
7✔
5251
        $d['emails'] = $this->getEmails();
7✔
5252

5253
        foreach ($d['emails'] as &$email) {
7✔
5254
            $email['added'] = Utils::ISODate($email['added']);
1✔
5255

5256
            if ($email['validated']) {
1✔
5257
                $email['validated'] = Utils::ISODate($email['validated']);
×
5258
            }
5259
        }
5260

5261
        $phones = $this->dbhr->preQuery("SELECT * FROM users_phones WHERE userid = ?;", [
7✔
5262
            $this->id
7✔
5263
        ]);
7✔
5264

5265
        foreach ($phones as $phone) {
7✔
5266
            $d['phone'] = $phone['number'];
6✔
5267
            $d['phonelastsent'] = Utils::ISODate($phone['lastsent']);
6✔
5268
            $d['phonelastclicked'] = Utils::ISODate($phone['lastclicked']);
6✔
5269
        }
5270

5271
        error_log("...logins");
7✔
5272
        $d['logins'] = $this->dbhr->preQuery("SELECT type, uid, added, lastaccess FROM users_logins WHERE userid = ?;", [
7✔
5273
            $this->id
7✔
5274
        ]);
7✔
5275

5276
        foreach ($d['logins'] as &$dd) {
7✔
5277
            $dd['added'] = Utils::ISODate($dd['added']);
7✔
5278
            $dd['lastaccess'] = Utils::ISODate($dd['lastaccess']);
7✔
5279
        }
5280

5281
        error_log("...memberships");
7✔
5282
        $d['memberships'] = $this->getMemberships();
7✔
5283

5284
        error_log("...memberships history");
7✔
5285
        $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✔
5286
        $membs = $this->dbhr->preQuery($sql, [$this->id]);
7✔
5287
        foreach ($membs as &$memb) {
7✔
5288
            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
7✔
5289
            $memb['namedisplay'] = $name;
7✔
5290
            $memb['added'] = Utils::ISODate($memb['added']);
7✔
5291
        }
5292

5293
        $d['membershipshistory'] = $membs;
7✔
5294

5295
        error_log("...searches");
7✔
5296
        $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✔
5297
            $this->id
7✔
5298
        ]);
7✔
5299

5300
        foreach ($d['searches'] as &$s) {
7✔
5301
            $s['date'] = Utils::ISODate($s['date']);
×
5302
        }
5303

5304
        error_log("...alerts");
7✔
5305
        $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✔
5306
            $this->id
7✔
5307
        ]);
7✔
5308

5309
        foreach ($d['alerts'] as &$s) {
7✔
5310
            $s['responded'] = Utils::ISODate($s['responded']);
×
5311
        }
5312

5313
        error_log("...donations");
7✔
5314
        $d['donations'] = $this->dbhr->preQuery("SELECT * FROM users_donations WHERE userid = ? ORDER BY timestamp ASC;", [
7✔
5315
            $this->id
7✔
5316
        ]);
7✔
5317

5318
        foreach ($d['donations'] as &$s) {
7✔
5319
            $s['timestamp'] = Utils::ISODate($s['timestamp']);
1✔
5320
        }
5321

5322
        error_log("...bans");
7✔
5323
        $d['bans'] = [];
7✔
5324

5325
        $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE byuser = ?;", [
7✔
5326
            $this->id
7✔
5327
        ]);
7✔
5328

5329
        foreach ($bans as $ban) {
7✔
5330
            $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
5331
            $u = User::get($this->dbhr, $this->dbhm, $ban['userid']);
1✔
5332
            $d['bans'][] = [
1✔
5333
                'date' => Utils::ISODate($ban['date']),
1✔
5334
                'group' => $g->getName(),
1✔
5335
                'email' => $u->getEmailPreferred(),
1✔
5336
                'userid' => $ban['userid']
1✔
5337
            ];
1✔
5338
        }
5339

5340
        error_log("...spammers");
7✔
5341
        $d['spammers'] = $this->dbhr->preQuery("SELECT * FROM spam_users WHERE byuserid = ? ORDER BY added ASC;", [
7✔
5342
            $this->id
7✔
5343
        ]);
7✔
5344

5345
        foreach ($d['spammers'] as &$s) {
7✔
5346
            $s['added'] = Utils::ISODate($s['added']);
×
5347
            $u = User::get($this->dbhr, $this->dbhm, $s['userid']);
×
5348
            $s['email'] = $u->getEmailPreferred();
×
5349
        }
5350

5351
        $d['spamdomains'] = $this->dbhr->preQuery("SELECT domain, date FROM spam_whitelist_links WHERE userid = ?;", [
7✔
5352
            $this->id
7✔
5353
        ]);
7✔
5354

5355
        foreach ($d['spamdomains'] as &$s) {
7✔
5356
            $s['date'] = Utils::ISODate($s['date']);
×
5357
        }
5358

5359
        error_log("...images");
7✔
5360
        $images = $this->dbhr->preQuery("SELECT id, url FROM users_images WHERE userid = ?;", [
7✔
5361
            $this->id
7✔
5362
        ]);
7✔
5363

5364
        $d['images'] = [];
7✔
5365

5366
        foreach ($images as $image) {
7✔
5367
            if (Utils::pres('url', $image)) {
6✔
5368
                $d['images'][] = [
6✔
5369
                    'id' => $image['id'],
6✔
5370
                    'thumb' => $image['url']
6✔
5371
                ];
6✔
5372
            } else {
5373
                $a = new Attachment($this->dbhr, $this->dbhm, NULL, Attachment::TYPE_USER);
×
5374
                $d['images'][] = [
×
5375
                    'id' => $image['id'],
×
5376
                    'thumb' => $a->getPath(TRUE, $image['id'])
×
5377
                ];
×
5378
            }
5379
        }
5380

5381
        error_log("...notifications");
7✔
5382
        $d['notifications'] = $this->dbhr->preQuery("SELECT timestamp, url FROM users_notifications WHERE touser = ? AND seen = 1;", [
7✔
5383
            $this->id
7✔
5384
        ]);
7✔
5385

5386
        foreach ($d['notifications'] as &$n) {
7✔
5387
            $n['timestamp'] = Utils::ISODate($n['timestamp']);
×
5388
        }
5389

5390
        error_log("...addresses");
7✔
5391
        $d['addresses'] = [];
7✔
5392

5393
        $addrs = $this->dbhr->preQuery("SELECT * FROM users_addresses WHERE userid = ?;", [
7✔
5394
            $this->id
7✔
5395
        ]);
7✔
5396

5397
        foreach ($addrs as $addr) {
7✔
5398
            $a = new Address($this->dbhr, $this->dbhm, $addr['id']);
×
5399
            $d['addresses'][] = $a->getPublic();
×
5400
        }
5401

5402
        error_log("...events");
7✔
5403
        $d['communityevents'] = [];
7✔
5404

5405
        $events = $this->dbhr->preQuery("SELECT id FROM communityevents WHERE userid = ?;", [
7✔
5406
            $this->id
7✔
5407
        ]);
7✔
5408

5409
        foreach ($events as $event) {
7✔
5410
            $e = new CommunityEvent($this->dbhr, $this->dbhm, $event['id']);
×
5411
            $d['communityevents'][] = $e->getPublic();
×
5412
        }
5413

5414
        error_log("...volunteering");
7✔
5415
        $d['volunteering'] = [];
7✔
5416

5417
        $events = $this->dbhr->preQuery("SELECT id FROM volunteering WHERE userid = ?;", [
7✔
5418
            $this->id
7✔
5419
        ]);
7✔
5420

5421
        foreach ($events as $event) {
7✔
5422
            $e = new Volunteering($this->dbhr, $this->dbhm, $event['id']);
×
5423
            $d['volunteering'][] = $e->getPublic();
×
5424
        }
5425

5426
        error_log("...comments");
7✔
5427
        $d['comments'] = [];
7✔
5428
        $comms = $this->dbhr->preQuery("SELECT * FROM users_comments WHERE byuserid = ? ORDER BY date ASC;", [
7✔
5429
            $this->id
7✔
5430
        ]);
7✔
5431

5432
        foreach ($comms as &$comm) {
7✔
5433
            $u = User::get($this->dbhr, $this->dbhm, $comm['userid']);
1✔
5434
            $comm['email'] = $u->getEmailPreferred();
1✔
5435
            $comm['date'] = Utils::ISODate($comm['date']);
1✔
5436
            $d['comments'][] = $comm;
1✔
5437
        }
5438

5439
        error_log("...ratings");
7✔
5440
        $d['ratings'] = $this->getRated();
7✔
5441

5442
        error_log("...locations");
7✔
5443
        $d['locations'] = [];
7✔
5444

5445
        $locs = $this->dbhr->preQuery("SELECT * FROM locations_excluded WHERE userid = ?;", [
7✔
5446
            $this->id
7✔
5447
        ]);
7✔
5448

5449
        foreach ($locs as $loc) {
7✔
5450
            $g = Group::get($this->dbhr, $this->dbhm, $loc['groupid']);
×
5451
            $l = new Location($this->dbhr, $this->dbhm, $loc['locationid']);
×
5452
            $d['locations'][] = [
×
5453
                'group' => $g->getName(),
×
5454
                'location' => $l->getPrivate('name'),
×
5455
                'date' => Utils::ISODate($loc['date'])
×
5456
            ];
×
5457
        }
5458

5459
        error_log("...messages");
7✔
5460
        $msgs = $this->dbhr->preQuery("SELECT id FROM messages WHERE fromuser = ? ORDER BY arrival ASC;", [
7✔
5461
            $this->id
7✔
5462
        ]);
7✔
5463

5464
        $d['messages'] = [];
7✔
5465

5466
        foreach ($msgs as $msg) {
7✔
5467
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
×
5468

5469
            # Show all info here even moderator attributes.  This wouldn't normally be shown to users, but none
5470
            # of it is confidential really.
5471
            $thisone = $m->getPublic(FALSE, FALSE, TRUE);
×
5472

5473
            if (count($thisone['groups']) > 0) {
×
5474
                $g = Group::get($this->dbhr, $this->dbhm, $thisone['groups'][0]['groupid']);
×
5475
                $thisone['groups'][0]['namedisplay'] = $g->getName();
×
5476
            }
5477

5478
            $d['messages'][] = $thisone;
×
5479
        }
5480

5481
        # Chats.  Can't use listForUser as that filters on various things and has a ModTools vs FD distinction, and
5482
        # we're interested in information we have provided.  So we get the chats mentioned in the roster (we have
5483
        # provided information about being online) and where we have sent or reviewed a chat message.
5484
        error_log("...chats");
7✔
5485
        $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✔
5486
            $this->id,
7✔
5487
            $this->id,
7✔
5488
            $this->id
7✔
5489
        ]);
7✔
5490

5491
        $d['chatrooms'] = [];
7✔
5492
        $count = 0;
7✔
5493

5494
        foreach ($chatids as $chatid) {
7✔
5495
            # We don't return the chat name because it's too slow to produce.
5496
            $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid['id']);
6✔
5497
            $thisone = [
6✔
5498
                'id' => $chatid['id'],
6✔
5499
                'name' => $r->getPublic($this)['name'],
6✔
5500
                'messages' => []
6✔
5501
            ];
6✔
5502

5503
            $sql = "SELECT date, lastip FROM chat_roster WHERE `chatid` = ? AND userid = ?;";
6✔
5504
            $roster = $this->dbhr->preQuery($sql, [$chatid['id'], $this->id]);
6✔
5505
            foreach ($roster as $rost) {
6✔
5506
                $thisone['lastip'] = $rost['lastip'];
6✔
5507
                $thisone['date'] = Utils::ISODate($rost['date']);
6✔
5508
            }
5509

5510
            # Get the messages we have sent in this chat.
5511
            $msgs = $this->dbhr->preQuery("SELECT id FROM chat_messages WHERE chatid = ? AND (userid = ? OR reviewedby = ?);", [
6✔
5512
                $chatid['id'],
6✔
5513
                $this->id,
6✔
5514
                $this->id
6✔
5515
            ]);
6✔
5516

5517
            $userlist = NULL;
6✔
5518

5519
            foreach ($msgs as $msg) {
6✔
5520
                $cm = new ChatMessage($this->dbhr, $this->dbhm, $msg['id']);
6✔
5521
                $thismsg = $cm->getPublic(FALSE, $userlist);
6✔
5522

5523
                # Strip out most of the refmsg detail - it's not ours and we need to save volume of data.
5524
                $refmsg = Utils::pres('refmsg', $thismsg);
6✔
5525

5526
                if ($refmsg) {
6✔
5527
                    $thismsg['refmsg'] = [
×
5528
                        'id' => $msg['id'],
×
5529
                        'subject' => Utils::presdef('subject', $refmsg, NULL)
×
5530
                    ];
×
5531
                }
5532

5533
                $thismsg['mine'] = Utils::presdef('userid', $thismsg, NULL) == $this->id;
6✔
5534
                $thismsg['date'] = Utils::ISODate($thismsg['date']);
6✔
5535
                $thisone['messages'][] = $thismsg;
6✔
5536

5537
                $count++;
6✔
5538
//
5539
//                if ($count > 200) {
5540
//                    break 2;
5541
//                }
5542
            }
5543

5544
            if (count($thisone['messages']) > 0) {
6✔
5545
                $d['chatrooms'][] = $thisone;
6✔
5546
            }
5547
        }
5548

5549
        error_log("...newsfeed");
7✔
5550
        $newsfeeds = $this->dbhr->preQuery("SELECT * FROM newsfeed WHERE userid = ?;", [
7✔
5551
            $this->id
7✔
5552
        ]);
7✔
5553

5554
        $d['newsfeed'] = [];
7✔
5555

5556
        foreach ($newsfeeds as $newsfeed) {
7✔
5557
            $n = new Newsfeed($this->dbhr, $this->dbhm, $newsfeed['id']);
6✔
5558
            $thisone = $n->getPublic(FALSE, FALSE, FALSE, FALSE);
6✔
5559
            $d['newsfeed'][] = $thisone;
6✔
5560
        }
5561

5562
        $d['newsfeed_unfollows'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_unfollow WHERE userid = ?;", [
7✔
5563
            $this->id
7✔
5564
        ]);
7✔
5565

5566
        foreach ($d['newsfeed_unfollows'] as &$dd) {
7✔
5567
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5568
        }
5569

5570
        $d['newsfeed_likes'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_likes WHERE userid = ?;", [
7✔
5571
            $this->id
7✔
5572
        ]);
7✔
5573

5574
        foreach ($d['newsfeed_likes'] as &$dd) {
7✔
5575
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5576
        }
5577

5578
        $d['newsfeed_reports'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_reports WHERE userid = ?;", [
7✔
5579
            $this->id
7✔
5580
        ]);
7✔
5581

5582
        foreach ($d['newsfeed_reports'] as &$dd) {
7✔
5583
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5584
        }
5585

5586
        $d['aboutme'] = $this->dbhr->preQuery("SELECT timestamp, text FROM users_aboutme WHERE userid = ? AND LENGTH(text) > 5;", [
7✔
5587
            $this->id
7✔
5588
        ]);
7✔
5589

5590
        foreach ($d['aboutme'] as &$dd) {
7✔
5591
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5592
        }
5593

5594
        error_log("...stories");
7✔
5595
        $d['stories'] = $this->dbhr->preQuery("SELECT date, headline, story FROM users_stories WHERE userid = ?;", [
7✔
5596
            $this->id
7✔
5597
        ]);
7✔
5598

5599
        foreach ($d['stories'] as &$dd) {
7✔
5600
            $dd['date'] = Utils::ISODate($dd['date']);
×
5601
        }
5602

5603
        $d['stories_likes'] = $this->dbhr->preQuery("SELECT storyid FROM users_stories_likes WHERE userid = ?;", [
7✔
5604
            $this->id
7✔
5605
        ]);
7✔
5606

5607
        error_log("...exports");
7✔
5608
        $d['exports'] = $this->dbhr->preQuery("SELECT userid, started, completed FROM users_exports WHERE userid = ?;", [
7✔
5609
            $this->id
7✔
5610
        ]);
7✔
5611

5612
        foreach ($d['exports'] as &$dd) {
7✔
5613
            $dd['started'] = Utils::ISODate($dd['started']);
7✔
5614
            $dd['completed'] = Utils::ISODate($dd['completed']);
7✔
5615
        }
5616

5617
        error_log("...logs");
7✔
5618
        $l = new Log($this->dbhr, $this->dbhm);
7✔
5619
        $ctx = NULL;
7✔
5620
        $d['logs'] = $l->get(NULL, NULL, NULL, NULL, NULL, NULL, PHP_INT_MAX, $ctx, $this->id);
7✔
5621

5622
        error_log("...add group to logs");
7✔
5623
        $loggroups = [];
7✔
5624
        foreach ($d['logs'] as &$log) {
7✔
5625
            if (Utils::pres('groupid', $log)) {
7✔
5626
                # Don't put the whole group info in there, as it is slow to get.
5627
                if (!array_key_exists($log['groupid'], $loggroups)) {
7✔
5628
                    $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
7✔
5629

5630
                    if ($g->getId() == $log['groupid']) {
7✔
5631
                        $loggroups[$log['groupid']] = [
7✔
5632
                            'id' => $log['groupid'],
7✔
5633
                            'nameshort' => $g->getPrivate('nameshort'),
7✔
5634
                            'namedisplay' => $g->getName()
7✔
5635
                        ];
7✔
5636
                    } else {
5637
                        $loggroups[$log['groupid']] = [
×
5638
                            'id' => $log['groupid'],
×
5639
                            'nameshort' => "DeletedGroup{$log['groupid']}",
×
5640
                            'namedisplay' => "Deleted group #{$log['groupid']}"
×
5641
                        ];
×
5642
                    }
5643
                }
5644

5645
                $log['group'] = $loggroups[$log['groupid']];
7✔
5646
            }
5647
        }
5648

5649
        # Gift aid
5650
        $don = new Donations($this->dbhr, $this->dbhm);
7✔
5651
        $d['giftaid'] = $don->getGiftAid($this->id);
7✔
5652

5653
        $ret = $d;
7✔
5654

5655
        # There are some other tables with information which we don't return.  Here's what and why:
5656
        # - Not part of the current UI so can't have any user data
5657
        #     polls_users
5658
        # - Covered by data that we do return from other tables
5659
        #     messages_drafts, messages_history, messages_groups, messages_outcomes,
5660
        #     messages_promises, users_modmails, modnotifs, users_dashboard,
5661
        #     users_nudges
5662
        # - Transient logging data
5663
        #     logs_emails, logs_sql, logs_api, logs_errors, logs_src
5664
        # - Not provided by the user themselves
5665
        #     user_comments, messages_reneged, spam_users, users_banned, users_stories_requested,
5666
        #     users_thanks
5667
        # - Inferred or derived data.  These are not considered to be provided by the user (see p10 of
5668
        #   http://ec.europa.eu/newsroom/document.cfm?doc_id=44099)
5669
        #     users_kudos, visualise
5670

5671
        # Compress the data in the DB because it can be huge.
5672
        #
5673
        error_log("...filter");
7✔
5674
        Utils::filterResult($ret);
7✔
5675
        error_log("...encode");
7✔
5676
        $data = json_encode($ret, JSON_PARTIAL_OUTPUT_ON_ERROR);
7✔
5677
        error_log("...encoded length " . strlen($data) . ", now compress");
7✔
5678
        $data = gzdeflate($data);
7✔
5679
        $this->dbhm->preExec("UPDATE users_exports SET completed = NOW(), data = ? WHERE id = ? AND tag = ?;", [
7✔
5680
            $data,
7✔
5681
            $exportid,
7✔
5682
            $tag
7✔
5683
        ]);
7✔
5684
        error_log("...completed, length " . strlen($data));
7✔
5685

5686
        return ($ret);
7✔
5687
    }
5688

5689
    function getExport($userid, $id, $tag)
5690
    {
5691
        $ret = NULL;
2✔
5692

5693
        $exports = $this->dbhr->preQuery("SELECT * FROM users_exports WHERE userid = ? AND id = ? AND tag = ?;", [
2✔
5694
            $userid,
2✔
5695
            $id,
2✔
5696
            $tag
2✔
5697
        ]);
2✔
5698

5699
        foreach ($exports as $export) {
2✔
5700
            $ret = $export;
2✔
5701
            $ret['requested'] = $ret['requested'] ? Utils::ISODate($ret['requested']) : NULL;
2✔
5702
            $ret['started'] = $ret['started'] ? Utils::ISODate($ret['started']) : NULL;
2✔
5703
            $ret['completed'] = $ret['completed'] ? Utils::ISODate($ret['completed']) : NULL;
2✔
5704

5705
            if ($ret['completed']) {
2✔
5706
                # This has completed.  Return the data.  Will be zapped in cron exports..
5707
                $ret['data'] = json_decode(gzinflate($export['data']), TRUE);
2✔
5708
                $ret['infront'] = 0;
2✔
5709
            } else {
5710
                # Find how many are in front of us.
5711
                $infront = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_exports WHERE id < ? AND completed IS NULL;", [
2✔
5712
                    $id
2✔
5713
                ]);
2✔
5714

5715
                $ret['infront'] = $infront[0]['count'];
2✔
5716
            }
5717
        }
5718

5719
        return ($ret);
2✔
5720
    }
5721

5722
    public function limbo() {
5723
        # We set the deleted attribute, which will cause the v2 Go API not to return any personal data.  The user
5724
        # can still log in, and potentially recover their account by calling session with PATCH of deleted = NULL.
5725
        # Otherwise a background script will purge their account after a couple of weeks.
5726
        #
5727
        # This allows us to handle the fairly common case of users deleting their accounts by mistake, or changing
5728
        # their minds.  This often happens because one-click unsubscribe in emails, which we need to have for
5729
        # delivery.
5730
        $this->dbhm->preExec("UPDATE users SET deleted = NOW() WHERE id = ?;", [
1✔
5731
            $this->id
1✔
5732
        ]);
1✔
5733
    }
5734

5735
    public function processForgets($id = NULL) {
5736
        $count = 0;
1✔
5737

5738
        $idq = $id ? "AND id = $id" : "";
1✔
5739
        $users = $this->dbhr->preQuery("SELECT id, deleted FROM users WHERE deleted IS NOT NULL AND DATEDIFF(NOW(), deleted) > 14 AND forgotten IS NULL $idq;");
1✔
5740

5741
        foreach ($users as $user) {
1✔
5742
            $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
5743
            $u->forget('Grace period');
1✔
5744
            $count++;
1✔
5745
        }
5746

5747
        return $count;
1✔
5748
    }
5749

5750
    public function forget($reason)
5751
    {
5752
        # Wipe a user of personal data, for the GDPR right to be forgotten.  We don't delete the user entirely
5753
        # otherwise it would mess up the stats.
5754

5755
        # Clear name etc.
5756
        $this->setPrivate('firstname', NULL);
7✔
5757
        $this->setPrivate('lastname', NULL);
7✔
5758
        $this->setPrivate('fullname', "Deleted User #" . $this->id);
7✔
5759
        $this->setPrivate('settings', NULL);
7✔
5760
        $this->setPrivate('yahooid', NULL);
7✔
5761

5762
        # Delete emails which aren't ours.
5763
        $emails = $this->getEmails();
7✔
5764

5765
        foreach ($emails as $email) {
7✔
5766
            if (!$email['ourdomain']) {
3✔
5767
                $this->removeEmail($email['email']);
3✔
5768
            }
5769
        }
5770

5771
        # Delete all logins.
5772
        $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ?;", [
7✔
5773
            $this->id
7✔
5774
        ]);
7✔
5775

5776
        # Delete any phone numbers.
5777
        $this->dbhm->preExec("DELETE FROM users_phones WHERE userid = ?;", [
7✔
5778
            $this->id
7✔
5779
        ]);
7✔
5780

5781
        # Delete the content (but not subject) of any messages, and any email header information such as their
5782
        # name and email address.
5783
        $msgs = $this->dbhm->preQuery("SELECT id FROM messages WHERE fromuser = ? AND messages.type IN (?, ?);", [
7✔
5784
            $this->id,
7✔
5785
            Message::TYPE_OFFER,
7✔
5786
            Message::TYPE_WANTED
7✔
5787
        ]);
7✔
5788

5789
        foreach ($msgs as $msg) {
7✔
5790
            $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✔
5791
                $msg['id']
1✔
5792
            ]);
1✔
5793

5794
            $this->dbhm->preExec("UPDATE messages_groups SET deleted = 1 WHERE msgid = ?;", [
1✔
5795
                $msg['id']
1✔
5796
            ]);
1✔
5797

5798
            # Delete outcome comments that they've added - just about might have personal data.
5799
            $this->dbhm->preExec("UPDATE messages_outcomes SET comments = NULL WHERE msgid = ?;", [
1✔
5800
                $msg['id']
1✔
5801
            ]);
1✔
5802

5803
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
1✔
5804

5805
            if (!$m->hasOutcome()) {
1✔
5806
                $m->withdraw('Withdrawn on user unsubscribe', NULL);
1✔
5807
            }
5808
        }
5809

5810
        # Remove all the content of all chat messages which they have sent (but not received).
5811
        $msgs = $this->dbhm->preQuery("SELECT id FROM chat_messages WHERE userid = ?;", [
7✔
5812
            $this->id
7✔
5813
        ]);
7✔
5814

5815
        foreach ($msgs as $msg) {
7✔
5816
            $this->dbhm->preExec("UPDATE chat_messages SET message = NULL WHERE id = ?;", [
1✔
5817
                $msg['id']
1✔
5818
            ]);
1✔
5819
        }
5820

5821
        # Delete completely any community events, volunteering opportunities, newsfeed posts, searches and stories
5822
        # they have created (their personal details might be in there), and any ratings by or about them.
5823
        $this->dbhm->preExec("DELETE FROM communityevents WHERE userid = ?;", [
7✔
5824
            $this->id
7✔
5825
        ]);
7✔
5826
        $this->dbhm->preExec("DELETE FROM volunteering WHERE userid = ?;", [
7✔
5827
            $this->id
7✔
5828
        ]);
7✔
5829
        $this->dbhm->preExec("DELETE FROM newsfeed WHERE userid = ?;", [
7✔
5830
            $this->id
7✔
5831
        ]);
7✔
5832
        $this->dbhm->preExec("DELETE FROM users_stories WHERE userid = ?;", [
7✔
5833
            $this->id
7✔
5834
        ]);
7✔
5835
        $this->dbhm->preExec("DELETE FROM users_searches WHERE userid = ?;", [
7✔
5836
            $this->id
7✔
5837
        ]);
7✔
5838
        $this->dbhm->preExec("DELETE FROM users_aboutme WHERE userid = ?;", [
7✔
5839
            $this->id
7✔
5840
        ]);
7✔
5841
        $this->dbhm->preExec("DELETE FROM ratings WHERE rater = ?;", [
7✔
5842
            $this->id
7✔
5843
        ]);
7✔
5844
        $this->dbhm->preExec("DELETE FROM ratings WHERE ratee = ?;", [
7✔
5845
            $this->id
7✔
5846
        ]);
7✔
5847

5848
        # Remove them from all groups.
5849
        $membs = $this->getMemberships();
7✔
5850

5851
        foreach ($membs as $memb) {
7✔
5852
            $this->removeMembership($memb['id']);
3✔
5853
        }
5854

5855
        # Delete any postal addresses
5856
        $this->dbhm->preExec("DELETE FROM users_addresses WHERE userid = ?;", [
7✔
5857
            $this->id
7✔
5858
        ]);
7✔
5859

5860
        # Delete any profile images
5861
        $this->dbhm->preExec("DELETE FROM users_images WHERE userid = ?;", [
7✔
5862
            $this->id
7✔
5863
        ]);
7✔
5864

5865
        # Remove any promises.
5866
        $this->dbhm->preExec("DELETE FROM messages_promises WHERE userid = ?;", [
7✔
5867
            $this->id
7✔
5868
        ]);
7✔
5869

5870
        $this->dbhm->preExec("UPDATE users SET forgotten = NOW(), tnuserid = NULL WHERE id = ?;", [
7✔
5871
            $this->id
7✔
5872
        ]);
7✔
5873

5874
        $this->dbhm->preExec("DELETE FROM sessions WHERE userid = ?;", [
7✔
5875
            $this->id
7✔
5876
        ]);
7✔
5877

5878
        $l = new Log($this->dbhr, $this->dbhm);
7✔
5879
        $l->log([
7✔
5880
            'type' => Log::TYPE_USER,
7✔
5881
            'subtype' => Log::SUBTYPE_DELETED,
7✔
5882
            'user' => $this->id,
7✔
5883
            'text' => $reason
7✔
5884
        ]);
7✔
5885
    }
5886

5887
    public function userRetention($userid = NULL)
5888
    {
5889
        # Find users who:
5890
        # - were added six months ago
5891
        # - are not on any groups
5892
        # - have not logged in for six months
5893
        # - are not on the spammer list
5894
        # - do not have mod notes
5895
        # - have no logs for six months
5896
        #
5897
        # We have no good reason to keep any data about them, and should therefore purge them.
5898
        $count = 0;
1✔
5899
        $userq = $userid ? " users.id = $userid AND " : '';
1✔
5900
        $mysqltime = date("Y-m-d", strtotime("6 months ago"));
1✔
5901
        $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✔
5902
        $users = $this->dbhr->preQuery($sql, [
1✔
5903
            User::SYSTEMROLE_USER
1✔
5904
        ]);
1✔
5905

5906
        foreach ($users as $user) {
1✔
5907
            $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✔
5908
                $user['id'],
1✔
5909
                Log::TYPE_USER,
1✔
5910
                Log::SUBTYPE_CREATED,
1✔
5911
                Log::SUBTYPE_DELETED
1✔
5912
            ]);
1✔
5913

5914
            error_log("#{$user['id']} Found logs " . count($logs) . " age " . (count($logs) > 0 ? $logs['0']['logsago'] : ' none '));
1✔
5915

5916
            if (count($logs) == 0 || $logs[0]['logsago'] > 90) {
1✔
5917
                error_log("...forget user #{$user['id']} " . (count($logs) > 0 ? $logs[0]['logsago'] : ''));
1✔
5918
                $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
5919
                $u->forget('Inactive');
1✔
5920
                $count++;
1✔
5921
            }
5922

5923
            # Prod garbage collection, as we've seen high memory usage by this.
5924
            User::clearCache();
1✔
5925
            gc_collect_cycles();
1✔
5926
        }
5927

5928
        # The only reason for preserving deleted users is as a placeholder user for messages they sent.  If they
5929
        # don't have any messages, they can go.
5930
        $ids = $this->dbhr->preQuery("SELECT users.id FROM `users` LEFT JOIN messages ON messages.fromuser = users.id WHERE users.forgotten IS NOT NULL AND users.lastaccess < ? AND messages.id IS NULL LIMIT 100000;", [
1✔
5931
            $mysqltime
1✔
5932
        ]);
1✔
5933

5934
        $total = count($ids);
1✔
5935
        $count = 0;
1✔
5936

5937
        foreach ($ids as $id) {
1✔
5938
            $u = new User($this->dbhr, $this->dbhm, $id['id']);
1✔
5939
            #error_log("...delete user #{$id['id']}");
5940
            $u->delete();
1✔
5941

5942
            $count++;
1✔
5943

5944
            if ($count % 1000 == 0) {
1✔
5945
                error_log("...delete $count / $total");
×
5946
            }
5947

5948

5949
            # Prod garbage collection, as we've seen high memory usage by this.
5950
            User::clearCache();
1✔
5951
            gc_collect_cycles();
1✔
5952
        }
5953

5954
        return ($count);
1✔
5955
    }
5956

5957
    public function recordActive()
5958
    {
5959
        # We record this on an hourly basis.  Avoid pointless mod ops for cluster health.
5960
        $now = date("Y-m-d H:00:00", time());
2✔
5961
        $already = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ? AND timestamp = ?;", [
2✔
5962
            $this->id,
2✔
5963
            $now
2✔
5964
        ]);
2✔
5965

5966
        if (count($already) == 0) {
2✔
5967
            $this->dbhm->background("INSERT IGNORE INTO users_active (userid, timestamp) VALUES ({$this->id}, '$now');");
2✔
5968
        }
5969
    }
5970

5971
    public function getActive()
5972
    {
5973
        $active = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ?;", [$this->id]);
1✔
5974
        return ($active);
1✔
5975
    }
5976

5977
    public function mostActive($gid, $limit = 20)
5978
    {
5979
        $limit = intval($limit);
1✔
5980
        $earliest = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
5981

5982
        $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✔
5983
            $gid,
1✔
5984
            User::SYSTEMROLE_USER,
1✔
5985
            $earliest
1✔
5986
        ]);
1✔
5987

5988
        $ret = [];
1✔
5989

5990
        foreach ($users as $user) {
1✔
5991
            $u = User::get($this->dbhr, $this->dbhm, $user['userid']);
1✔
5992
            $thisone = $u->getPublic();
1✔
5993
            $thisone['groupid'] = $gid;
1✔
5994
            $thisone['email'] = $u->getEmailPreferred();
1✔
5995

5996
            if (Utils::pres('memberof', $thisone)) {
1✔
5997
                foreach ($thisone['memberof'] as $group) {
1✔
5998
                    if ($group['id'] == $gid) {
1✔
5999
                        $thisone['joined'] = $group['added'];
1✔
6000
                    }
6001
                }
6002
            }
6003

6004
            $ret[] = $thisone;
1✔
6005
        }
6006

6007
        return ($ret);
1✔
6008
    }
6009

6010
    public function formatPhone($num)
6011
    {
6012
        $num = str_replace(' ', '', $num);
11✔
6013
        $num = preg_replace('/^(\+)?[04]+([^4])/', '$2', $num);
11✔
6014

6015
        if (substr($num, 0, 1) ==  '0') {
11✔
6016
            $num = substr($num, 1);
×
6017
        }
6018

6019
        $num = "+44$num";
11✔
6020

6021
        return ($num);
11✔
6022
    }
6023

6024
    public function sms($msg, $url, $from = TWILIO_FROM, $sid = TWILIO_SID, $auth = TWILIO_AUTH, $forcemsg = NULL)
6025
    {
6026
        # We only want to send SMS to people who are clicking on the links.  So if we've sent them one and they've
6027
        # not clicked on it, we stop.  This saves significant amounts of money.
6028
        $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)));", [
18✔
6029
            $this->id
18✔
6030
        ]);
18✔
6031

6032
        foreach ($phones as $phone) {
18✔
6033
            try {
6034
                $last = Utils::presdef('lastsent', $phone, NULL);
2✔
6035
                $last = $last ? strtotime($last) : NULL;
2✔
6036

6037
                # Only send one SMS per day.  This keeps the cost down.
6038
                if ($forcemsg || !$last || (time() - $last > 24 * 60 * 60)) {
2✔
6039
                    $client = new Client($sid, $auth);
2✔
6040

6041
                    $text = $forcemsg ? $forcemsg : "$msg Click $url Don't reply to this text.  No more texts sent today.";
2✔
6042
                    $rsp = $client->messages->create(
2✔
6043
                        $this->formatPhone($phone['number']),
2✔
6044
                        array(
2✔
6045
                            'from' => $from,
2✔
6046
                            'body' => $text,
2✔
6047
                            'statusCallback' => 'https://' . USER_SITE . '/twilio/status.php'
2✔
6048
                        )
2✔
6049
                    );
2✔
6050

6051
                    $this->dbhr->preExec("UPDATE users_phones SET lastsent = NOW(), count = count + 1, lastresponse = ? WHERE id = ?;", [
1✔
6052
                        $rsp->sid,
1✔
6053
                        $phone['id']
1✔
6054
                    ]);
1✔
6055
                    error_log("Sent SMS to {$phone['number']} result {$rsp->sid}");
1✔
6056
                } else {
6057
                    error_log("Don't send SMS to {$phone['number']}, too recent");
1✔
6058
                }
6059
            } catch (\Exception $e) {
2✔
6060
                error_log("Send to {$phone['number']} failed with " . $e->getMessage());
2✔
6061
                $this->dbhr->preExec("UPDATE users_phones SET lastsent = NOW(), lastresponse = ? WHERE id = ?;", [
2✔
6062
                    $e->getMessage(),
2✔
6063
                    $phone['id']
2✔
6064
                ]);
2✔
6065
            }
6066

6067
        }
6068
    }
6069

6070
    public function addPhone($phone)
6071
    {
6072
        $this->dbhm->preExec("REPLACE INTO users_phones (userid, number, valid) VALUES (?, ?, 1);", [
10✔
6073
            $this->id,
10✔
6074
            $this->formatPhone($phone),
10✔
6075
        ]);
10✔
6076

6077
        return($this->dbhm->lastInsertId());
10✔
6078
    }
6079

6080
    public function removePhone()
6081
    {
6082
        $this->dbhm->preExec("DELETE FROM users_phones WHERE userid = ?;", [
2✔
6083
            $this->id
2✔
6084
        ]);
2✔
6085
    }
6086

6087
    public function getPhone()
6088
    {
6089
        $ret = NULL;
21✔
6090
        $phones = $this->dbhr->preQuery("SELECT *, DATE(lastclicked) AS lastclicked, DATE(lastsent) AS lastsent FROM users_phones WHERE userid = ?;", [
21✔
6091
            $this->id
21✔
6092
        ]);
21✔
6093

6094
        foreach ($phones as $phone) {
21✔
6095
            $ret = [ $phone['number'], Utils::ISODate($phone['lastsent']), Utils::ISODate($phone['lastclicked']) ];
2✔
6096
        }
6097

6098
        return ($ret);
21✔
6099
    }
6100

6101
    public function setAboutMe($text) {
6102
        $this->dbhm->preExec("INSERT INTO users_aboutme (userid, text) VALUES (?, ?);", [
3✔
6103
            $this->id,
3✔
6104
            $text
3✔
6105
        ]);
3✔
6106

6107
        $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id = {$this->id};");
3✔
6108

6109
        return($this->dbhm->lastInsertId());
3✔
6110
    }
6111

6112
    public function rate($rater, $ratee, $rating, $reason = NULL, $text = NULL) {
6113
        $ret = NULL;
2✔
6114

6115
        if ($rater != $ratee) {
2✔
6116
            # Can't rate yourself.
6117
            $review = $rating == User::RATING_DOWN && $reason && $text;
2✔
6118
            $this->dbhm->preExec("REPLACE INTO ratings (rater, ratee, rating, reason, text, timestamp, reviewrequired) VALUES (?, ?, ?, ?, ?, NOW(), ?);", [
2✔
6119
                $rater,
2✔
6120
                $ratee,
2✔
6121
                $rating,
2✔
6122
                $reason,
2✔
6123
                $text,
2✔
6124
                $review
2✔
6125
            ]);
2✔
6126

6127
            $ret = $this->dbhm->lastInsertId();
2✔
6128

6129
            $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id IN ($rater, $ratee);");
2✔
6130
        }
6131

6132
        return($ret);
2✔
6133
    }
6134

6135
    public function getRatings($uids) {
6136
        $mysqltime = date("Y-m-d", strtotime("Midnight 182 days ago"));
121✔
6137
        $ret = [];
121✔
6138
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
121✔
6139
        $myid = $me ? $me->getId() : NULL;
121✔
6140

6141
        # We show visible ratings, ones we have made ourselves, or those from TN.
6142
        $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;";
121✔
6143
        $ratings = $this->dbhr->preQuery($sql, [ $myid, $myid ]);
121✔
6144

6145
        foreach ($uids as $uid) {
121✔
6146
            $ret[$uid] = [
121✔
6147
                User::RATING_UP => 0,
121✔
6148
                User::RATING_DOWN => 0,
121✔
6149
                User::RATING_MINE => NULL
121✔
6150
            ];
121✔
6151

6152
            foreach ($ratings as $rate) {
121✔
6153
                if ($rate['ratee'] == $uid) {
1✔
6154
                    $ret[$uid][$rate['rating']] = $rate['count'];
1✔
6155
                }
6156
            }
6157
        }
6158

6159
        $ratings = $this->dbhr->preQuery("SELECT rating, ratee FROM ratings WHERE ratee IN (" . implode(',', $uids) . ") AND rater = ? AND timestamp >= '$mysqltime';", [
121✔
6160
            $myid
121✔
6161
        ]);
121✔
6162

6163
        foreach ($uids as $uid) {
121✔
6164
            if ($myid != $this->id) {
121✔
6165
                # We can't rate ourselves, so don't bother checking.
6166

6167
                foreach ($ratings as $rating) {
79✔
6168
                    if ($rating['ratee'] == $uid) {
1✔
6169
                        $ret[$uid][User::RATING_MINE] = $rating['rating'];
1✔
6170
                    }
6171
                }
6172
            }
6173
        }
6174

6175
        return($ret);
121✔
6176
    }
6177

6178
    public function getAllRatings($since) {
6179
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6180

6181
        $sql = "SELECT * FROM ratings WHERE timestamp >= ? AND visible = 1;";
1✔
6182
        $ratings = $this->dbhr->preQuery($sql, [
1✔
6183
            $mysqltime
1✔
6184
        ]);
1✔
6185

6186
        foreach ($ratings as &$rating) {
1✔
6187
            $rating['timestamp'] = Utils::ISODate($rating['timestamp']);
1✔
6188
        }
6189

6190
        return $ratings;
1✔
6191
    }
6192

6193
    public function getVisibleRatings($unreviewedonly, $since = '7 days ago') {
6194
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
3✔
6195
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
6196

6197
        $modships = $me->getModeratorships(NULL, TRUE);
3✔
6198

6199
        $ret = [];
3✔
6200
        $revq = $unreviewedonly ? " AND reviewrequired = 1" : '';
3✔
6201

6202
        if (count($modships)) {
3✔
6203
            $sql = "SELECT ratings.*, m1.groupid,
2✔
6204
       CASE WHEN u1.fullname IS NOT NULL THEN u1.fullname ELSE CONCAT(u1.firstname, ' ', u1.lastname) END AS raterdisplayname,
6205
       CASE WHEN u2.fullname IS NOT NULL THEN u2.fullname ELSE CONCAT(u2.firstname, ' ', u2.lastname) END AS rateedisplayname
6206
    FROM ratings 
6207
    INNER JOIN memberships m1 ON m1.userid = ratings.rater
6208
    INNER JOIN memberships m2 ON m2.userid = ratings.ratee
6209
    INNER JOIN users u1 ON ratings.rater = u1.id
6210
    INNER JOIN users u2 ON ratings.ratee = u2.id
6211
    WHERE ratings.timestamp >= ? AND 
6212
        m1.groupid IN (" . implode(',', $modships) . ") AND
2✔
6213
        m2.groupid IN (" . implode(',', $modships) . ") AND
2✔
6214
        m1.groupid = m2.groupid AND
6215
        ratings.rating IS NOT NULL 
6216
        $revq    
2✔
6217
        GROUP BY ratings.rater ORDER BY ratings.timestamp DESC;";
2✔
6218

6219
            $ret = $this->dbhr->preQuery($sql, [
2✔
6220
                $mysqltime
2✔
6221
            ]);
2✔
6222

6223
            foreach ($ret as &$r) {
2✔
6224
                $r['timestamp'] = Utils::ISODate($r['timestamp']);
1✔
6225
            }
6226
        }
6227

6228
        return $ret;
3✔
6229
    }
6230

6231
    public function ratingReviewed($ratingid) {
6232
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6233

6234
        $unreviewed = $me->getVisibleRatings(TRUE);
1✔
6235

6236
        foreach ($unreviewed as $r) {
1✔
6237
            if ($r['id'] == $ratingid) {
1✔
6238
                $this->dbhm->preExec("UPDATE ratings SET reviewrequired = 0 WHERE id = ?;", [
1✔
6239
                    $ratingid
1✔
6240
                ]);
1✔
6241
            }
6242
        }
6243
    }
6244

6245
    public function getChanges($since) {
6246
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6247

6248
        $users = $this->dbhr->preQuery("SELECT id, lastupdated FROM users WHERE lastupdated >= ?;", [
1✔
6249
            $mysqltime
1✔
6250
        ]);
1✔
6251

6252
        foreach ($users as &$user) {
1✔
6253
            $user['lastupdated'] = Utils::ISODate($user['lastupdated']);
1✔
6254
        }
6255

6256
        return $users;
1✔
6257
    }
6258

6259
    public function getRated() {
6260
        $rateds = $this->dbhr->preQuery("SELECT * FROM ratings WHERE rater = ?;", [
8✔
6261
            $this->id
8✔
6262
        ]);
8✔
6263

6264
        foreach ($rateds as &$rate) {
8✔
6265
            $rate['timestamp'] = Utils::ISODate($rate['timestamp']);
1✔
6266
        }
6267

6268
        return($rateds);
8✔
6269
    }
6270

6271
    public function getActiveSince($since, $createdbefore) {
6272
        $sincetime = date("Y-m-d H:i:s", strtotime($since));
1✔
6273
        $beforetime = date("Y-m-d H:i:s", strtotime($createdbefore));
1✔
6274
        $ids = $this->dbhr->preQuery("SELECT id FROM users WHERE lastaccess >= ? AND added <= ?;", [
1✔
6275
            $sincetime,
1✔
6276
            $beforetime
1✔
6277
        ]);
1✔
6278

6279
        return(count($ids) ? array_filter(array_column($ids, 'id')) : []);
1✔
6280
    }
6281

6282
    public static function encodeId($id) {
6283
        # We're told that this is affecting our spam rating.  Let's see.
6284
        return '';
9✔
6285
//        $bin = base_convert($id, 10, 2);
6286
//        $bin = str_replace('0', '-', $bin);
6287
//        $bin = str_replace('1', '~', $bin);
6288
//        return($bin);
6289
    }
6290

6291
    public static function decodeId($enc) {
6292
        $enc = trim($enc);
×
6293
        $enc = str_replace('-', '0', $enc);
×
6294
        $enc = str_replace('~', '1', $enc);
×
6295
        $id  = base_convert($enc, 2, 10);
×
6296
        return($id);
×
6297
    }
6298

6299
    public function getCity()
6300
    {
6301
        $city = NULL;
23✔
6302

6303
        # Find the closest town
6304
        list ($lat, $lng, $loc) = $this->getLatLng(FALSE, TRUE);
23✔
6305

6306
        if ($lat || $lng) {
23✔
6307
            $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✔
6308
            #error_log("Get $sql, $lng, $lat");
6309
            $towns = $this->dbhr->preQuery($sql);
1✔
6310

6311
            foreach ($towns as $town) {
1✔
6312
                $city = $town['name'];
1✔
6313
            }
6314
        }
6315

6316
        return([ $city, $lat, $lng ]);
23✔
6317
    }
6318

6319
    public function microVolunteering() {
6320
        // Are we on a group where microvolunteering is enabled.
6321
        $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✔
6322
            $this->id
22✔
6323
        ]);
22✔
6324

6325
        return count($groups);
22✔
6326
    }
6327

6328
    public function getJobAds() {
6329
        # We want to show a few job ads from nearby.
6330
        $search = NULL;
33✔
6331
        $ret = '<span class="jobads">';
33✔
6332

6333
        list ($lat, $lng) = $this->getLatLng();
33✔
6334

6335
        if ($lat || $lng) {
33✔
6336
            $j = new Jobs($this->dbhr, $this->dbhm);
5✔
6337
            $jobs = $j->query($lat, $lng, 4);
5✔
6338

6339
            foreach ($jobs as $job) {
5✔
6340
                $loc = Utils::presdef('location', $job, '');
3✔
6341
                $title = "{$job['title']}" . ($loc !== ' ' ? " ($loc)" : '');
3✔
6342

6343
                # Link via our site to avoid spam trap warnings.
6344
                $url = "https://" . USER_SITE . "/job/{$job['id']}";
3✔
6345
                $ret .= '<a href="' . $url . '" target="_blank" style="color:black; font-weight:bold;">' . htmlentities($title) . '</a><br />';
3✔
6346
            }
6347
        }
6348

6349
        $ret .= '</span>';
33✔
6350

6351
        return([
33✔
6352
            'location' => $search,
33✔
6353
            'jobs' => $ret
33✔
6354
        ]);
33✔
6355
    }
6356

6357
    public function updateModMails($uid = NULL) {
6358
        # We maintain a count of recent modmails by scanning logs regularly, and pruning old ones.  This means we can
6359
        # find the value in a well-indexed way without the disk overhead of having a two-column index on logs.
6360
        #
6361
        # Ignore logs where the user is the same as the byuser - for example a user can delete their own posts, and we are
6362
        # only interested in things where a mod has done something to another user.
6363
        $mysqltime = date("Y-m-d H:i:s", strtotime("10 minutes ago"));
1✔
6364
        $uidq = $uid ? " AND user = $uid " : '';
1✔
6365

6366
        $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✔
6367
            $mysqltime
1✔
6368
        ]);
1✔
6369

6370
        foreach ($logs as $log) {
1✔
6371
            $this->dbhm->preExec("INSERT IGNORE INTO users_modmails (userid, logid, timestamp, groupid) VALUES (?,?,?,?);", [
1✔
6372
                $log['user'],
1✔
6373
                $log['id'],
1✔
6374
                $log['timestamp'],
1✔
6375
                $log['groupid']
1✔
6376
            ]);
1✔
6377
        }
6378

6379
        # Prune old ones.
6380
        $mysqltime = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6381
        $uidq2 = $uid ? " AND userid = $uid " : '';
1✔
6382

6383
        $logs = $this->dbhr->preQuery("SELECT id FROM users_modmails WHERE timestamp < ? $uidq2;", [
1✔
6384
            $mysqltime
1✔
6385
        ]);
1✔
6386

6387
        foreach ($logs as $log) {
1✔
6388
            $this->dbhm->preExec("DELETE FROM users_modmails WHERE id = ?;", [ $log['id'] ], FALSE);
×
6389
        }
6390
    }
6391

6392
    public function getModGroupsByActivity() {
6393
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
6394
        $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✔
6395
        return $this->dbhr->preQuery($sql, [
1✔
6396
            $this->id
1✔
6397
        ]);
1✔
6398
    }
6399

6400
    public function related($userlist) {
6401
        $userlist = array_unique($userlist);
2✔
6402

6403
        foreach ($userlist as $user1) {
2✔
6404
            foreach ($userlist as $user2) {
2✔
6405
                if ($user1 && $user2 && $user1 !== $user2) {
2✔
6406
                    # We may be passed user ids which no longer exist.
6407
                    $u1 = User::get($this->dbhr, $this->dbhm, $user1);
2✔
6408
                    $u2 = User::get($this->dbhr, $this->dbhm, $user2);
2✔
6409

6410
                    if ($u1->getId() && $u2->getId() && !$u1->isAdminOrSupport() && !$u2->isAdminOrSupport()) {
2✔
6411
                        $this->dbhm->background("INSERT INTO users_related (user1, user2) VALUES ($user1, $user2) ON DUPLICATE KEY UPDATE timestamp = NOW();");
2✔
6412
                    }
6413
                }
6414
            }
6415
        }
6416
    }
6417

6418
    public function getRelated($userid, $since = "30 days ago") {
6419
        $starttime = date("Y-m-d H:i:s", strtotime($since));
1✔
6420
        $users = $this->dbhr->preQuery("SELECT * FROM users_related WHERE user1 = ? AND timestamp >= '$starttime';", [
1✔
6421
            $userid
1✔
6422
        ]);
1✔
6423

6424
        return ($users);
1✔
6425
    }
6426

6427
    public function listRelated($groupids, &$ctx, $limit = 10) {
6428
        # The < condition ensures we don't duplicate during a single run.
6429
        $limit = intval($limit);
1✔
6430
        $ret = [];
1✔
6431
        $backstop = 100;
1✔
6432

6433
        do {
6434
            $ctx = $ctx ? $ctx : [ 'id'  => NULL ];
1✔
6435

6436
            if ($groupids && count($groupids)) {
1✔
6437
                $ctxq = ($ctx && intval($ctx['id'])) ? (" WHERE id < " . intval($ctx['id'])) : '';
1✔
6438
                $groupq = "(" . implode(',', $groupids) . ")";
1✔
6439
                $sql = "SELECT DISTINCT id, user1, user2 FROM (
1✔
6440
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6441
INNER JOIN memberships ON users_related.user1 = memberships.userid 
6442
INNER JOIN users u1 ON users_related.user1 = u1.id AND u1.deleted IS NULL AND u1.systemrole = 'User'
6443
WHERE 
6444
user1 < user2 AND
6445
notified = 0 AND
6446
memberships.groupid IN $groupq UNION
1✔
6447
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6448
INNER JOIN memberships ON users_related.user2 = memberships.userid 
6449
INNER JOIN users u2 ON users_related.user2 = u2.id AND u2.deleted IS NULL AND u2.systemrole = 'User'
6450
WHERE 
6451
user1 < user2 AND
6452
notified = 0 AND
6453
memberships.groupid IN $groupq 
1✔
6454
) t $ctxq ORDER BY id DESC LIMIT $limit;";
1✔
6455
                $members = $this->dbhr->preQuery($sql);
1✔
6456
            } else {
6457
                $ctxq = ($ctx && intval($ctx['id'])) ? (" AND users_related.id < " . intval($ctx['id'])) : '';
1✔
6458
                $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✔
6459
                $members = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
1✔
6460
            }
6461

6462
            $uids1 = array_column($members, 'user1');
1✔
6463
            $uids2 = array_column($members, 'user2');
1✔
6464

6465
            $related = [];
1✔
6466
            foreach ($members as $member) {
1✔
6467
                $related[$member['user1']] = $member['user2'];
1✔
6468
                $ctx['id'] = $member['id'];
1✔
6469
            }
6470

6471
            $users = $this->getPublicsById(array_merge($uids1, $uids2));
1✔
6472

6473
            foreach ($users as &$user1) {
1✔
6474
                if (Utils::pres($user1['id'], $related)) {
1✔
6475
                    $thisone = $user1;
1✔
6476

6477
                    foreach ($users as $user2) {
1✔
6478
                        if ($user2['id'] == $related[$user1['id']]) {
1✔
6479
                            $user2['userid'] = $user2['id'];
1✔
6480
                            $thisone['relatedto'] = $user2;
1✔
6481
                            break;
1✔
6482
                        }
6483
                    }
6484

6485
                    $logins = $this->getLogins(FALSE, $thisone['id'], TRUE);
1✔
6486
                    $rellogins = $this->getLogins(FALSE, $thisone['relatedto']['id'], TRUE);
1✔
6487

6488
                    if ($thisone['deleted'] ||
1✔
6489
                        $thisone['relatedto']['deleted'] ||
1✔
6490
                        $thisone['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6491
                        $thisone['relatedto']['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6492
                        !count($logins) ||
1✔
6493
                        !count($rellogins)) {
1✔
6494
                        # No sense in telling people about these.
6495
                        #
6496
                        # If there are n valid login types for one of the users - no way they can log in again so no point notifying.
6497
                        $this->dbhm->preExec("UPDATE users_related SET notified = 1 WHERE (user1 = ? AND user2 = ?) OR (user1 = ? AND user2 = ?);", [
1✔
6498
                            $thisone['id'],
1✔
6499
                            $thisone['relatedto']['id'],
1✔
6500
                            $thisone['relatedto']['id'],
1✔
6501
                            $thisone['id']
1✔
6502
                        ]);
1✔
6503
                    } else {
6504
                        $thisone['userid'] = $thisone['id'];
1✔
6505
                        $thisone['logins'] = $logins;
1✔
6506
                        $thisone['relatedto']['logins'] = $rellogins;
1✔
6507

6508
                        $ret[] = $thisone;
1✔
6509
                    }
6510
                }
6511
            }
6512

6513
            $backstop--;
1✔
6514
        } while ($backstop > 0 && count($ret) < $limit && count($members));
1✔
6515

6516
        return $ret;
1✔
6517
    }
6518

6519
    public function getExpectedReplies($uids, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6520
        # We count replies where the user has been active since the reply was requested, which means they've had
6521
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6522
        #
6523
        # $since here has to match the value in ChatRoom::
6524
        $starttime = date("Y-m-d H:i:s", strtotime($since));
121✔
6525
        $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) >= ?", [
121✔
6526
            $grace
121✔
6527
        ]);
121✔
6528

6529
        return($replies);
121✔
6530
    }
6531

6532
    public function listExpectedReplies($uid, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6533
        # We count replies where the user has been active since the reply was requested, which means they've had
6534
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6535
        #
6536
        # $since here has to match the value in ChatRoom::
6537
        $starttime = date("Y-m-d H:i:s", strtotime($since));
21✔
6538
        $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✔
6539
            $uid,
21✔
6540
            $grace
21✔
6541
        ]);
21✔
6542

6543
        $ret = [];
21✔
6544

6545
        if (count($replies)) {
21✔
6546
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6547
            $myid = $me ? $me->getId() : NULL;
1✔
6548

6549
            $r = new ChatRoom($this->dbhr, $this->dbhm);
1✔
6550
            $rooms = $r->fetchRooms(array_column($replies, 'chatid'), $myid, TRUE);
1✔
6551

6552
            foreach ($rooms as $room) {
1✔
6553
                $ret[] = [
1✔
6554
                    'id' => $room['id'],
1✔
6555
                    'name' => $room['name']
1✔
6556
                ];
1✔
6557
            }
6558
        }
6559

6560
        return $ret;
21✔
6561
    }
6562
    
6563
    public function getWorkCounts($groups = NULL) {
6564
        # Tell them what mod work there is.  Similar code in Notifications.
6565
        $ret = [];
26✔
6566
        $total = 0;
26✔
6567

6568
        $national = $this->hasPermission(User::PERM_NATIONAL_VOLUNTEERS);
26✔
6569

6570
        if ($national) {
26✔
6571
            $v = new Volunteering($this->dbhr, $this->dbhm);
1✔
6572
            $ret['pendingvolunteering'] = $v->systemWideCount();
1✔
6573
        }
6574

6575
        $s = new Spam($this->dbhr, $this->dbhm);
26✔
6576
        $spamcounts = $s->collectionCounts();
26✔
6577
        $ret['spammerpendingadd'] = $spamcounts[Spam::TYPE_PENDING_ADD];
26✔
6578
        $ret['spammerpendingremove'] = $spamcounts[Spam::TYPE_PENDING_REMOVE];
26✔
6579

6580
        # Show social actions from last 4 days.
6581
        $ctx = NULL;
26✔
6582
        $f = new GroupFacebook($this->dbhr, $this->dbhm);
26✔
6583
        $ret['socialactions'] = count($f->listSocialActions($ctx,$this));
26✔
6584

6585
        $g = new Group($this->dbhr, $this->dbhm);
26✔
6586
        $ret['popularposts'] = count($g->getPopularMessages());
26✔
6587

6588
        if ($this->hasPermission(User::PERM_GIFTAID)) {
26✔
6589
            $d = new Donations($this->dbhr, $this->dbhm);
1✔
6590
            $ret['giftaid'] = $d->countGiftAidReview();
1✔
6591
        }
6592

6593
        if (!$groups) {
26✔
6594
            $groups = $this->getMemberships(FALSE, NULL, TRUE, TRUE, $this->id, FALSE);
12✔
6595
        }
6596

6597
        foreach ($groups as &$group) {
26✔
6598
            if (Utils::pres('work', $group)) {
19✔
6599
                foreach ($group['work'] as $key => $work) {
17✔
6600
                    if (Utils::pres($key, $ret)) {
17✔
6601
                        $ret[$key] += $work;
2✔
6602
                    } else {
6603
                        $ret[$key] = $work;
17✔
6604
                    }
6605
                }
6606
            }
6607
        }
6608

6609
        $s = new Story($this->dbhr, $this->dbhm);
26✔
6610
        $ret['stories'] = $s->getReviewCount(FALSE, $this, $groups);
26✔
6611
        $ret['newsletterstories'] = $this->hasPermission(User::PERM_NEWSLETTER) ? $s->getReviewCount(TRUE) : 0;
26✔
6612

6613
        // All the types of work which are worth nagging about.
6614
        $worktypes = [
26✔
6615
            'pendingvolunteering',
26✔
6616
            'socialactions',
26✔
6617
            'popularposts',
26✔
6618
            'chatreview',
26✔
6619
            'relatedmembers',
26✔
6620
            'stories',
26✔
6621
            'newsletterstories',
26✔
6622
            'pending',
26✔
6623
            'spam',
26✔
6624
            'pendingmembers',
26✔
6625
            'pendingevents',
26✔
6626
            'spammembers',
26✔
6627
            'editreview',
26✔
6628
            'pendingadmins'
26✔
6629
        ];
26✔
6630

6631
        if ($this->isAdminOrSupport()) {
26✔
6632
            $worktypes[] = 'spammerpendingadd';
1✔
6633
            $worktypes[] = 'spammerpendingremove';
1✔
6634
        }
6635

6636
        foreach ($worktypes as $key) {
26✔
6637
            $total += Utils::presdef($key, $ret, 0);
26✔
6638
        }
6639

6640
        $ret['total'] = $total;
26✔
6641

6642
        return $ret;
26✔
6643
    }
6644

6645
    public function ratingVisibility($since = "1 hour ago") {
6646
        $mysqltime = date("Y-m-d", strtotime($since));
1✔
6647

6648
        $ratings = $this->dbhr->preQuery("SELECT * FROM ratings WHERE timestamp >= ?;", [
1✔
6649
            $mysqltime
1✔
6650
        ]);
1✔
6651

6652
        foreach ($ratings as $rating) {
1✔
6653
            # A rating is visible to others if there is a chat between the two members, and
6654
            # - the ratee replied to a post, or
6655
            # - there is at least one message from each of them.
6656
            # This means that has been an exchange substantial enough for the rating not to just be frivolous.  It
6657
            # deliberately excludes interactions on ChitChat, where we have seen some people go a bit overboard on
6658
            # rating people.
6659
            $visible = FALSE;
1✔
6660
            #error_log("Check {$rating['rater']} rating of {$rating['ratee']}");
6661

6662
            $chats = $this->dbhr->preQuery("SELECT id FROM chat_rooms WHERE (user1 = ? AND user2 = ?) OR (user2 = ? AND user1 = ?)", [
1✔
6663
                $rating['rater'],
1✔
6664
                $rating['ratee'],
1✔
6665
                $rating['rater'],
1✔
6666
                $rating['ratee'],
1✔
6667
            ]);
1✔
6668

6669
            foreach ($chats as $chat) {
1✔
6670
                $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✔
6671
                    $chat['id']
1✔
6672
                ]);
1✔
6673

6674
                if ($distincts[0]['count'] >= 2) {
1✔
6675
                    #error_log("At least one real message from each of them in {$chat['id']}");
6676
                    $visible = TRUE;
1✔
6677
                } else {
6678
                    $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✔
6679
                        $chat['id'],
1✔
6680
                        $rating['ratee']
1✔
6681
                    ]);
1✔
6682

6683
                    if ($replies[0]['count']) {
1✔
6684
                        #error_log("Significant reply from {$rating['ratee']} in {$chat['id']}");
6685
                        $visible = TRUE;
1✔
6686
                    }
6687
                }
6688
            }
6689

6690
            #error_log("Use {$rating['rating']} from {$rating['rater']} ? " . ($visible ? 'yes': 'no'));
6691
            $oldvisible = intval($rating['visible']) ? TRUE : FALSE;
1✔
6692

6693
            if ($visible != $oldvisible) {
1✔
6694
                $this->dbhm->preExec("UPDATE ratings SET visible = ?, timestamp = NOW() WHERE id = ?;", [
1✔
6695
                    $visible,
1✔
6696
                    $rating['id']
1✔
6697
                ]);
1✔
6698
            }
6699
        }
6700
    }
6701

6702
    public function unban($groupid) {
6703
        $this->dbhm->preExec("DELETE FROM users_banned WHERE userid = ? AND groupid = ?;", [
4✔
6704
            $this->id,
4✔
6705
            $groupid
4✔
6706
        ]);
4✔
6707
    }
6708

6709
    public function hasFacebookLogin() {
6710
        $logins = $this->getLogins();
4✔
6711
        $ret = FALSE;
4✔
6712

6713
        foreach ($logins as $login) {
4✔
6714
            if ($login['type'] == User::LOGIN_FACEBOOK) {
4✔
6715
                $ret = TRUE;
1✔
6716
            }
6717
        }
6718

6719
        return $ret;
4✔
6720
    }
6721

6722
    public function memberReview($groupid, $request, $reason) {
6723
        $mysqltime = date('Y-m-d H:i');
6✔
6724

6725
        if ($request) {
6✔
6726
            # Requesting review.  Leave reviewedat unchanged, so that we can use it to avoid asking too
6727
            # frequently.
6728
            $this->setMembershipAtt($groupid, 'reviewreason', $reason);
4✔
6729
            $this->setMembershipAtt($groupid, 'reviewrequestedat', $mysqltime);
4✔
6730
        } else {
6731
            # We have reviewed.  Note that they might have been removed, in which case the set will do nothing.
6732
            $this->setMembershipAtt($groupid, 'reviewrequestedat', NULL);
3✔
6733
            $this->setMembershipAtt($groupid, 'reviewedat', $mysqltime);
3✔
6734
        }
6735
    }
6736

6737
    private function checkSupporterSettings($settings) {
6738
        $ret = TRUE;
77✔
6739

6740
        if ($settings) {
77✔
6741
            $s = json_decode($settings, TRUE);
13✔
6742

6743
            if ($s && array_key_exists('hidesupporter', $s)) {
13✔
6744
                $ret = !$s['hidesupporter'];
1✔
6745
            }
6746
        }
6747

6748
        return $ret;
77✔
6749
    }
6750

6751
    public function getSupporters(&$rets, $users) {
6752
        $idsleft = [];
277✔
6753

6754
        foreach ($rets as $userid => $ret) {
277✔
6755
            if (Utils::pres($userid, $users)) {
247✔
6756
                if (array_key_exists('supporter', $users[$userid])) {
10✔
6757
                    $rets[$userid]['supporter'] = $users[$userid]['supporter'];
3✔
6758
                    $rets[$userid]['donor'] = Utils::presdef('donor', $users[$userid], FALSE);
10✔
6759
                }
6760
            } else {
6761
                $idsleft[] = $userid;
243✔
6762
            }
6763
        }
6764

6765
        if (count($idsleft)) {
277✔
6766
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
243✔
6767
            $myid = $me ? $me->getId() : null;
243✔
6768

6769
            # A supporter is a mod, someone who has donated recently, or done microvolunteering recently.
6770
            if (count($idsleft)) {
243✔
6771
                $start = date('Y-m-d', strtotime("360 days ago"));
243✔
6772
                $info = $this->dbhr->preQuery(
243✔
6773
                    "SELECT DISTINCT users.id AS userid, settings, systemrole FROM users 
243✔
6774
    LEFT JOIN microactions ON users.id = microactions.userid
6775
    LEFT JOIN users_donations ON users_donations.userid = users.id 
6776
    WHERE users.id IN (" . implode(
243✔
6777
                        ',',
243✔
6778
                        $idsleft
243✔
6779
                    ) . ") AND 
243✔
6780
                    (systemrole IN (?, ?, ?) OR microactions.timestamp >= ? OR users_donations.timestamp >= ?);",
243✔
6781
                    [
243✔
6782
                        User::SYSTEMROLE_ADMIN,
243✔
6783
                        User::SYSTEMROLE_SUPPORT,
243✔
6784
                        User::SYSTEMROLE_MODERATOR,
243✔
6785
                        $start,
243✔
6786
                        $start
243✔
6787
                    ]
243✔
6788
                );
243✔
6789

6790
                $found = [];
243✔
6791

6792
                foreach ($info as $i) {
243✔
6793
                    $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
77✔
6794
                    $found[] = $i['userid'];
77✔
6795
                }
6796

6797
                $left = array_diff($idsleft, $found);
243✔
6798

6799
                # If we are one of the users, then we want to return whether we are a donor.
6800
                if (in_array($myid, $idsleft)) {
243✔
6801
                    $left[] = $myid;
141✔
6802
                    $left = array_filter(array_unique($left));
141✔
6803
                }
6804

6805
                if (count($left)) {
243✔
6806
                    $info = $this->dbhr->preQuery(
241✔
6807
                        "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(
241✔
6808
                            ',',
241✔
6809
                            $left
241✔
6810
                        ) . ") GROUP BY TransactionType;",
241✔
6811
                        [
241✔
6812
                            $start
241✔
6813
                        ]
241✔
6814
                    );
241✔
6815

6816
                    foreach ($info as $i) {
241✔
6817
                        $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
3✔
6818

6819
                        if ($i['userid'] == $myid) {
3✔
6820
                            # Only return this info for ourselves, otherwise it's a privacy leak.
6821
                            $rets[$i['userid']]['donor'] = TRUE;
3✔
6822

6823
                            if ($i['TransactionType'] == 'subscr_payment' || $i['TransactionType'] == 'recurring_payment') {
3✔
6824
                                $rets[$i['userid']]['donorrecurring'] = TRUE;
1✔
6825
                            }
6826
                        }
6827
                    }
6828
                }
6829
            }
6830
        }
6831
    }
6832

6833
    public function obfuscateEmail($email) {
6834
        $p = strpos($email, '@');
2✔
6835
        $q = strpos($email, 'privaterelay.appleid.com');
2✔
6836

6837
        if ($q) {
2✔
6838
            $email = 'Your Apple ID';
1✔
6839
        } else {
6840
            # For very short emails, we just show the first character.
6841
            if ($p <= 3) {
2✔
6842
                $email = substr($email, 0, 1) . "***" . substr($email, $p);
1✔
6843
            } else if ($p < 10) {
2✔
6844
                $email = substr($email, 0, 1) . "***" . substr($email, $p - 1);
2✔
6845
            } else {
6846
                $email = substr($email, 0, 3) . "***" . substr($email, $p - 3);
1✔
6847
            }
6848
        }
6849

6850
        return $email;
2✔
6851
    }
6852

6853
    public function setSimpleMail($simplemail) {
6854
        $s = $this->getPrivate('settings');
2✔
6855

6856
        if ($s) {
2✔
6857
            $settings = json_decode($s, TRUE);
×
6858
        } else {
6859
            $settings = [];
2✔
6860
        }
6861

6862
        $this->dbhm->beginTransaction();
2✔
6863

6864
        switch ($simplemail) {
6865
            case User::SIMPLE_MAIL_NONE: {
6866
                # No digests, no events/volunteering.
6867
                # No relevant or newsletters.
6868
                # No email notifications.
6869
                # No enagement.
6870
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 0, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
2✔
6871
                    $this->id
2✔
6872
                ]);
2✔
6873

6874
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
2✔
6875
                    $this->id
2✔
6876
                ]);
2✔
6877

6878
                $settings['notifications']['email'] = false;
2✔
6879
                $settings['notifications']['emailmine'] = false;
2✔
6880
                $settings['notificationmails']= false;
2✔
6881
                $settings['engagement'] = false;
2✔
6882
                break;
2✔
6883
            }
6884
            case User::SIMPLE_MAIL_BASIC: {
6885
                # Daily digests, no events/volunteering.
6886
                # No relevant or newsletters.
6887
                # Chat email notifications.
6888
                # No enagement.
6889
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 24, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
1✔
6890
                    $this->id
1✔
6891
                ]);
1✔
6892

6893
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
1✔
6894
                    $this->id
1✔
6895
                ]);
1✔
6896

6897
                $settings['notifications']['email'] = true;
1✔
6898
                $settings['notifications']['emailmine'] = false;
1✔
6899
                $settings['notificationmails']= false;
1✔
6900
                $settings['engagement']= false;
1✔
6901
                break;
1✔
6902
            }
6903
            case User::SIMPLE_MAIL_FULL: {
6904
                # Immediate mails, events/volunteering.
6905
                # Relevant and newsletters.
6906
                # Email notifications.
6907
                # Enagement.
6908
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = -1, eventsallowed = 1, volunteeringallowed = 1 WHERE userid = ?;", [
1✔
6909
                    $this->id
1✔
6910
                ]);
1✔
6911

6912
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 1,  newslettersallowed = 1 WHERE id = ?;", [
1✔
6913
                    $this->id
1✔
6914
                ]);
1✔
6915

6916
                $settings['notifications']['email'] = true;
1✔
6917
                $settings['notifications']['emailmine'] = false;
1✔
6918
                $settings['notificationmails']= true;
1✔
6919
                $settings['engagement']= true;
1✔
6920
                break;
1✔
6921
            }
6922
        }
6923

6924
        $settings['simplemail'] = $simplemail;
2✔
6925

6926
        # Holiday no longer exposed so turn off.
6927
        $this->dbhm->preExec("UPDATE users SET onholidaytill = NULL, settings = ? WHERE id = ?;", [
2✔
6928
            json_encode($settings),
2✔
6929
            $this->id
2✔
6930
        ]);
2✔
6931

6932
        $this->dbhm->commit();
2✔
6933
    }
6934

6935
    public function processMemberships($g = NULL) {
6936
        $memberships = $this->dbhr->preQuery("SELECT id FROM `memberships_history` WHERE processingrequired = 1 ORDER BY id ASC;");
6✔
6937

6938
        foreach ($memberships as $membership) {
6✔
6939
            $this->processMembership($membership['id'], $g);
6✔
6940
        }
6941
    }
6942

6943
    public function processMembership($id, $g) {
6944
        $memberships = $this->dbhr->preQuery("SELECT * FROM memberships_history WHERE id = ?;",[
6✔
6945
            $id
6✔
6946
        ]);
6✔
6947

6948
        foreach ($memberships as $membership) {
6✔
6949
            $groupid = $membership['groupid'];
6✔
6950
            $userid = $membership['userid'];
6✔
6951
            $collection = $membership['collection'];
6✔
6952

6953
            $g = $g ? $g : Group::get($this->dbhr, $this->dbhm, $groupid);
6✔
6954

6955
            # The membership didn't already exist.  We might want to send a welcome mail.
6956
            if ($g->getPrivate('welcomemail') && $collection == MembershipCollection::APPROVED && $g->getPrivate('onhere')) {
6✔
6957
                # They are now approved.  We need to send a per-group welcome mail.
6958
                try {
6959
                    error_log(date("Y-m-d H:i:s") . "Send welcome to $userid for membership $id\n");
2✔
6960
                    $g->sendWelcome($userid, false);
2✔
6961
                } catch (Exception $e) {
×
6962
                    error_log("Welcome failed: " . $e->getMessage());
×
6963
                    \Sentry\captureException($e);
×
6964
                }
6965
            }
6966

6967
            # Check whether this user now counts as a possible spammer.
6968
            $s = new Spam($this->dbhr, $this->dbhm);
6✔
6969
            $s->checkUser($userid, $groupid);
6✔
6970

6971
            # We might have mod notes which require this member to be flagged up.
6972
            $comments = $this->dbhr->preQuery(
6✔
6973
                "SELECT COUNT(*) AS count FROM users_comments WHERE userid = ? AND flag = 1;", [
6✔
6974
                    $userid,
6✔
6975
                ]
6✔
6976
            );
6✔
6977

6978
            if ($comments[0]['count'] > 0) {
6✔
6979
                $this->memberReview($groupid, true, 'Note flagged to other groups');
1✔
6980
            }
6981

6982
            $this->dbhm->preExec("UPDATE memberships_history SET processingrequired = 0 WHERE id = ?", [
6✔
6983
                $id
6✔
6984
            ]);
6✔
6985
        }
6986
    }
6987

6988
    public function getUserKey($id) {
6989
        $key = null;
92✔
6990
        $sql = "SELECT * FROM users_logins WHERE userid = ? AND type = ?;";
92✔
6991
        $logins = $this->dbhr->preQuery($sql, [$id, User::LOGIN_LINK]);
92✔
6992
        foreach ($logins as $login) {
92✔
6993
            $key = $login['credentials'];
29✔
6994
        }
6995

6996
        if (!$key) {
92✔
6997
            $key = Utils::randstr(32);
92✔
6998
            $rc = $this->dbhm->preExec("INSERT INTO users_logins (userid, type, credentials) VALUES (?,?,?);", [
92✔
6999
                $id,
92✔
7000
                User::LOGIN_LINK,
92✔
7001
                $key
92✔
7002
            ]);
92✔
7003
        }
7004

7005
        return $key;
92✔
7006
    }
7007
}
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