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

Freegle / iznik-server / #2550

17 Dec 2025 10:43AM UTC coverage: 89.531% (-0.003%) from 89.534%
#2550

push

php-coveralls

edwh
Fix flaky EngageTest by cleaning up engage table in setUp

The engage table tracks when users were sent engagement emails and
prevents re-sending within 7 days. This caused test isolation issues
when test users from previous runs still had entries.

Clean up engage table entries for test users at the start of each test.

26596 of 29706 relevant lines covered (89.53%)

31.63 hits per line

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

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

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

8
use Jenssegers\ImageHash\ImageHash;
9

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

18
    const OPEN_AGE = 90;
19

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

204
        return ($u);
640✔
205
    }
206

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

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

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

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

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

240
                        User::clearCache($this->id);
290✔
241

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

250
                        return (TRUE);
290✔
251
                    }
252
                }
253
            }
254
        }
255

256
        return (FALSE);
4✔
257
    }
258

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

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

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

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

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

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

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

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

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

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

304
        $name = NULL;
609✔
305

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

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

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

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

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

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

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

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

352
        return ($name);
609✔
353
    }
354

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

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

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

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

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

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

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

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

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

414
        $pw = '';
5✔
415

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

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

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

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

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

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

448
        return ($this->emails);
396✔
449
    }
450

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

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

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

464
        return ($ret);
180✔
465
    }
466

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

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

486
        return ($ret);
383✔
487
    }
488

489
    public function getOurEmail($emails = NULL)
490
    {
491
        $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✔
492
            [$this->id]);
2✔
493
        $ret = NULL;
2✔
494

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

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

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

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

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

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

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

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

539
        return (NULL);
1✔
540
    }
541

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

548
        $ret = NULL;
1✔
549

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

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

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

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

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

568
        return $ret;
4✔
569
    }
570

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

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

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

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

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

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

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

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

623
        return (NULL);
1✔
624
    }
625

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

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

636
        return (NULL);
3✔
637
    }
638

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

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

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

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

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

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

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

675
        return ($email);
539✔
676
    }
677

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

682
        # Invalidate cache.
683
        $this->emails = NULL;
540✔
684

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

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

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

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

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

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

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

745
        $this->assignUserToToDonation($email, $this->id);
540✔
746

747
        return ($rc);
540✔
748
    }
749

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

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

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

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

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

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

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

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

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

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

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

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

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

834
        return FALSE;
481✔
835
    }
836

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

843
        Session::clearSessionCache();
481✔
844

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

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

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

861
        $emailfrequency = 24;
481✔
862
        $eventsallowed = 1;
481✔
863
        $volunteeringallowed = 1;
481✔
864

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

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

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

888
        $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 = ?;", [
481✔
889
            $this->id,
481✔
890
            $groupid,
481✔
891
            $role,
481✔
892
            $collection,
481✔
893
            $emailfrequency,
481✔
894
            $eventsallowed,
481✔
895
            $volunteeringallowed,
481✔
896
            $role,
481✔
897
            $collection,
481✔
898
            $emailfrequency,
481✔
899
            $eventsallowed,
481✔
900
            $volunteeringallowed
481✔
901
        ]);
481✔
902

903
        $membershipid = $this->dbhm->lastInsertId();
481✔
904

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

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

917
        $historyid = $this->dbhm->lastInsertId();
481✔
918

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

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

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

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

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

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

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

959
        return ($rc);
481✔
960
    }
961

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

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

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

977
        return ($this->memberships);
367✔
978
    }
979

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

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

993
        return ($val);
191✔
994
    }
995

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

1007
        return ($rc);
250✔
1008
    }
1009

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

1020
        return ($rc);
×
1021
    }
1022

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1160
            $groupsettings[$group['groupid']] = $one['mysettings'];
125✔
1161

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

1170
            $ret[] = $one;
125✔
1171
        }
1172

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

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

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

1195

1196
            $extraworkids = [];
27✔
1197

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

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

1216
        return ($ret);
163✔
1217
    }
1218

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

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

1232
            $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✔
1233
            $ids = $this->dbhr->preQuery($sql);
22✔
1234
        } else {
1235
            # We only want to see the configs that we are actively using.  This reduces the size of what we return
1236
            # for people on many groups.
1237
            $sql = "SELECT DISTINCT configid AS id FROM memberships WHERE userid = ? AND configid IS NOT NULL;";
3✔
1238
            $ids = $this->dbhr->preQuery($sql, [ $me->getId() ]);
3✔
1239
        }
1240

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1340
        return ($logins);
300✔
1341
    }
1342

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

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

1352
        return (NULL);
5✔
1353
    }
1354

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

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

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

1374
        return ($rc);
518✔
1375
    }
1376

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

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

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

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

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

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

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

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

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

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

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

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

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

1469
        return ($ret);
485✔
1470
    }
1471

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

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

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

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

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

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

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

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

1523
        return $widerreview;
20✔
1524
    }
1525

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

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

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

1542
        $settings = $defaults;
155✔
1543

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

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

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

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

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

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

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

1573
        return ($settings);
155✔
1574
    }
1575

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

1581
        Session::clearSessionCache();
46✔
1582

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1695
        if (Session::modtools()) {
131✔
1696
            $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 ";
129✔
1697
            $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 ";
129✔
1698
        }
1699

1700
        $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
131✔
1701
(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
131✔
1702
(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
131✔
1703
;";
131✔
1704
        $counts = $this->dbhr->preQuery($sql, [
131✔
1705
            $start,
131✔
1706
            ChatMessage::TYPE_INTERESTED,
131✔
1707
            $start,
131✔
1708
            Message::TYPE_OFFER,
131✔
1709
            ChatMessage::TYPE_INTERESTED
131✔
1710
        ]);
131✔
1711

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

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

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

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

1737
        $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;";
131✔
1738
        $counts = $this->dbhr->preQuery($sql, [
131✔
1739
            $start,
131✔
1740
            MessageCollection::APPROVED
131✔
1741
        ]);
131✔
1742

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

1750
            foreach ($counts as $count) {
131✔
1751
                if ($count['userid'] == $users[$uid]['id']) {
64✔
1752
                    if ($count['type'] == Message::TYPE_OFFER) {
64✔
1753
                        $users[$uid]['info']['offers'] += $count['count'];
48✔
1754

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

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

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

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

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

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

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

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

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

1795
        $nudges = $r->nudgeCounts($uids);
131✔
1796

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

1801
        $ratings = $this->getRatings($uids);
131✔
1802

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1970
            $hash = NULL;
19✔
1971

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

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

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

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

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

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

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

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

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

2037
        return($atts);
261✔
2038
    }
2039

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

2046
            $atts = $this->publicatts;
266✔
2047

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

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

2059
            $rets[$user['id']]['settings'] = ['dummy' => TRUE];
266✔
2060

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2319

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2483
        $uidsleft = array_filter($uidsleft);
143✔
2484

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

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

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

2501
        return($rets);
143✔
2502
    }
2503

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2719
        $rets = [];
266✔
2720

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

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

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

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

2738
            if ($comments) {
248✔
2739
                $this->getComments($me, $rets);
204✔
2740
            }
2741

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

2747
        return ($rets);
266✔
2748
    }
2749

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

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

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

2765
        return $ret;
98✔
2766
    }
2767

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3016

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3337
                $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✔
3338
            }
3339

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3436
            $commentusers = [];
78✔
3437

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3505
        return $comments;
1✔
3506
    }
3507

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

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

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

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

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

3530
        return (NULL);
1✔
3531
    }
3532

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3696
        # Chats which reference the messages sent from that email must also be intended for the split user.
3697
        $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✔
3698
            $email
2✔
3699
        ]);
2✔
3700

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3779
        $message = \Swift_Message::newInstance()
1✔
3780
            ->setSubject("We've turned off emails for you")
1✔
3781
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
1✔
3782
            ->setTo($this->getEmailPreferred())
1✔
3783
//            ->setBcc('log@ehibbert.org.uk')
1✔
3784
            ->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✔
3785

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3886
            $message = \Swift_Message::newInstance()
5✔
3887
                ->setSubject("Please verify your email")
5✔
3888
                ->setFrom([NOREPLY_ADDR => SITE_NAME])
5✔
3889
                ->setReturnPath($this->getBounce())
5✔
3890
                ->setTo([$email => $this->getName()])
5✔
3891
                ->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✔
3892

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

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

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

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

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

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

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

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

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

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

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

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

3940
        $message = \Swift_Message::newInstance()
2✔
3941
            ->setSubject("Please confirm you want to leave Freegle")
2✔
3942
            ->setFrom(NOREPLY_ADDR)
2✔
3943
            ->setReplyTo(SUPPORT_ADDR)
2✔
3944
            ->setTo($this->getEmailPreferred())
2✔
3945
            ->setDate(time())
2✔
3946
            ->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✔
3947

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

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

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

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

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

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

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

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

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

4013
                    # Build list of words to exclude from invented email - includes name parts and any
4014
                    # words from %encoded emails (like real%test.com@gtempaccount.com -> "test")
4015
                    $excludeWords = [];
33✔
4016

4017
                    foreach (['firstname', 'lastname', 'fullname'] as $att) {
33✔
4018
                        $words = explode(' ', $this->user[$att]);
33✔
4019
                        foreach ($words as $word) {
33✔
4020
                            $word = trim($word);
33✔
4021
                            if (strlen($word) >= 3 && $word !== '-') {
33✔
4022
                                $excludeWords[] = strtolower($word);
23✔
4023
                            }
4024
                        }
4025
                    }
4026

4027
                    # Check for %encoded emails and extract the encoded domain parts
4028
                    $preferredEmail = $this->getEmailPreferred();
33✔
4029
                    if ($preferredEmail && strpos($preferredEmail, '%') !== FALSE) {
33✔
4030
                        # Extract the LHS before @ which contains the encoded real email
4031
                        $atPos = strpos($preferredEmail, '@');
1✔
4032
                        if ($atPos !== FALSE) {
1✔
4033
                            $lhs = substr($preferredEmail, 0, $atPos);
1✔
4034
                            # Split on % and . to get individual words
4035
                            $parts = preg_split('/[%.@]/', $lhs);
1✔
4036
                            foreach ($parts as $part) {
1✔
4037
                                $part = trim($part);
1✔
4038
                                if (strlen($part) >= 3) {
1✔
4039
                                    $excludeWords[] = strtolower($part);
1✔
4040
                                }
4041
                            }
4042
                        }
4043
                    }
4044

4045
                    do {
4046
                        $length = \Wordle\array_weighted_rand($lengths);
33✔
4047
                        $start = \Wordle\array_weighted_rand($bigrams);
33✔
4048
                        $email = strtolower(\Wordle\fill_word($start, $length, $trigrams)) . '-' . $this->id . '@' . USER_DOMAIN;
33✔
4049

4050
                        # Check that invented email doesn't contain any excluded words
4051
                        $q = strpos($email, '-');
33✔
4052
                        $wordPart = substr($email, 0, $q);
33✔
4053

4054
                        foreach ($excludeWords as $word) {
33✔
4055
                            if (stripos($wordPart, $word) !== FALSE) {
23✔
4056
                                $email = NULL;
×
4057
                                break;
×
4058
                            }
4059
                        }
4060
                    } while (!$email);
33✔
4061
                }
4062
            }
4063
        }
4064

4065
        return ($email);
46✔
4066
    }
4067

4068
    public function delete($groupid = NULL, $subject = NULL, $body = NULL, $log = TRUE)
4069
    {
4070
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
18✔
4071

4072
        # Delete memberships.  This will remove any Yahoo memberships.
4073
        $membs = $this->getMemberships();
18✔
4074
        #error_log("Members in delete " . var_export($membs, TRUE));
4075
        foreach ($membs as $memb) {
18✔
4076
            $this->removeMembership($memb['id']);
8✔
4077
        }
4078

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

4081
        if ($rc && $log) {
18✔
4082
            $this->log->log([
5✔
4083
                'type' => Log::TYPE_USER,
5✔
4084
                'subtype' => Log::SUBTYPE_DELETED,
5✔
4085
                'user' => $this->id,
5✔
4086
                'byuser' => $me ? $me->getId() : NULL,
5✔
4087
                'text' => $this->getName()
5✔
4088
            ]);
5✔
4089
        }
4090

4091
        return ($rc);
18✔
4092
    }
4093

4094
    public function getUnsubLink($domain, $id, $type = NULL, $auto = FALSE)
4095
    {
4096
        return (User::loginLink($domain, $id, "/unsubscribe/$id", $type, $auto));
26✔
4097
    }
4098

4099
    public function listUnsubscribe($id, $type = NULL) {
4100
        # These are links which will completely unsubscribe the user.  This is necessary because of Yahoo and Gmail
4101
        # changes in 2024, and also useful for CAN-SPAM.  We want them to involve the key to prevent spoof unsubscribes.
4102
        #
4103
        # We only include the web link, because this providers a better user experience - we can tell them
4104
        # things afterwards.  This is valid - RFC8058 the RFC says you MUST include an HTTPS link, and you MAY
4105
        # include others.
4106
        $key = $id ? $this->getUserKey($id) : '1234';
137✔
4107
        $key = $key ? $key : '1234';
136✔
4108
        #$ret = "<mailto:unsubscribe-$id-$key-$type@" . USER_DOMAIN . "?subject=unsubscribe>, <https://" . USER_SITE . "/one-click-unsubscribe/$id/$key>";
4109
        $ret = "<https://" . USER_SITE . "/one-click-unsubscribe/$id/$key>";
136✔
4110
        #$ret = "<http://localhost:3002/one-click-unsubscribe/$id/$key>";
4111
        return $ret;
136✔
4112
    }
4113

4114
    public function loginLink($domain, $id, $url = '/', $type = NULL, $auto = FALSE)
4115
    {
4116
        $p = strpos($url, '?');
58✔
4117
        $ret = $p === FALSE ? "https://$domain$url?u=$id&src=$type" : "https://$domain$url&u=$id&src=$type";
58✔
4118

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

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

4126
            $p = strpos($url, '?');
11✔
4127
            $src = $type ? "&src=$type" : "";
11✔
4128
            $ret = $p === FALSE ? ("https://$domain$url?u=$id&k=$key$src") : ("https://$domain$url&u=$id&k=$key$src");
11✔
4129
        }
4130

4131
        return ($ret);
58✔
4132
    }
4133

4134
    public function sendOurMails($g = NULL, $checkholiday = TRUE, $checkbouncing = TRUE)
4135
    {
4136
        if ($this->getPrivate('deleted')) {
101✔
4137
            return FALSE;
1✔
4138
        }
4139

4140
        # We have two kinds of email settings - the top-level Simple one, and more detailed per group ones.
4141
        # Where present the Simple one overrides the group ones, so check that first.
4142
        $simplemail = $this->getSetting('simplemail', NULL);
101✔
4143
        if ($simplemail === User::SIMPLE_MAIL_NONE) {
101✔
4144
            return FALSE;
×
4145
        }
4146

4147
        # We don't want to send emails to people who haven't been active for more than six months.  This improves
4148
        # our spam reputation, by avoiding honeytraps.
4149
        $sendit = FALSE;
101✔
4150
        $lastaccess = strtotime($this->getPrivate('lastaccess'));
101✔
4151

4152
        // This time is also present on the client in ModMember, and in Engage.
4153
        if (time() - $lastaccess <= Engage::USER_INACTIVE) {
101✔
4154
            $sendit = TRUE;
101✔
4155

4156
            if ($sendit && $checkholiday) {
101✔
4157
                # We might be on holiday.
4158
                $hol = $this->getPrivate('onholidaytill');
23✔
4159
                $till = $hol ? strtotime($hol) : 0;
23✔
4160
                #error_log("Holiday $till vs " . time());
4161

4162
                $sendit = time() > $till;
23✔
4163
            }
4164

4165
            if ($sendit && $checkbouncing) {
101✔
4166
                # And don't send if we're bouncing.
4167
                $sendit = !$this->getPrivate('bouncing');
23✔
4168
                #error_log("After bouncing $sendit");
4169
            }
4170
        }
4171

4172
        #error_log("Sendit? $sendit");
4173
        return ($sendit);
101✔
4174
    }
4175

4176
    public function getMembershipHistory()
4177
    {
4178
        # We get this from our logs.
4179
        $sql = "SELECT * FROM logs WHERE user = ? AND `type` = ? ORDER BY id DESC;";
5✔
4180
        $logs = $this->dbhr->preQuery($sql, [$this->id, Log::TYPE_GROUP]);
5✔
4181

4182
        $ret = [];
5✔
4183
        foreach ($logs as $log) {
5✔
4184
            $thisone = NULL;
3✔
4185
            switch ($log['subtype']) {
3✔
4186
                case Log::SUBTYPE_JOINED:
3✔
4187
                case Log::SUBTYPE_APPROVED:
1✔
4188
                case Log::SUBTYPE_REJECTED:
1✔
4189
                case Log::SUBTYPE_APPLIED:
1✔
4190
                case Log::SUBTYPE_LEFT:
1✔
4191
                    {
3✔
4192
                        $thisone = $log['subtype'];
3✔
4193
                        break;
3✔
4194
                    }
3✔
4195
            }
4196

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

4201
                if ($g->getId() ==  $log['groupid']) {
3✔
4202
                    $ret[] = [
3✔
4203
                        'timestamp' => Utils::ISODate($log['timestamp']),
3✔
4204
                        'type' => $thisone,
3✔
4205
                        'group' => [
3✔
4206
                            'id' => $log['groupid'],
3✔
4207
                            'nameshort' => $g->getPrivate('nameshort'),
3✔
4208
                            'namedisplay' => $g->getName()
3✔
4209
                        ],
3✔
4210
                        'text' => $log['text']
3✔
4211
                    ];
3✔
4212
                }
4213
            }
4214
        }
4215

4216
        return ($ret);
5✔
4217
    }
4218

4219
    public function search($search, $ctx)
4220
    {
4221
        if (preg_replace('/\-|\~/', '', $search) ==  '') {
5✔
4222
            # Most likely an encoded id.
4223
            $search = User::decodeId($search);
×
4224
        }
4225

4226
        if (preg_match('/story-(.*)/', $search, $matches)) {
5✔
4227
            # Story.
4228
            $s = new Story($this->dbhr, $this->dbhm, $matches[1]);
×
4229
            $search = $s->getPrivate('userid');
×
4230
        }
4231

4232
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
5✔
4233
        $id = intval(Utils::presdef('id', $ctx, 0));
5✔
4234
        $ctx = $ctx ? $ctx : [];
5✔
4235
        $q = $this->dbhr->quote("$search%");
5✔
4236
        $backwards = strrev($search);
5✔
4237
        $qb = $this->dbhr->quote("$backwards%");
5✔
4238

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

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

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

4248
        $sql = "SELECT DISTINCT userid FROM
5✔
4249
                ((SELECT userid FROM users_emails WHERE canon LIKE $canon OR backwards LIKE $qb) UNION
5✔
4250
                (SELECT userid FROM users_emails WHERE canon LIKE $canon2) UNION
5✔
4251
                (SELECT id AS userid FROM users WHERE fullname LIKE $q) UNION
5✔
4252
                (SELECT id AS userid FROM users WHERE yahooid LIKE $q) UNION
5✔
4253
                (SELECT id AS userid FROM users WHERE id = ?) UNION
4254
                (SELECT userid FROM users_logins WHERE uid LIKE $q)) t WHERE userid > ? ORDER BY userid ASC";
5✔
4255
        $users = $this->dbhr->preQuery($sql, [$search, $id]);
5✔
4256

4257
        $ret = [];
5✔
4258

4259
        foreach ($users as $user) {
5✔
4260
            $ctx['id'] = $user['userid'];
4✔
4261

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

4264
            $thisone = $u->getPublic(NULL, TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, [
4✔
4265
                MessageCollection::PENDING,
4✔
4266
                MessageCollection::APPROVED
4✔
4267
            ], TRUE);
4✔
4268

4269
            # We might not have the emails.
4270
            $thisone['email'] = $u->getEmailPreferred();
4✔
4271
            $thisone['emails'] = $u->getEmails();
4✔
4272

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

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

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

4282
            # Also return the chats for this user.  Can't use ChatRooms::listForUser because that would exclude any
4283
            # chats on groups where we were no longer a member.
4284
            $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✔
4285
                $user['userid'],
4✔
4286
                ChatRoom::TYPE_USER2USER,
4✔
4287
                $user['userid'],
4✔
4288
            ]), 'id'));
4✔
4289

4290
            $thisone['chatrooms'] = [];
4✔
4291

4292
            if ($rooms) {
4✔
4293
                $r = new ChatRoom($this->dbhr, $this->dbhm);
×
4294
                $thisone['chatrooms'] = $r->fetchRooms($rooms, $user['userid'], FALSE);
×
4295
            }
4296

4297
            # Add the public location and best guess lat/lng
4298
            $thisone['info']['publiclocation'] = $u->getPublicLocation();
4✔
4299
            $latlng = $u->getLatLng(FALSE, TRUE);
4✔
4300
            $thisone['privateposition'] = [
4✔
4301
                'lat' => $latlng[0],
4✔
4302
                'lng' => $latlng[1],
4✔
4303
                'name' => $latlng[2]
4✔
4304
            ];
4✔
4305

4306
            $thisone['comments'] = $this->getCommentsForSingleUser($user['userid']);
4✔
4307
            $thisone['tnuserid'] = $u->getPrivate('tnuserid');
4✔
4308

4309
            $push = $this->dbhr->preQuery("SELECT MAX(lastsent) AS lastpush FROM users_push_notifications WHERE userid = ?;", [
4✔
4310
                $user['userid']
4✔
4311
            ]);
4✔
4312

4313
            foreach ($push as $p) {
4✔
4314
                $thisone['lastpush'] = Utils::ISODate($p['lastpush']);
4✔
4315
            }
4316

4317
            $thisone['info'] = $u->getInfo();
4✔
4318
            $thisone['trustlevel'] = $u->getPrivate('trustlevel');
4✔
4319

4320
            $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE userid = ?;", [
4✔
4321
                $u->getId()
4✔
4322
            ]);
4✔
4323

4324
            $thisone['bans'] = [];
4✔
4325

4326
            foreach ($bans as $ban) {
4✔
4327
                $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
4328
                $banner = User::get($this->dbhr, $this->dbhm, $ban['byuser']);
1✔
4329
                $thisone['bans'][] = [
1✔
4330
                    'date' => Utils::ISODate($ban['date']),
1✔
4331
                    'group' => $g->getName(),
1✔
4332
                    'byemail' => $banner->getEmailPreferred(),
1✔
4333
                    'byuserid' => $ban['byuser']
1✔
4334
                ];
1✔
4335
            }
4336

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

4340
            if ($me->hasPermission(User::PERM_GIFTAID)) {
4✔
4341
                $thisone['donations'] = $d->listByUser($user['userid']);
2✔
4342
            }
4343

4344
            $thisone['newsfeedmodstatus'] = $u->getPrivate('newsfeedmodstatus');
4✔
4345
            $thisone['newsfeed'] = $this->dbhr->preQuery("SELECT id, message, timestamp, hidden, hiddenby, deleted, deletedby FROM newsfeed WHERE userid = ? ORDER BY id DESC;", [
4✔
4346
                $user['userid']
4✔
4347
            ]);
4✔
4348

4349
            foreach ($thisone['newsfeed'] as &$nf) {
4✔
4350
                $nf['timestamp'] = Utils::ISODate($nf['timestamp']);
×
4351
                $nf['deleted'] = Utils::ISODate($nf['deleted']);
×
4352
                $nf['hidden'] = Utils::ISODate($nf['hidden']);
×
4353
            }
4354

4355
            $ret[] = $thisone;
4✔
4356
        }
4357

4358
        return ($ret);
5✔
4359
    }
4360

4361
    private function safeGetPostcode($val) {
4362
        $ret = [ NULL, NULL ];
59✔
4363

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

4366
        if (Utils::pres('mylocation', $settings) &&
59✔
4367
            Utils::presdef('type', $settings['mylocation'], NULL) == 'Postcode') {
59✔
4368
            $ret = [
14✔
4369
                Utils::presdef('id', $settings['mylocation'], NULL),
14✔
4370
                Utils::presdef('name', $settings['mylocation'], NULL)
14✔
4371
            ];
14✔
4372
        }
4373

4374
        return $ret;
59✔
4375
    }
4376

4377
    public function setPrivate($att, $val)
4378
    {
4379
        if (!strcmp($att, 'settings') && $val) {
196✔
4380
            # Possible location change.
4381
            list ($oldid, $oldloc) = $this->safeGetPostcode($this->getPrivate('settings'));
59✔
4382
            list ($newid, $newloc) = $this->safeGetPostcode($val);
59✔
4383

4384
            if ($oldloc !== $newloc) {
59✔
4385
                # We have changed our location.
4386
                parent::setPrivate('lastlocation', $newid);
14✔
4387
                $i = new Isochrone($this->dbhr, $this->dbhm);
14✔
4388
                $i->deleteForUser($this->id);
14✔
4389

4390
                $this->log->log([
14✔
4391
                            'type' => Log::TYPE_USER,
14✔
4392
                            'subtype' => Log::SUBTYPE_POSTCODECHANGE,
14✔
4393
                            'user' => $this->id,
14✔
4394
                            'text' => $newloc
14✔
4395
                        ]);
14✔
4396
            }
4397

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

4402
        User::clearCache($this->id);
196✔
4403
        parent::setPrivate($att, $val);
196✔
4404
    }
4405

4406
    public static function pruneSettings($val) {
4407
        // Prune info from what we store in the user table to keep it smaller.
4408
        if (strpos($val, 'groupsnear') !== FALSE) {
59✔
4409
            $decoded = json_decode($val, TRUE);
×
4410
            if (Utils::pres('mylocation', $decoded) && Utils::pres('groupsnear', $decoded['mylocation'])) {
×
4411
                unset($decoded['mylocation']['groupsnear']);
×
4412
                $val = json_encode($decoded);
×
4413
            }
4414
        }
4415

4416
        return $val;
59✔
4417
    }
4418

4419
    public function canMerge()
4420
    {
4421
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
16✔
4422
        return (array_key_exists('canmerge', $settings) ? $settings['canmerge'] : TRUE);
16✔
4423
    }
4424

4425
    public function notifsOn($type, $groupid = NULL)
4426
    {
4427
        if ($this->getPrivate('deleted')) {
84✔
4428
            return FALSE;
1✔
4429
        }
4430

4431
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
84✔
4432
        $notifs = Utils::pres('notifications', $settings);
84✔
4433

4434
        $defs = [
84✔
4435
            self::NOTIFS_EMAIL => TRUE,
84✔
4436
            self::NOTIFS_EMAIL_MINE => FALSE,
84✔
4437
            self::NOTIFS_PUSH => TRUE
84✔
4438
        ];
84✔
4439

4440
        $ret = ($notifs && array_key_exists($type, $notifs)) ? $notifs[$type] : $defs[$type];
84✔
4441

4442
        if ($ret && $groupid) {
84✔
4443
            # Check we're an active mod on this group - if not then we don't want the notifications.
4444
            $ret = $this->activeModForGroup($groupid);
5✔
4445
        }
4446

4447
        #error_log("Notifs on for user #{$this->id} type $type ? $ret from " . var_export($notifs, TRUE));
4448
        return ($ret);
84✔
4449
    }
4450

4451
    public function getNotificationPayload($modtools)
4452
    {
4453
        # This gets a notification count/title/message for this user.
4454
        $notifcount = 0;
11✔
4455
        $title = '';
11✔
4456
        $message = NULL;
11✔
4457
        $chatids = [];
11✔
4458
        $route = NULL;
11✔
4459
        $category = NULL;
11✔
4460
        $threadId = NULL;
11✔
4461
        $image = NULL;
11✔
4462

4463
        if (!$modtools) {
11✔
4464
            # User notification.  We want to show a count of chat messages, or some of the message if there is just one.
4465
            $r = new ChatRoom($this->dbhr, $this->dbhm);
9✔
4466
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_USER2USER, ChatRoom::TYPE_USER2MOD], $modtools);
9✔
4467
            $chatcount = count($unseen);
9✔
4468
            $total = $chatcount;
9✔
4469
            foreach ($unseen as $un) {
9✔
4470
                $chatids[] = $un['chatid'];
5✔
4471
            };
4472

4473
            #error_log("Chats with unseen " . var_export($chatids, TRUE));
4474
            $n = new Notifications($this->dbhr, $this->dbhm);
9✔
4475
            $notifcount = $n->countUnseen($this->id);
9✔
4476

4477
            if ($total ==  1) {
9✔
4478
                $r = new ChatRoom($this->dbhr, $this->dbhm, $unseen[0]['chatid']);
4✔
4479
                $atts = $r->getPublic($this);
4✔
4480
                $title = $atts['name'];
4✔
4481
                list($msgs, $users) = $r->getMessages(100, 0);
4✔
4482

4483
                if (count($msgs) > 0) {
4✔
4484
                    $message = Utils::presdef('message', $msgs[count($msgs) - 1], "You have a message");
4✔
4485

4486
                    # Decode emoji escape sequences to actual emojis for display.
4487
                    $message = Utils::decodeEmojis($message);
4✔
4488

4489
                    $message = strlen($message) > 256 ? (substr($message, 0, 256) . "...") : $message;
4✔
4490
                }
4491

4492
                $route = "/chats/" . $unseen[0]['chatid'];
4✔
4493
                $category = PushNotifications::CATEGORY_CHAT_MESSAGE;
4✔
4494
                $threadId = 'chat_' . $unseen[0]['chatid'];
4✔
4495
                $image = Utils::presdef('icon', $atts, NULL);
4✔
4496

4497
                if ($notifcount) {
4✔
4498
                    $total += $notifcount;
4✔
4499
                }
4500
            } else if ($total > 1) {
5✔
4501
                $title = "You have $total new messages";
1✔
4502
                $route = "/chats";
1✔
4503
                $category = PushNotifications::CATEGORY_CHAT_MESSAGE;
1✔
4504
                $threadId = 'chats';
1✔
4505

4506
                if ($notifcount) {
1✔
4507
                    $total += $notifcount;
1✔
4508
                    $title .= " and $notifcount notification" . ($notifcount == 1 ? '' : 's');
1✔
4509
                }
4510
            } else {
4511
                # Add in the notifications you see primarily from the newsfeed.
4512
                if ($notifcount) {
5✔
4513
                    $total += $notifcount;
5✔
4514
                    $ctx = NULL;
5✔
4515
                    $notifs = $n->get($this->id, $ctx);
5✔
4516
                    $title = $n->getNotifTitle($notifs);
5✔
4517

4518
                    if ($title) {
5✔
4519
                        $route = '/';
5✔
4520

4521
                        if (count($notifs) > 0) {
5✔
4522
                            # For newsfeed notifications sent a route to the right place.
4523
                            # Also set the appropriate notification category and thread ID.
4524
                            switch ($notifs[0]['type']) {
5✔
4525
                                case Notifications::TYPE_COMMENT_ON_YOUR_POST:
5✔
4526
                                    $route = '/chitchat/' . $notifs[0]['newsfeedid'];
1✔
4527
                                    $category = PushNotifications::CATEGORY_CHITCHAT_COMMENT;
1✔
4528
                                    $threadId = 'chitchat_' . $notifs[0]['newsfeedid'];
1✔
4529
                                    break;
1✔
4530
                                case Notifications::TYPE_COMMENT_ON_COMMENT:
5✔
4531
                                    $route = '/chitchat/' . $notifs[0]['newsfeedid'];
1✔
4532
                                    $category = PushNotifications::CATEGORY_CHITCHAT_REPLY;
1✔
4533
                                    $threadId = 'chitchat_' . $notifs[0]['newsfeedid'];
1✔
4534
                                    break;
1✔
4535
                                case Notifications::TYPE_LOVED_COMMENT:
5✔
4536
                                case Notifications::TYPE_LOVED_POST:
5✔
4537
                                    $route = '/chitchat/' . $notifs[0]['newsfeedid'];
2✔
4538
                                    $category = PushNotifications::CATEGORY_CHITCHAT_LOVED;
2✔
4539
                                    $threadId = 'chitchat_' . $notifs[0]['newsfeedid'];
2✔
4540
                                    break;
2✔
4541
                                case Notifications::TYPE_EXHORT:
3✔
4542
                                    $category = PushNotifications::CATEGORY_EXHORT;
1✔
4543
                                    $threadId = 'tips';
1✔
4544
                                    $message = Utils::presdef('text', $notifs[0], NULL);
1✔
4545
                                    if (Utils::presdef('url', $notifs[0], NULL)) {
1✔
4546
                                        $route = $notifs[0]['url'];
×
4547
                                    }
4548
                                    break;
9✔
4549
                            }
4550
                        }
4551
                    }
4552
                }
4553
            }
4554
        } else {
4555
            # ModTools notification.  We show the count of work + chats.
4556
            $r = new ChatRoom($this->dbhr, $this->dbhm);
4✔
4557
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_MOD2MOD, ChatRoom::TYPE_USER2MOD], $modtools);
4✔
4558
            $chatcount = count($unseen);
4✔
4559

4560
            $work = $this->getWorkCounts();
4✔
4561
            $total = $work['total'] + $chatcount;
4✔
4562

4563
            // The order of these is important as the route will be the last matching.
4564
            $types = [
4✔
4565
                'pendingvolunteering' => [ 'volunteer op', 'volunteerops', '/modtools/volunteering' ],
4✔
4566
                'pendingevents' => [ 'event', 'events', '/modtools/communityevents' ],
4✔
4567
                'socialactions' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4✔
4568
                'popularposts' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4✔
4569
                'stories' => [ 'story', 'stories', '/modtools/members/stories' ],
4✔
4570
                'newsletterstories' => [ 'newsletter story', 'newsletter stories', '/modtools/members/newsletter' ],
4✔
4571
                'chatreview' => [ 'chat message to review', 'chat messages to review', '/modtools/chats/review' ],
4✔
4572
                'pendingadmins' => [ 'admin', 'admins', '/modtools/admins' ],
4✔
4573
                'spammembers' => [ 'member to review', 'members to review', '/modtools/members/review' ],
4✔
4574
                'relatedmembers' => [ 'related member to review', 'related members to review', '/modtools/members/related' ],
4✔
4575
                'editreview' => [ 'edit', 'edits', '/modtools/messages/edits' ],
4✔
4576
                'spam' => [ 'message to review', 'messages to review', '/modtools/messages/pending' ],
4✔
4577
                'pending' => [ 'pending message', 'pending messages', '/modtools/messages/pending' ]
4✔
4578
            ];
4✔
4579

4580
            $title = $chatcount ? ("$chatcount chat message" . ($chatcount != 1 ? 's' : '') . "\n") : '';
4✔
4581
            $route = NULL;
4✔
4582

4583
            foreach ($types as $type => $vals) {
4✔
4584
                if (Utils::presdef($type, $work, 0) > 0) {
4✔
4585
                    $title .= $work[$type] . ' ' . ($work[$type] != 1 ? $vals[1] : $vals[0] ) . "\n";
1✔
4586
                    $route = $vals[2];
1✔
4587
                }
4588
            }
4589

4590
            $title = $title == '' ? NULL : $title;
4✔
4591
        }
4592

4593

4594
        return ([$total, $chatcount, $notifcount, $title, $message, array_unique($chatids), $route, $category, $threadId, $image]);
11✔
4595
    }
4596

4597
    public function hasPermission($perm)
4598
    {
4599
        $perms = $this->user['permissions'];
39✔
4600
        return ($perms && stripos($perms, $perm) !== FALSE);
39✔
4601
    }
4602

4603
    public function sendIt($mailer, $message)
4604
    {
4605
        $mailer->send($message);
34✔
4606
    }
4607

4608
    public function thankDonation()
4609
    {
4610
        try {
4611
            $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/donations');
1✔
4612
            $twig = new \Twig_Environment($loader);
1✔
4613
            list ($transport, $mailer) = Mail::getMailer();
1✔
4614

4615
            $message = \Swift_Message::newInstance()
1✔
4616
                ->setSubject("Thank you for supporting Freegle!")
1✔
4617
                ->setFrom(PAYPAL_THANKS_FROM)
1✔
4618
                ->setReplyTo(PAYPAL_THANKS_FROM)
1✔
4619
                ->setTo($this->getEmailPreferred())
1✔
4620
                ->setBody("Thank you for supporting Freegle!");
1✔
4621

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

4624
            $html = $twig->render('thank.html', [
1✔
4625
                'name' => $this->getName(),
1✔
4626
                'email' => $this->getEmailPreferred(),
1✔
4627
                'unsubscribe' => $this->loginLink(USER_SITE, $this->getId(), "/unsubscribe", NULL)
1✔
4628
            ]);
1✔
4629

4630
            # Add HTML in base-64 as default quoted-printable encoding leads to problems on
4631
            # Outlook.
4632
            $htmlPart = \Swift_MimePart::newInstance();
1✔
4633
            $htmlPart->setCharset('utf-8');
1✔
4634
            $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
4635
            $htmlPart->setContentType('text/html');
1✔
4636
            $htmlPart->setBody($html);
1✔
4637
            $message->attach($htmlPart);
1✔
4638

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

4641
            $this->sendIt($mailer, $message);
1✔
4642
        } catch (\Exception $e) { error_log("Failed " . $e->getMessage()); };
×
4643
    }
4644

4645
    public function invite($email)
4646
    {
4647
        $ret = FALSE;
9✔
4648

4649
        # We can only invite logged in.
4650
        if ($this->id) {
9✔
4651
            # ...and only if we have spare.
4652
            if ($this->user['invitesleft'] > 0) {
9✔
4653
                # They might already be using us - but they might also have forgotten.  So allow that case.  However if
4654
                # they have actively declined a previous invitation we suppress this one.
4655
                $previous = $this->dbhr->preQuery("SELECT id FROM users_invitations WHERE email = ? AND outcome = ?;", [
9✔
4656
                    $email,
9✔
4657
                    User::INVITE_DECLINED
9✔
4658
                ]);
9✔
4659

4660
                if (count($previous) == 0) {
9✔
4661
                    # The table has a unique key on userid and email, so that means we can only invite the same person
4662
                    # once.  That avoids us pestering them.
4663
                    try {
4664
                        $this->dbhm->preExec("INSERT INTO users_invitations (userid, email) VALUES (?,?);", [
9✔
4665
                            $this->id,
9✔
4666
                            $email
9✔
4667
                        ]);
9✔
4668

4669
                        # We're ok to invite.
4670
                        $fromname = $this->getName();
9✔
4671
                        $frommail = $this->getEmailPreferred();
9✔
4672
                        $url = "https://" . USER_SITE . "/invite/" . $this->dbhm->lastInsertId();
9✔
4673

4674
                        list ($transport, $mailer) = Mail::getMailer();
9✔
4675
                        $message = \Swift_Message::newInstance()
9✔
4676
                            ->setSubject("$fromname has invited you to try Freegle!")
9✔
4677
                            ->setFrom([NOREPLY_ADDR => SITE_NAME])
9✔
4678
                            ->setReplyTo($frommail)
9✔
4679
                            ->setTo($email)
9✔
4680
                            ->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✔
4681

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

4684
                        $html = invite($fromname, $frommail, $url);
9✔
4685

4686
                        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
4687
                        # Outlook.
4688
                        $htmlPart = \Swift_MimePart::newInstance();
9✔
4689
                        $htmlPart->setCharset('utf-8');
9✔
4690
                        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
9✔
4691
                        $htmlPart->setContentType('text/html');
9✔
4692
                        $htmlPart->setBody($html);
9✔
4693
                        $message->attach($htmlPart);
9✔
4694

4695
                        $this->sendIt($mailer, $message);
9✔
4696
                        $ret = TRUE;
9✔
4697

4698
                        $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft - 1 WHERE id = ?;", [
9✔
4699
                            $this->id
9✔
4700
                        ]);
9✔
4701
                    } catch (\Exception $e) {
1✔
4702
                        # Probably a duplicate.
4703
                    }
4704
                }
4705
            }
4706
        }
4707

4708
        return ($ret);
9✔
4709
    }
4710

4711
    public function inviteOutcome($id, $outcome)
4712
    {
4713
        $invites = $this->dbhm->preQuery("SELECT * FROM users_invitations WHERE id = ?;", [
1✔
4714
            $id
1✔
4715
        ]);
1✔
4716

4717
        foreach ($invites as $invite) {
1✔
4718
            if ($invite['outcome'] == User::INVITE_PENDING) {
1✔
4719
                $this->dbhm->preExec("UPDATE users_invitations SET outcome = ?, outcometimestamp = NOW() WHERE id = ?;", [
1✔
4720
                    $outcome,
1✔
4721
                    $id
1✔
4722
                ]);
1✔
4723

4724
                if ($outcome == User::INVITE_ACCEPTED) {
1✔
4725
                    # Give the sender two more invites.  This means that if their invitations are unsuccessful, they will
4726
                    # stall, but if they do ok, they won't.  This isn't perfect - someone could fake up emails and do
4727
                    # successful invitations that way.
4728
                    $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft + 2 WHERE id = ?;", [
1✔
4729
                        $invite['userid']
1✔
4730
                    ]);
1✔
4731
                }
4732
            }
4733
        }
4734
    }
4735

4736
    public function listInvitations($since = "30 days ago")
4737
    {
4738
        $ret = [];
8✔
4739

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

4746
        foreach ($invites as $invite) {
8✔
4747
            # Check if this email is now on the platform.
4748
            $invite['date'] = Utils::ISODate($invite['date']);
7✔
4749
            $invite['outcometimestamp'] = $invite['outcometimestamp'] ? Utils::ISODate($invite['outcometimestamp']) : NULL;
7✔
4750
            $ret[] = $invite;
7✔
4751
        }
4752

4753
        return ($ret);
8✔
4754
    }
4755

4756
    public function getLatLng($usedef = TRUE, $usegroup = TRUE, $blur = Utils::BLUR_NONE)
4757
    {
4758
        $ret = [ 0, 0, NULL ];
175✔
4759

4760
        if ($this->id) {
175✔
4761
            $locs = $this->getLatLngs([ $this->user ], $usedef, $usegroup, FALSE, [ $this->user ]);
175✔
4762
            $loc = $locs[$this->id];
175✔
4763

4764
            if ($loc) {
175✔
4765
                if ($blur && ($loc['lat'] || $loc['lng'])) {
173✔
4766
                    list ($loc['lat'], $loc['lng']) = Utils::blur($loc['lat'], $loc['lng'], $blur);
4✔
4767
                }
4768

4769
                $ret = [ $loc['lat'], $loc['lng'], Utils::presdef('loc', $loc, NULL) ];
173✔
4770
            }
4771
        }
4772

4773
        return $ret;
175✔
4774
    }
4775

4776
    public function getPublicLocations(&$users, $atts = NULL)
4777
    {
4778
        $idsleft = [];
108✔
4779
        
4780
        foreach ($users as $userid => $user) {
108✔
4781
            if (!Utils::pres('info', $user) || !Utils::pres('publiclocation', $user['info'])) {
108✔
4782
                $idsleft[] = $userid;
108✔
4783
            }
4784
        }
4785
        
4786
        $areas = NULL;
108✔
4787
        $membs = NULL;
108✔
4788

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

4793
            foreach ($atts as $att) {
108✔
4794
                $loc = NULL;
108✔
4795
                $grp = NULL;
108✔
4796

4797
                $aid = NULL;
108✔
4798
                $lid = NULL;
108✔
4799
                $lat = NULL;
108✔
4800
                $lng = NULL;
108✔
4801

4802
                # Default to nowhere.
4803
                $users[$att['id']]['info']['publiclocation'] = [
108✔
4804
                    'display' => '',
108✔
4805
                    'location' => NULL,
108✔
4806
                    'groupname' => NULL
108✔
4807
                ];
108✔
4808

4809
                if (Utils::pres('settings', $att)) {
108✔
4810
                    $settings = $att['settings'];
23✔
4811
                    $settings = json_decode($settings, TRUE);
23✔
4812

4813
                    if (Utils::pres('mylocation', $settings) && Utils::pres('area', $settings['mylocation'])) {
23✔
4814
                        $loc = $settings['mylocation']['area']['name'];
7✔
4815
                        $lid = $settings['mylocation']['id'];
7✔
4816
                        $lat = $settings['mylocation']['lat'];
7✔
4817
                        $lng = $settings['mylocation']['lng'];
7✔
4818
                    }
4819
                }
4820

4821
                if (!$loc) {
108✔
4822
                    # Get the name of the last area we used.
4823
                    if (is_null($areas)) {
101✔
4824
                        $areas = $this->dbhr->preQuery("SELECT l2.id, l2.name, l2.lat, l2.lng, users.id AS userid FROM locations l1 
101✔
4825
                            INNER JOIN users ON users.lastlocation = l1.id
4826
                            INNER JOIN locations l2 ON l2.id = l1.areaid
4827
                            WHERE users.id IN (" . implode(',', $idsleft) . ");", NULL, FALSE, FALSE);
101✔
4828
                    }
4829

4830
                    foreach ($areas as $area) {
101✔
4831
                        if ($att['id'] ==  $area['userid']) {
25✔
4832
                            $loc = $area['name'];
25✔
4833
                            $lid = $area['id'];
25✔
4834
                            $lat = $area['lat'];
25✔
4835
                            $lng = $area['lng'];
25✔
4836
                        }
4837
                    }
4838
                }
4839

4840
                if (!$lid) {
108✔
4841
                    # Find the group of which we are a member which is closest to our location.  We do this because generally
4842
                    # the number of groups we're in is small and therefore this will be quick, whereas the groupsNear call is
4843
                    # fairly slow.
4844
                    $closestdist = PHP_INT_MAX;
101✔
4845
                    $closestname = NULL;
101✔
4846

4847
                    # Get all the memberships.
4848
                    if (!$membs) {
101✔
4849
                        $sql = "SELECT memberships.userid, groups.id, groups.nameshort, groups.namefull, groups.lat, groups.lng FROM `groups` INNER JOIN memberships ON groups.id = memberships.groupid WHERE memberships.userid IN (" . implode(
101✔
4850
                                ',',
101✔
4851
                                $idsleft
101✔
4852
                            ) . ") ORDER BY added ASC;";
101✔
4853
                        $membs = $this->dbhr->preQuery($sql);
101✔
4854
                    }
4855

4856
                    foreach ($membs as $memb) {
101✔
4857
                        if ($memb['userid'] == $att['id']) {
90✔
4858
                            $dist = \GreatCircle::getDistance($lat, $lng, $memb['lat'], $memb['lng']);
90✔
4859

4860
                            if ($dist < $closestdist) {
90✔
4861
                                $closestdist = $dist;
90✔
4862
                                $closestname = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
90✔
4863
                            }
4864
                        }
4865
                    }
4866

4867
                    if (!is_null($closestname)) {
101✔
4868
                        $grp = $closestname;
90✔
4869

4870
                        # The location name might be in the group name, in which case just use the group.
4871
                        $loc = stripos($grp, $loc) !== FALSE ? NULL : $loc;
90✔
4872
                    }
4873
                }
4874

4875
                if ($loc) {
108✔
4876
                    $display = $loc ? ($loc . ($grp ? ", $grp" : "")) : ($grp ? $grp : '');
32✔
4877

4878
                    $users[$att['id']]['info']['publiclocation'] = [
32✔
4879
                        'display' => $display,
32✔
4880
                        'location' => $loc,
32✔
4881
                        'groupname' => $grp
32✔
4882
                    ];
32✔
4883

4884
                    $idsleft = array_filter($idsleft, function($val) use ($att) {
32✔
4885
                        return($val != $att['id']);
32✔
4886
                    });
32✔
4887
                }
4888
            }
4889

4890
            if (count($idsleft) > 0) {
108✔
4891
                # We have some left which don't have explicit postcodes.  Try for a group name.
4892
                #
4893
                # First check the group we used most recently.
4894
                #error_log("Look for group name only for {$att['id']}");
4895
                $found = [];
101✔
4896
                foreach ($idsleft as $userid) {
101✔
4897
                    $messages = $this->dbhr->preQuery("SELECT subject FROM messages INNER JOIN messages_groups ON messages_groups.msgid = messages.id WHERE fromuser = ? ORDER BY messages.arrival DESC LIMIT 1;", [
101✔
4898
                        $userid
101✔
4899
                    ]);
101✔
4900

4901
                    foreach ($messages as $msg) {
101✔
4902
                        list ($type, $item, $location) = Message::parseSubject($msg['subject']);
63✔
4903

4904
                        if ($item) {
63✔
4905
                            $grp = $location;
44✔
4906

4907
                            // Handle some misformed locations which end up with spurious brackets.
4908
                            $grp = preg_replace('/\(|\)/', '', $grp);
44✔
4909

4910
                            $users[$userid]['info']['publiclocation'] = [
44✔
4911
                                'display' => $grp,
44✔
4912
                                'location' => NULL,
44✔
4913
                                'groupname' => $grp
44✔
4914
                            ];
44✔
4915

4916
                            $found[] = $userid;
44✔
4917
                        }
4918
                    }
4919
                }
4920

4921
                $idsleft = array_diff($idsleft, $found);
101✔
4922
                
4923
                # Now check just membership.
4924
                if (count($idsleft)) {
101✔
4925
                    if (!$membs) {
67✔
4926
                        $sql = "SELECT memberships.userid, groups.id, groups.nameshort, groups.namefull, groups.lat, groups.lng FROM `groups` INNER JOIN memberships ON groups.id = memberships.groupid WHERE memberships.userid IN (" . implode(
22✔
4927
                                ',',
22✔
4928
                                $idsleft
22✔
4929
                            ) . ") ORDER BY added ASC;";
22✔
4930
                        $membs = $this->dbhr->preQuery($sql);
22✔
4931
                    }
4932
                    
4933
                    foreach ($idsleft as $userid) {
67✔
4934
                        # Now check the group we joined most recently.
4935
                        foreach ($membs as $memb) {
67✔
4936
                            if ($memb['userid'] == $userid) {
49✔
4937
                                $grp = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
49✔
4938

4939
                                $users[$userid]['info']['publiclocation'] = [
49✔
4940
                                    'display' => $grp,
49✔
4941
                                    'location' => NULL,
49✔
4942
                                    'groupname' => $grp
49✔
4943
                                ];
49✔
4944
                            }
4945
                        }
4946
                    }
4947
                }
4948
            }
4949
        }
4950
    }
4951

4952
    public function getLatLngs($users, $usedef = TRUE, $usegroup = TRUE, $needgroup = FALSE, $atts = NULL, $blur = NULL)
4953
    {
4954
        $userids = array_filter(array_column($users, 'id'));
176✔
4955
        $ret = [];
176✔
4956

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

4960
            foreach ($atts as $att) {
176✔
4961
                $lat = NULL;
176✔
4962
                $lng = NULL;
176✔
4963
                $loc = NULL;
176✔
4964

4965
                if (Utils::pres('settings', $att)) {
176✔
4966
                    $settings = $att['settings'];
37✔
4967
                    $settings = json_decode($settings, TRUE);
37✔
4968

4969
                    if (Utils::pres('mylocation', $settings)) {
37✔
4970
                        $lat = $settings['mylocation']['lat'];
33✔
4971
                        $lng = $settings['mylocation']['lng'];
33✔
4972
                        $loc = Utils::presdef('name', $settings['mylocation'], NULL);
33✔
4973
                        #error_log("Got from mylocation $lat, $lng, $loc");
4974
                    }
4975
                }
4976

4977
                if (is_null($lat)) {
176✔
4978
                    $lid = $att['lastlocation'];
157✔
4979

4980
                    if ($lid) {
157✔
4981
                        $l = new Location($this->dbhr, $this->dbhm, $lid);
23✔
4982
                        $lat = $l->getPrivate('lat');
23✔
4983
                        $lng = $l->getPrivate('lng');
23✔
4984
                        $loc = $l->getPrivate('name');
23✔
4985
                        #error_log("Got from last location $lat, $lng, $loc");
4986
                    }
4987
                }
4988

4989
                if (!is_null($lat)) {
176✔
4990
                    $ret[$att['id']] = [
52✔
4991
                        'lat' => $lat,
52✔
4992
                        'lng' => $lng,
52✔
4993
                        'loc' => $loc,
52✔
4994
                    ];
52✔
4995

4996
                    $userids = array_filter($userids, function($id) use ($att) {
52✔
4997
                        return $id != $att['id'];
52✔
4998
                    });
52✔
4999
                }
5000
            }
5001
        }
5002

5003
        if ($userids && count($userids) && $usegroup) {
176✔
5004
            # Still some we haven't handled.  Get the last message posted on a group with a location, if any.
5005
            $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);
150✔
5006
            foreach ($membs as $memb) {
150✔
5007
                $ret[$memb['userid']] = [
3✔
5008
                    'lat' => $memb['lat'],
3✔
5009
                    'lng' => $memb['lng']
3✔
5010
                ];
3✔
5011

5012
                #error_log("Got from last message posted {$memb['lat']}, {$memb['lng']}");
5013

5014
                $userids = array_filter($userids, function($id) use ($memb) {
3✔
5015
                    return $id != $memb['userid'];
3✔
5016
                });
3✔
5017
            }
5018
        }
5019

5020
        if ($userids && count($userids) && $usegroup) {
176✔
5021
            # Still some we haven't handled.  Get the memberships.  Logic will choose most recently joined.
5022
            $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);
148✔
5023
            foreach ($membs as $memb) {
148✔
5024
                $ret[$memb['userid']] = [
130✔
5025
                    'lat' => $memb['lat'],
130✔
5026
                    'lng' => $memb['lng'],
130✔
5027
                    'group' => Utils::presdef('namefull', $memb, $memb['nameshort'])
130✔
5028
                ];
130✔
5029

5030
                #error_log("Got from membership {$memb['lat']}, {$memb['lng']}, " . Utils::presdef('namefull', $memb, $memb['nameshort']));
5031

5032
                $userids = array_filter($userids, function($id) use ($memb) {
130✔
5033
                    return $id != $memb['userid'];
130✔
5034
                });
130✔
5035
            }
5036
        }
5037

5038
        if ($userids && count($userids)) {
176✔
5039
            # Still some we haven't handled.
5040
            foreach ($userids as $userid) {
26✔
5041
                if ($usedef) {
26✔
5042
                    $ret[$userid] = [
21✔
5043
                        'lat' => 53.9450,
21✔
5044
                        'lng' => -2.5209
21✔
5045
                    ];
21✔
5046
                } else {
5047
                    $ret[$userid] = NULL;
15✔
5048
                }
5049
            }
5050
        }
5051

5052
        if ($needgroup) {
176✔
5053
            # Get a group name.
5054
            $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✔
5055
            foreach ($membs as $memb) {
7✔
5056
                $ret[$memb['userid']]['group'] = Utils::presdef('namefull', $memb, $memb['nameshort']);
7✔
5057
            }
5058
        }
5059

5060
        if ($blur) {
176✔
5061
            foreach ($ret as &$memb) {
7✔
5062
                if ($memb['lat'] || $memb['lng']) {
7✔
5063
                    list ($memb['lat'], $memb['lng']) = Utils::blur($memb['lat'], $memb['lng'], $blur);
7✔
5064
                }
5065
            }
5066
        }
5067

5068
        return ($ret);
176✔
5069
    }
5070

5071
    public function isFreegleMod()
5072
    {
5073
        $ret = FALSE;
176✔
5074

5075
        $this->cacheMemberships();
176✔
5076

5077
        foreach ($this->memberships as $mem) {
176✔
5078
            if ($mem['type'] == Group::GROUP_FREEGLE && ($mem['role'] == User::ROLE_OWNER || $mem['role'] == User::ROLE_MODERATOR)) {
145✔
5079
                $ret = TRUE;
43✔
5080
            }
5081
        }
5082

5083
        return ($ret);
176✔
5084
    }
5085

5086
    public function getKudos($id = NULL)
5087
    {
5088
        $id = $id ? $id : $this->id;
1✔
5089
        $kudos = [
1✔
5090
            'userid' => $id,
1✔
5091
            'posts' => 0,
1✔
5092
            'chats' => 0,
1✔
5093
            'newsfeed' => 0,
1✔
5094
            'events' => 0,
1✔
5095
            'vols' => 0,
1✔
5096
            'facebook' => 0,
1✔
5097
            'platform' => 0,
1✔
5098
            'kudos' => 0,
1✔
5099
        ];
1✔
5100

5101
        $kudi = $this->dbhr->preQuery("SELECT * FROM users_kudos WHERE userid = ?;", [
1✔
5102
            $id
1✔
5103
        ]);
1✔
5104

5105
        foreach ($kudi as $k) {
1✔
5106
            $kudos = $k;
1✔
5107
        }
5108

5109
        return ($kudos);
1✔
5110
    }
5111

5112
    public function updateKudos($id = NULL, $force = FALSE)
5113
    {
5114
        $current = $this->getKudos($id);
1✔
5115

5116
        # Only update if we don't have one or it's older than a day.  This avoids repeatedly updating the entry
5117
        # for the same user in some bulk operations.
5118
        if (!Utils::pres('timestamp', $current) || (time() - strtotime($current['timestamp']) > 24 * 60 * 60)) {
1✔
5119
            # We analyse a user's activity and assign them a level.
5120
            #
5121
            # Only interested in activity in the last year.
5122
            $id = $id ? $id : $this->id;
1✔
5123
            $start = date('Y-m-d', strtotime("365 days ago"));
1✔
5124

5125
            # First, the number of months in which they have posted.
5126
            $posts = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM messages WHERE fromuser = ? AND date >= '$start';", [
1✔
5127
                $id
1✔
5128
            ])[0]['count'];
1✔
5129

5130
            # Ditto communicated with people.
5131
            $chats = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM chat_messages WHERE userid = ? AND date >= '$start';", [
1✔
5132
                $id
1✔
5133
            ])[0]['count'];
1✔
5134

5135
            # Newsfeed posts
5136
            $newsfeed = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(timestamp), '-', MONTH(timestamp)))) AS count FROM newsfeed WHERE userid = ? AND added >= '$start';", [
1✔
5137
                $id
1✔
5138
            ])[0]['count'];
1✔
5139

5140
            # Events
5141
            $events = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM communityevents WHERE userid = ? AND added >= '$start';", [
1✔
5142
                $id
1✔
5143
            ])[0]['count'];
1✔
5144

5145
            # Volunteering
5146
            $vols = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM volunteering WHERE userid = ? AND added >= '$start';", [
1✔
5147
                $id
1✔
5148
            ])[0]['count'];
1✔
5149

5150
            # Do they have a Facebook login?
5151
            $facebook = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_logins WHERE userid = ? AND type = ?", [
1✔
5152
                    $id,
1✔
5153
                    User::LOGIN_FACEBOOK
1✔
5154
                ])[0]['count'] > 0;
1✔
5155

5156
            # Have they posted using the platform?
5157
            $platform = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM messages WHERE fromuser = ? AND arrival >= '$start' AND sourceheader = ?;", [
1✔
5158
                    $id,
1✔
5159
                    Message::PLATFORM
1✔
5160
                ])[0]['count'] > 0;
1✔
5161

5162
            $kudos = $posts + $chats + $newsfeed + $events + $vols;
1✔
5163

5164
            if ($kudos > 0 || $force) {
1✔
5165
                # No sense in creating entries which are blank or the same.
5166
                $current = $this->getKudos($id);
1✔
5167

5168
                if ($current['kudos'] != $kudos || $force) {
1✔
5169
                    $this->dbhm->preExec("REPLACE INTO users_kudos (userid, kudos, posts, chats, newsfeed, events, vols, facebook, platform) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", [
1✔
5170
                        $id,
1✔
5171
                        $kudos,
1✔
5172
                        $posts,
1✔
5173
                        $chats,
1✔
5174
                        $newsfeed,
1✔
5175
                        $events,
1✔
5176
                        $vols,
1✔
5177
                        $facebook,
1✔
5178
                        $platform
1✔
5179
                    ], FALSE);
1✔
5180
                }
5181
            }
5182
        }
5183
    }
5184

5185
    public function topKudos($gid, $limit = 10)
5186
    {
5187
        $limit = intval($limit);
1✔
5188

5189
        $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✔
5190
            $gid,
1✔
5191
            User::ROLE_MEMBER
1✔
5192
        ]);
1✔
5193

5194
        $ret = [];
1✔
5195

5196
        foreach ($kudos as $k) {
1✔
5197
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5198
            $atts = $u->getPublic();
1✔
5199
            $atts['email'] = $u->getEmailPreferred();
1✔
5200

5201
            $thisone = [
1✔
5202
                'user' => $atts,
1✔
5203
                'kudos' => $k
1✔
5204
            ];
1✔
5205

5206
            $ret[] = $thisone;
1✔
5207
        }
5208

5209
        return ($ret);
1✔
5210
    }
5211

5212
    public function possibleMods($gid, $limit = 10)
5213
    {
5214
        # We look for users who are not mods with top kudos who also:
5215
        # - active in last 60 days
5216
        # - not bouncing
5217
        # - using a location which is in the group area
5218
        # - have posted with the platform, as we don't want loyal users of TN or Yahoo.
5219
        # - have a Facebook login, as they are more likely to do publicity.
5220
        $limit = intval($limit);
1✔
5221
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
5222
        $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✔
5223
        $kudos = $this->dbhr->preQuery($sql, [
1✔
5224
            $gid,
1✔
5225
            User::ROLE_MEMBER
1✔
5226
        ]);
1✔
5227

5228
        $ret = [];
1✔
5229

5230
        foreach ($kudos as $k) {
1✔
5231
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5232
            $atts = $u->getPublic();
1✔
5233
            $atts['email'] = $u->getEmailPreferred();
1✔
5234

5235
            $thisone = [
1✔
5236
                'user' => $atts,
1✔
5237
                'kudos' => $k
1✔
5238
            ];
1✔
5239

5240
            $ret[] = $thisone;
1✔
5241
        }
5242

5243
        return ($ret);
1✔
5244
    }
5245

5246
    public function requestExport($sync = FALSE)
5247
    {
5248
        $tag = Utils::randstr(64);
8✔
5249

5250
        # Flag sync ones as started to avoid window with background thread.
5251
        $sync = $sync ? "NOW()" : "NULL";
8✔
5252
        $this->dbhm->preExec("INSERT INTO users_exports (userid, tag, started) VALUES (?, ?, $sync);", [
8✔
5253
            $this->id,
8✔
5254
            $tag
8✔
5255
        ]);
8✔
5256

5257
        return ([$this->dbhm->lastInsertId(), $tag]);
8✔
5258
    }
5259

5260
    public function export($exportid, $tag)
5261
    {
5262
        $this->dbhm->preExec("UPDATE users_exports SET started = NOW() WHERE id = ? AND tag = ?;", [
7✔
5263
            $exportid,
7✔
5264
            $tag
7✔
5265
        ]);
7✔
5266

5267
        # For GDPR we support the ability for a user to export the data we hold about them.  Key points about this:
5268
        #
5269
        # - It needs to be at a high level of abstraction and understandable by the user, not just a cryptic data
5270
        #   dump.
5271
        # - It needs to include data provided by the user and data observed about the user, but not profiling
5272
        #   or categorisation based on that data.  This means that (for example) we need to return which
5273
        #   groups they have joined, but not whether joining those groups has flagged them up as a potential
5274
        #   spammer.
5275
        $ret = [];
7✔
5276
        error_log("...basic info");
7✔
5277

5278
        # Data in user table.
5279
        $d = [];
7✔
5280
        $d['Our_internal_ID_for_you'] = $this->getPrivate('id');
7✔
5281
        $d['Your_full_name'] = $this->getPrivate('fullname');
7✔
5282
        $d['Your_first_name'] = $this->getPrivate('firstname');
7✔
5283
        $d['Your_last_name'] = $this->getPrivate('lastname');
7✔
5284
        $d['Your_Yahoo_ID'] = $this->getPrivate('yahooid');
7✔
5285
        $d['Your_role_on_the_system'] = $this->getPrivate('systemrole');
7✔
5286
        $d['When_you_joined_the_site'] = Utils::ISODate($this->getPrivate('added'));
7✔
5287
        $d['When_you_last_accessed_the_site'] = Utils::ISODate($this->getPrivate('lastaccess'));
7✔
5288
        $d['When_we_last_checked_for_relevant_posts_for_you'] = Utils::ISODate($this->getPrivate('lastrelevantcheck'));
7✔
5289
        $d['Whether_your_email_is_bouncing'] = $this->getPrivate('bouncing') ? 'Yes' : 'No';
7✔
5290
        $d['Permissions_you_have_on_the_site'] = $this->getPrivate('permissions');
7✔
5291
        $d['Number_of_remaining_invitations_you_can_send_to_other_people'] = $this->getPrivate('invitesleft');
7✔
5292

5293
        $lastlocation = $this->user['lastlocation'];
7✔
5294

5295
        if ($lastlocation) {
7✔
5296
            $l = new Location($this->dbhr, $this->dbhm, $lastlocation);
×
5297
            $d['Last_location_you_posted_from'] = $l->getPrivate('name') . " (" . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
×
5298
        }
5299

5300
        $settings = $this->getPrivate('settings');
7✔
5301

5302
        if ($settings) {
7✔
5303
            $settings = json_decode($settings, TRUE);
7✔
5304

5305
            $location = Utils::presdef('id', Utils::presdef('mylocation', $settings, []), NULL);
7✔
5306

5307
            if ($location) {
7✔
5308
                $l = new Location($this->dbhr, $this->dbhm, $location);
6✔
5309
                $d['Last_location_you_entered'] = $l->getPrivate('name') . ' (' . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
6✔
5310
            }
5311

5312
            $notifications = Utils::pres('notifications', $settings);
7✔
5313

5314
            $d['Notifications']['Send_email_notifications_for_chat_messages'] = Utils::presdef('email', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5315
            $d['Notifications']['Send_email_notifications_of_chat_messages_you_send'] = Utils::presdef('emailmine', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5316
            $d['Notifications']['Send_push_notifications_to_web_browsers'] = Utils::presdef('push', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5317
            $d['Notifications']['Send_Facebook_notifications'] = Utils::presdef('facebook', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5318
            $d['Notifications']['Send_emails_about_notifications_on_the_site'] = Utils::presdef('notificationmails', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5319

5320
            $d['Hide_profile_picture'] = Utils::presdef('useprofile', $settings, TRUE) ? 'Yes' : 'No';
7✔
5321

5322
            if ($this->isModerator()) {
7✔
5323
                $d['Show_members_that_you_are_a_moderator'] = Utils::pres('showmod', $settings) ? 'Yes' : 'No';
1✔
5324

5325
                switch (Utils::presdef('modnotifs', $settings, 4)) {
1✔
5326
                    case 24:
1✔
5327
                        $d['Send_notifications_of_active_mod_work'] = 'After 24 hours';
×
5328
                        break;
×
5329
                    case 12:
1✔
5330
                        $d['Send_notifications_of_active_mod_work'] = 'After 12 hours';
×
5331
                        break;
×
5332
                    case 4:
1✔
5333
                        $d['Send_notifications_of_active_mod_work'] = 'After 4 hours';
1✔
5334
                        break;
1✔
5335
                    case 2:
×
5336
                        $d['Send_notifications_of_active_mod_work'] = 'After 2 hours';
×
5337
                        break;
×
5338
                    case 1:
×
5339
                        $d['Send_notifications_of_active_mod_work'] = 'After 1 hours';
×
5340
                        break;
×
5341
                    case 0:
×
5342
                        $d['Send_notifications_of_active_mod_work'] = 'Immediately';
×
5343
                        break;
×
5344
                    case -1:
5345
                        $d['Send_notifications_of_active_mod_work'] = 'Never';
×
5346
                        break;
×
5347
                }
5348

5349
                switch (Utils::presdef('backupmodnotifs', $settings, 12)) {
1✔
5350
                    case 24:
1✔
5351
                        $d['Send_notifications_of_backup_mod_work'] = 'After 24 hours';
×
5352
                        break;
×
5353
                    case 12:
1✔
5354
                        $d['Send_notifications_of_backup_mod_work'] = 'After 12 hours';
1✔
5355
                        break;
1✔
5356
                    case 4:
×
5357
                        $d['Send_notifications_of_backup_mod_work'] = 'After 4 hours';
×
5358
                        break;
×
5359
                    case 2:
×
5360
                        $d['Send_notifications_of_backup_mod_work'] = 'After 2 hours';
×
5361
                        break;
×
5362
                    case 1:
×
5363
                        $d['Send_notifications_of_backup_mod_work'] = 'After 1 hours';
×
5364
                        break;
×
5365
                    case 0:
×
5366
                        $d['Send_notifications_of_backup_mod_work'] = 'Immediately';
×
5367
                        break;
×
5368
                    case -1:
5369
                        $d['Send_notifications_of_backup_mod_work'] = 'Never';
×
5370
                        break;
×
5371
                }
5372

5373
                $d['Show_members_that_you_are_a_moderator'] = Utils::presdef('showmod', $settings, TRUE) ? 'Yes' : 'No';
1✔
5374
            }
5375
        }
5376

5377
        # Invitations.  Only show what we sent; the outcome is not this user's business.
5378
        error_log("...invitations");
7✔
5379
        $invites = $this->listInvitations("1970-01-01");
7✔
5380
        $d['invitations'] = [];
7✔
5381

5382
        foreach ($invites as $invite) {
7✔
5383
            $d['invitations'][] = [
6✔
5384
                'email' => $invite['email'],
6✔
5385
                'date' => Utils::ISODate($invite['date'])
6✔
5386
            ];
6✔
5387
        }
5388

5389
        error_log("...emails");
7✔
5390
        $d['emails'] = $this->getEmails();
7✔
5391

5392
        foreach ($d['emails'] as &$email) {
7✔
5393
            $email['added'] = Utils::ISODate($email['added']);
1✔
5394

5395
            if ($email['validated']) {
1✔
5396
                $email['validated'] = Utils::ISODate($email['validated']);
×
5397
            }
5398
        }
5399

5400
        error_log("...logins");
7✔
5401
        $d['logins'] = $this->dbhr->preQuery("SELECT type, uid, added, lastaccess FROM users_logins WHERE userid = ?;", [
7✔
5402
            $this->id
7✔
5403
        ]);
7✔
5404

5405
        foreach ($d['logins'] as &$dd) {
7✔
5406
            $dd['added'] = Utils::ISODate($dd['added']);
7✔
5407
            $dd['lastaccess'] = Utils::ISODate($dd['lastaccess']);
7✔
5408
        }
5409

5410
        error_log("...memberships");
7✔
5411
        $d['memberships'] = $this->getMemberships();
7✔
5412

5413
        error_log("...memberships history");
7✔
5414
        $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✔
5415
        $membs = $this->dbhr->preQuery($sql, [$this->id]);
7✔
5416
        foreach ($membs as &$memb) {
7✔
5417
            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
7✔
5418
            $memb['namedisplay'] = $name;
7✔
5419
            $memb['added'] = Utils::ISODate($memb['added']);
7✔
5420
        }
5421

5422
        $d['membershipshistory'] = $membs;
7✔
5423

5424
        error_log("...searches");
7✔
5425
        $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✔
5426
            $this->id
7✔
5427
        ]);
7✔
5428

5429
        foreach ($d['searches'] as &$s) {
7✔
5430
            $s['date'] = Utils::ISODate($s['date']);
×
5431
        }
5432

5433
        error_log("...alerts");
7✔
5434
        $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✔
5435
            $this->id
7✔
5436
        ]);
7✔
5437

5438
        foreach ($d['alerts'] as &$s) {
7✔
5439
            $s['responded'] = Utils::ISODate($s['responded']);
×
5440
        }
5441

5442
        error_log("...donations");
7✔
5443
        $d['donations'] = $this->dbhr->preQuery("SELECT * FROM users_donations WHERE userid = ? ORDER BY timestamp ASC;", [
7✔
5444
            $this->id
7✔
5445
        ]);
7✔
5446

5447
        foreach ($d['donations'] as &$s) {
7✔
5448
            $s['timestamp'] = Utils::ISODate($s['timestamp']);
1✔
5449
        }
5450

5451
        error_log("...bans");
7✔
5452
        $d['bans'] = [];
7✔
5453

5454
        $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE byuser = ?;", [
7✔
5455
            $this->id
7✔
5456
        ]);
7✔
5457

5458
        foreach ($bans as $ban) {
7✔
5459
            $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
5460
            $u = User::get($this->dbhr, $this->dbhm, $ban['userid']);
1✔
5461
            $d['bans'][] = [
1✔
5462
                'date' => Utils::ISODate($ban['date']),
1✔
5463
                'group' => $g->getName(),
1✔
5464
                'email' => $u->getEmailPreferred(),
1✔
5465
                'userid' => $ban['userid']
1✔
5466
            ];
1✔
5467
        }
5468

5469
        error_log("...spammers");
7✔
5470
        $d['spammers'] = $this->dbhr->preQuery("SELECT * FROM spam_users WHERE byuserid = ? ORDER BY added ASC;", [
7✔
5471
            $this->id
7✔
5472
        ]);
7✔
5473

5474
        foreach ($d['spammers'] as &$s) {
7✔
5475
            $s['added'] = Utils::ISODate($s['added']);
×
5476
            $u = User::get($this->dbhr, $this->dbhm, $s['userid']);
×
5477
            $s['email'] = $u->getEmailPreferred();
×
5478
        }
5479

5480
        $d['spamdomains'] = $this->dbhr->preQuery("SELECT domain, date FROM spam_whitelist_links WHERE userid = ?;", [
7✔
5481
            $this->id
7✔
5482
        ]);
7✔
5483

5484
        foreach ($d['spamdomains'] as &$s) {
7✔
5485
            $s['date'] = Utils::ISODate($s['date']);
×
5486
        }
5487

5488
        error_log("...images");
7✔
5489
        $images = $this->dbhr->preQuery("SELECT id, url FROM users_images WHERE userid = ?;", [
7✔
5490
            $this->id
7✔
5491
        ]);
7✔
5492

5493
        $d['images'] = [];
7✔
5494

5495
        foreach ($images as $image) {
7✔
5496
            if (Utils::pres('url', $image)) {
6✔
5497
                $d['images'][] = [
6✔
5498
                    'id' => $image['id'],
6✔
5499
                    'thumb' => $image['url']
6✔
5500
                ];
6✔
5501
            } else {
5502
                $a = new Attachment($this->dbhr, $this->dbhm, NULL, Attachment::TYPE_USER);
×
5503
                $d['images'][] = [
×
5504
                    'id' => $image['id'],
×
5505
                    'thumb' => $a->getPath(TRUE, $image['id'])
×
5506
                ];
×
5507
            }
5508
        }
5509

5510
        error_log("...notifications");
7✔
5511
        $d['notifications'] = $this->dbhr->preQuery("SELECT timestamp, url FROM users_notifications WHERE touser = ? AND seen = 1;", [
7✔
5512
            $this->id
7✔
5513
        ]);
7✔
5514

5515
        foreach ($d['notifications'] as &$n) {
7✔
5516
            $n['timestamp'] = Utils::ISODate($n['timestamp']);
×
5517
        }
5518

5519
        error_log("...addresses");
7✔
5520
        $d['addresses'] = [];
7✔
5521

5522
        $addrs = $this->dbhr->preQuery("SELECT * FROM users_addresses WHERE userid = ?;", [
7✔
5523
            $this->id
7✔
5524
        ]);
7✔
5525

5526
        foreach ($addrs as $addr) {
7✔
5527
            $a = new Address($this->dbhr, $this->dbhm, $addr['id']);
×
5528
            $d['addresses'][] = $a->getPublic();
×
5529
        }
5530

5531
        error_log("...events");
7✔
5532
        $d['communityevents'] = [];
7✔
5533

5534
        $events = $this->dbhr->preQuery("SELECT id FROM communityevents WHERE userid = ?;", [
7✔
5535
            $this->id
7✔
5536
        ]);
7✔
5537

5538
        foreach ($events as $event) {
7✔
5539
            $e = new CommunityEvent($this->dbhr, $this->dbhm, $event['id']);
×
5540
            $d['communityevents'][] = $e->getPublic();
×
5541
        }
5542

5543
        error_log("...volunteering");
7✔
5544
        $d['volunteering'] = [];
7✔
5545

5546
        $events = $this->dbhr->preQuery("SELECT id FROM volunteering WHERE userid = ?;", [
7✔
5547
            $this->id
7✔
5548
        ]);
7✔
5549

5550
        foreach ($events as $event) {
7✔
5551
            $e = new Volunteering($this->dbhr, $this->dbhm, $event['id']);
×
5552
            $d['volunteering'][] = $e->getPublic();
×
5553
        }
5554

5555
        error_log("...comments");
7✔
5556
        $d['comments'] = [];
7✔
5557
        $comms = $this->dbhr->preQuery("SELECT * FROM users_comments WHERE byuserid = ? ORDER BY date ASC;", [
7✔
5558
            $this->id
7✔
5559
        ]);
7✔
5560

5561
        foreach ($comms as &$comm) {
7✔
5562
            $u = User::get($this->dbhr, $this->dbhm, $comm['userid']);
1✔
5563
            $comm['email'] = $u->getEmailPreferred();
1✔
5564
            $comm['date'] = Utils::ISODate($comm['date']);
1✔
5565
            $d['comments'][] = $comm;
1✔
5566
        }
5567

5568
        error_log("...ratings");
7✔
5569
        $d['ratings'] = $this->getRated();
7✔
5570

5571
        error_log("...locations");
7✔
5572
        $d['locations'] = [];
7✔
5573

5574
        $locs = $this->dbhr->preQuery("SELECT * FROM locations_excluded WHERE userid = ?;", [
7✔
5575
            $this->id
7✔
5576
        ]);
7✔
5577

5578
        foreach ($locs as $loc) {
7✔
5579
            $g = Group::get($this->dbhr, $this->dbhm, $loc['groupid']);
×
5580
            $l = new Location($this->dbhr, $this->dbhm, $loc['locationid']);
×
5581
            $d['locations'][] = [
×
5582
                'group' => $g->getName(),
×
5583
                'location' => $l->getPrivate('name'),
×
5584
                'date' => Utils::ISODate($loc['date'])
×
5585
            ];
×
5586
        }
5587

5588
        error_log("...messages");
7✔
5589
        $msgs = $this->dbhr->preQuery("SELECT id FROM messages WHERE fromuser = ? ORDER BY arrival ASC;", [
7✔
5590
            $this->id
7✔
5591
        ]);
7✔
5592

5593
        $d['messages'] = [];
7✔
5594

5595
        foreach ($msgs as $msg) {
7✔
5596
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
×
5597

5598
            # Show all info here even moderator attributes.  This wouldn't normally be shown to users, but none
5599
            # of it is confidential really.
5600
            $thisone = $m->getPublic(FALSE, FALSE, TRUE);
×
5601

5602
            if (count($thisone['groups']) > 0) {
×
5603
                $g = Group::get($this->dbhr, $this->dbhm, $thisone['groups'][0]['groupid']);
×
5604
                $thisone['groups'][0]['namedisplay'] = $g->getName();
×
5605
            }
5606

5607
            $d['messages'][] = $thisone;
×
5608
        }
5609

5610
        # Chats.  Can't use listForUser as that filters on various things and has a ModTools vs FD distinction, and
5611
        # we're interested in information we have provided.  So we get the chats mentioned in the roster (we have
5612
        # provided information about being online) and where we have sent or reviewed a chat message.
5613
        error_log("...chats");
7✔
5614
        $chatids = $this->dbhr->preQuery("SELECT DISTINCT id, latestmessage FROM chat_rooms INNER JOIN (SELECT DISTINCT chatid FROM chat_roster WHERE userid = ? UNION SELECT DISTINCT chatid FROM chat_messages WHERE userid = ? OR reviewedby = ?) t ON t.chatid = chat_rooms.id ORDER BY latestmessage ASC;", [
7✔
5615
            $this->id,
7✔
5616
            $this->id,
7✔
5617
            $this->id
7✔
5618
        ]);
7✔
5619

5620
        $d['chatrooms'] = [];
7✔
5621
        $count = 0;
7✔
5622

5623
        foreach ($chatids as $chatid) {
7✔
5624
            # We don't return the chat name because it's too slow to produce.
5625
            $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid['id']);
6✔
5626
            $thisone = [
6✔
5627
                'id' => $chatid['id'],
6✔
5628
                'name' => $r->getPublic($this)['name'],
6✔
5629
                'messages' => []
6✔
5630
            ];
6✔
5631

5632
            $sql = "SELECT date, lastip FROM chat_roster WHERE `chatid` = ? AND userid = ?;";
6✔
5633
            $roster = $this->dbhr->preQuery($sql, [$chatid['id'], $this->id]);
6✔
5634
            foreach ($roster as $rost) {
6✔
5635
                $thisone['lastip'] = $rost['lastip'];
6✔
5636
                $thisone['date'] = Utils::ISODate($rost['date']);
6✔
5637
            }
5638

5639
            # Get the messages we have sent in this chat.
5640
            $msgs = $this->dbhr->preQuery("SELECT id FROM chat_messages WHERE chatid = ? AND (userid = ? OR reviewedby = ?);", [
6✔
5641
                $chatid['id'],
6✔
5642
                $this->id,
6✔
5643
                $this->id
6✔
5644
            ]);
6✔
5645

5646
            $userlist = NULL;
6✔
5647

5648
            foreach ($msgs as $msg) {
6✔
5649
                $cm = new ChatMessage($this->dbhr, $this->dbhm, $msg['id']);
6✔
5650
                $thismsg = $cm->getPublic(FALSE, $userlist);
6✔
5651

5652
                # Strip out most of the refmsg detail - it's not ours and we need to save volume of data.
5653
                $refmsg = Utils::pres('refmsg', $thismsg);
6✔
5654

5655
                if ($refmsg) {
6✔
5656
                    $thismsg['refmsg'] = [
×
5657
                        'id' => $msg['id'],
×
5658
                        'subject' => Utils::presdef('subject', $refmsg, NULL)
×
5659
                    ];
×
5660
                }
5661

5662
                $thismsg['mine'] = Utils::presdef('userid', $thismsg, NULL) == $this->id;
6✔
5663
                $thismsg['date'] = Utils::ISODate($thismsg['date']);
6✔
5664
                $thisone['messages'][] = $thismsg;
6✔
5665

5666
                $count++;
6✔
5667
//
5668
//                if ($count > 200) {
5669
//                    break 2;
5670
//                }
5671
            }
5672

5673
            if (count($thisone['messages']) > 0) {
6✔
5674
                $d['chatrooms'][] = $thisone;
6✔
5675
            }
5676
        }
5677

5678
        error_log("...newsfeed");
7✔
5679
        $newsfeeds = $this->dbhr->preQuery("SELECT * FROM newsfeed WHERE userid = ?;", [
7✔
5680
            $this->id
7✔
5681
        ]);
7✔
5682

5683
        $d['newsfeed'] = [];
7✔
5684

5685
        foreach ($newsfeeds as $newsfeed) {
7✔
5686
            $n = new Newsfeed($this->dbhr, $this->dbhm, $newsfeed['id']);
6✔
5687
            $thisone = $n->getPublic(FALSE, FALSE, FALSE, FALSE);
6✔
5688
            $d['newsfeed'][] = $thisone;
6✔
5689
        }
5690

5691
        $d['newsfeed_unfollows'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_unfollow WHERE userid = ?;", [
7✔
5692
            $this->id
7✔
5693
        ]);
7✔
5694

5695
        foreach ($d['newsfeed_unfollows'] as &$dd) {
7✔
5696
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5697
        }
5698

5699
        $d['newsfeed_likes'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_likes WHERE userid = ?;", [
7✔
5700
            $this->id
7✔
5701
        ]);
7✔
5702

5703
        foreach ($d['newsfeed_likes'] as &$dd) {
7✔
5704
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5705
        }
5706

5707
        $d['newsfeed_reports'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_reports WHERE userid = ?;", [
7✔
5708
            $this->id
7✔
5709
        ]);
7✔
5710

5711
        foreach ($d['newsfeed_reports'] as &$dd) {
7✔
5712
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5713
        }
5714

5715
        $d['aboutme'] = $this->dbhr->preQuery("SELECT timestamp, text FROM users_aboutme WHERE userid = ? AND LENGTH(text) > 5;", [
7✔
5716
            $this->id
7✔
5717
        ]);
7✔
5718

5719
        foreach ($d['aboutme'] as &$dd) {
7✔
5720
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5721
        }
5722

5723
        error_log("...stories");
7✔
5724
        $d['stories'] = $this->dbhr->preQuery("SELECT date, headline, story FROM users_stories WHERE userid = ?;", [
7✔
5725
            $this->id
7✔
5726
        ]);
7✔
5727

5728
        foreach ($d['stories'] as &$dd) {
7✔
5729
            $dd['date'] = Utils::ISODate($dd['date']);
×
5730
        }
5731

5732
        $d['stories_likes'] = $this->dbhr->preQuery("SELECT storyid FROM users_stories_likes WHERE userid = ?;", [
7✔
5733
            $this->id
7✔
5734
        ]);
7✔
5735

5736
        error_log("...exports");
7✔
5737
        $d['exports'] = $this->dbhr->preQuery("SELECT userid, started, completed FROM users_exports WHERE userid = ?;", [
7✔
5738
            $this->id
7✔
5739
        ]);
7✔
5740

5741
        foreach ($d['exports'] as &$dd) {
7✔
5742
            $dd['started'] = Utils::ISODate($dd['started']);
7✔
5743
            $dd['completed'] = Utils::ISODate($dd['completed']);
7✔
5744
        }
5745

5746
        error_log("...logs");
7✔
5747
        $l = new Log($this->dbhr, $this->dbhm);
7✔
5748
        $ctx = NULL;
7✔
5749
        $d['logs'] = $l->get(NULL, NULL, NULL, NULL, NULL, NULL, PHP_INT_MAX, $ctx, $this->id);
7✔
5750

5751
        error_log("...add group to logs");
7✔
5752
        $loggroups = [];
7✔
5753
        foreach ($d['logs'] as &$log) {
7✔
5754
            if (Utils::pres('groupid', $log)) {
7✔
5755
                # Don't put the whole group info in there, as it is slow to get.
5756
                if (!array_key_exists($log['groupid'], $loggroups)) {
7✔
5757
                    $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
7✔
5758

5759
                    if ($g->getId() == $log['groupid']) {
7✔
5760
                        $loggroups[$log['groupid']] = [
7✔
5761
                            'id' => $log['groupid'],
7✔
5762
                            'nameshort' => $g->getPrivate('nameshort'),
7✔
5763
                            'namedisplay' => $g->getName()
7✔
5764
                        ];
7✔
5765
                    } else {
5766
                        $loggroups[$log['groupid']] = [
×
5767
                            'id' => $log['groupid'],
×
5768
                            'nameshort' => "DeletedGroup{$log['groupid']}",
×
5769
                            'namedisplay' => "Deleted group #{$log['groupid']}"
×
5770
                        ];
×
5771
                    }
5772
                }
5773

5774
                $log['group'] = $loggroups[$log['groupid']];
7✔
5775
            }
5776
        }
5777

5778
        # Gift aid
5779
        $don = new Donations($this->dbhr, $this->dbhm);
7✔
5780
        $d['giftaid'] = $don->getGiftAid($this->id);
7✔
5781

5782
        $ret = $d;
7✔
5783

5784
        # There are some other tables with information which we don't return.  Here's what and why:
5785
        # - Not part of the current UI so can't have any user data
5786
        #     polls_users
5787
        # - Covered by data that we do return from other tables
5788
        #     messages_drafts, messages_history, messages_groups, messages_outcomes,
5789
        #     messages_promises, users_modmails, modnotifs, users_dashboard,
5790
        #     users_nudges
5791
        # - Transient logging data
5792
        #     logs_emails, logs_sql, logs_api, logs_errors, logs_src
5793
        # - Not provided by the user themselves
5794
        #     user_comments, messages_reneged, spam_users, users_banned, users_stories_requested,
5795
        #     users_thanks
5796
        # - Inferred or derived data.  These are not considered to be provided by the user (see p10 of
5797
        #   http://ec.europa.eu/newsroom/document.cfm?doc_id=44099)
5798
        #     users_kudos, visualise
5799

5800
        # Compress the data in the DB because it can be huge.
5801
        #
5802
        error_log("...filter");
7✔
5803
        Utils::filterResult($ret);
7✔
5804
        error_log("...encode");
7✔
5805
        $data = json_encode($ret, JSON_PARTIAL_OUTPUT_ON_ERROR);
7✔
5806
        error_log("...encoded length " . strlen($data) . ", now compress");
7✔
5807
        $data = gzdeflate($data);
7✔
5808
        $this->dbhm->preExec("UPDATE users_exports SET completed = NOW(), data = ? WHERE id = ? AND tag = ?;", [
7✔
5809
            $data,
7✔
5810
            $exportid,
7✔
5811
            $tag
7✔
5812
        ]);
7✔
5813
        error_log("...completed, length " . strlen($data));
7✔
5814

5815
        return ($ret);
7✔
5816
    }
5817

5818
    function getExport($userid, $id, $tag)
5819
    {
5820
        $ret = NULL;
2✔
5821

5822
        $exports = $this->dbhr->preQuery("SELECT * FROM users_exports WHERE userid = ? AND id = ? AND tag = ?;", [
2✔
5823
            $userid,
2✔
5824
            $id,
2✔
5825
            $tag
2✔
5826
        ]);
2✔
5827

5828
        foreach ($exports as $export) {
2✔
5829
            $ret = $export;
2✔
5830
            $ret['requested'] = $ret['requested'] ? Utils::ISODate($ret['requested']) : NULL;
2✔
5831
            $ret['started'] = $ret['started'] ? Utils::ISODate($ret['started']) : NULL;
2✔
5832
            $ret['completed'] = $ret['completed'] ? Utils::ISODate($ret['completed']) : NULL;
2✔
5833

5834
            if ($ret['completed']) {
2✔
5835
                # This has completed.  Return the data.  Will be zapped in cron exports..
5836
                $ret['data'] = json_decode(gzinflate($export['data']), TRUE);
2✔
5837
                $ret['infront'] = 0;
2✔
5838
            } else {
5839
                # Find how many are in front of us.
5840
                $infront = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_exports WHERE id < ? AND completed IS NULL;", [
2✔
5841
                    $id
2✔
5842
                ]);
2✔
5843

5844
                $ret['infront'] = $infront[0]['count'];
2✔
5845
            }
5846
        }
5847

5848
        return ($ret);
2✔
5849
    }
5850

5851
    public function limbo() {
5852
        # We set the deleted attribute, which will cause the v2 Go API not to return any personal data.  The user
5853
        # can still log in, and potentially recover their account by calling session with PATCH of deleted = NULL.
5854
        # Otherwise a background script will purge their account after a couple of weeks.
5855
        #
5856
        # This allows us to handle the fairly common case of users deleting their accounts by mistake, or changing
5857
        # their minds.  This often happens because one-click unsubscribe in emails, which we need to have for
5858
        # delivery.
5859
        $this->dbhm->preExec("UPDATE users SET deleted = NOW() WHERE id = ?;", [
5✔
5860
            $this->id
5✔
5861
        ]);
5✔
5862

5863
        # Send email notification about account removal
5864
        $email = $this->getEmailPreferred();
5✔
5865
        if ($email) {
5✔
5866
            try {
5867
                $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
5✔
5868
                $twig = new \Twig_Environment($loader);
5✔
5869

5870
                list ($transport, $mailer) = Mail::getMailer();
5✔
5871

5872
                $html = $twig->render('limbo.html', [
5✔
5873
                    'site_url' => 'https://' . USER_SITE
5✔
5874
                ]);
5✔
5875

5876
                $message = \Swift_Message::newInstance()
5✔
5877
                    ->setSubject("Your Freegle account has been removed as requested")
5✔
5878
                    ->setFrom([NOREPLY_ADDR => SITE_NAME])
5✔
5879
                    ->setReplyTo(SUPPORT_ADDR)
5✔
5880
                    ->setTo($email)
5✔
5881
                    ->setBody("We've removed your Freegle account as requested. Your personal data has been marked for deletion and will be completely removed from our systems within 14 days.\n\nIf you changed your mind or removed your account by mistake, don't worry! You can still reactivate it by visiting " . USER_SITE . " and logging back in.\n\nOnce you visit the site and log back in, your account will be automatically restored with all your data intact.\n\nIf you meant to remove your account, you can safely ignore this email. After 14 days, your data will be permanently deleted and cannot be recovered.");
5✔
5882

5883
                # Add HTML in base-64 as default quoted-printable encoding leads to problems on
5884
                # Outlook.
5885
                $htmlPart = \Swift_MimePart::newInstance();
5✔
5886
                $htmlPart->setCharset('utf-8');
5✔
5887
                $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
5✔
5888
                $htmlPart->setContentType('text/html');
5✔
5889
                $htmlPart->setBody($html);
5✔
5890
                $message->attach($htmlPart);
5✔
5891

5892
                Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::LIMBO, $this->id);
5✔
5893
                $this->sendIt($mailer, $message);
5✔
5894
            } catch (\Exception $e) {
×
5895
                error_log("Failed to send limbo email to user {$this->id}: " . $e->getMessage());
×
5896
            }
5897
        }
5898
    }
5899

5900
    public function processForgets($id = NULL) {
5901
        $count = 0;
1✔
5902

5903
        $idq = $id ? "AND id = $id" : "";
1✔
5904
        $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✔
5905

5906
        foreach ($users as $user) {
1✔
5907
            $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
5908
            $u->forget('Grace period');
1✔
5909
            $count++;
1✔
5910
        }
5911

5912
        return $count;
1✔
5913
    }
5914

5915
    public function forget($reason)
5916
    {
5917
        # Wipe a user of personal data, for the GDPR right to be forgotten.  We don't delete the user entirely
5918
        # otherwise it would mess up the stats.
5919

5920
        # Clear name etc.
5921
        $this->setPrivate('firstname', NULL);
11✔
5922
        $this->setPrivate('lastname', NULL);
11✔
5923
        $this->setPrivate('fullname', "Deleted User #" . $this->id);
11✔
5924
        $this->setPrivate('settings', NULL);
11✔
5925
        $this->setPrivate('yahooid', NULL);
11✔
5926

5927
        # Delete emails which aren't ours.
5928
        $emails = $this->getEmails();
11✔
5929

5930
        foreach ($emails as $email) {
11✔
5931
            if (!$email['ourdomain']) {
8✔
5932
                $this->removeEmail($email['email']);
8✔
5933
            }
5934
        }
5935

5936
        # Delete all logins.
5937
        $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ?;", [
11✔
5938
            $this->id
11✔
5939
        ]);
11✔
5940

5941
        # Delete the content (but not subject) of any messages, and any email header information such as their
5942
        # name and email address.
5943
        $msgs = $this->dbhm->preQuery("SELECT id FROM messages WHERE fromuser = ? AND messages.type IN (?, ?);", [
11✔
5944
            $this->id,
11✔
5945
            Message::TYPE_OFFER,
11✔
5946
            Message::TYPE_WANTED
11✔
5947
        ]);
11✔
5948

5949
        foreach ($msgs as $msg) {
11✔
5950
            $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✔
5951
                $msg['id']
1✔
5952
            ]);
1✔
5953

5954
            $this->dbhm->preExec("UPDATE messages_groups SET deleted = 1 WHERE msgid = ?;", [
1✔
5955
                $msg['id']
1✔
5956
            ]);
1✔
5957

5958
            # Delete outcome comments that they've added - just about might have personal data.
5959
            $this->dbhm->preExec("UPDATE messages_outcomes SET comments = NULL WHERE msgid = ?;", [
1✔
5960
                $msg['id']
1✔
5961
            ]);
1✔
5962

5963
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
1✔
5964

5965
            if (!$m->hasOutcome()) {
1✔
5966
                $m->withdraw('Withdrawn on user unsubscribe', NULL);
1✔
5967
            }
5968
        }
5969

5970
        # Remove all the content of all chat messages which they have sent (but not received).
5971
        $msgs = $this->dbhm->preQuery("SELECT id FROM chat_messages WHERE userid = ?;", [
11✔
5972
            $this->id
11✔
5973
        ]);
11✔
5974

5975
        foreach ($msgs as $msg) {
11✔
5976
            $this->dbhm->preExec("UPDATE chat_messages SET message = NULL WHERE id = ?;", [
1✔
5977
                $msg['id']
1✔
5978
            ]);
1✔
5979
        }
5980

5981
        # Delete completely any community events, volunteering opportunities, newsfeed posts, searches and stories
5982
        # they have created (their personal details might be in there), and any ratings by or about them.
5983
        $this->dbhm->preExec("DELETE FROM communityevents WHERE userid = ?;", [
11✔
5984
            $this->id
11✔
5985
        ]);
11✔
5986
        $this->dbhm->preExec("DELETE FROM volunteering WHERE userid = ?;", [
11✔
5987
            $this->id
11✔
5988
        ]);
11✔
5989
        $this->dbhm->preExec("DELETE FROM newsfeed WHERE userid = ?;", [
11✔
5990
            $this->id
11✔
5991
        ]);
11✔
5992
        $this->dbhm->preExec("DELETE FROM users_stories WHERE userid = ?;", [
11✔
5993
            $this->id
11✔
5994
        ]);
11✔
5995
        $this->dbhm->preExec("DELETE FROM users_searches WHERE userid = ?;", [
11✔
5996
            $this->id
11✔
5997
        ]);
11✔
5998
        $this->dbhm->preExec("DELETE FROM users_aboutme WHERE userid = ?;", [
11✔
5999
            $this->id
11✔
6000
        ]);
11✔
6001
        $this->dbhm->preExec("DELETE FROM ratings WHERE rater = ?;", [
11✔
6002
            $this->id
11✔
6003
        ]);
11✔
6004
        $this->dbhm->preExec("DELETE FROM ratings WHERE ratee = ?;", [
11✔
6005
            $this->id
11✔
6006
        ]);
11✔
6007

6008
        # Remove them from all groups.
6009
        $membs = $this->getMemberships();
11✔
6010

6011
        foreach ($membs as $memb) {
11✔
6012
            $this->removeMembership($memb['id']);
7✔
6013
        }
6014

6015
        # Delete any postal addresses
6016
        $this->dbhm->preExec("DELETE FROM users_addresses WHERE userid = ?;", [
11✔
6017
            $this->id
11✔
6018
        ]);
11✔
6019

6020
        # Delete any profile images
6021
        $this->dbhm->preExec("DELETE FROM users_images WHERE userid = ?;", [
11✔
6022
            $this->id
11✔
6023
        ]);
11✔
6024

6025
        # Remove any promises.
6026
        $this->dbhm->preExec("DELETE FROM messages_promises WHERE userid = ?;", [
11✔
6027
            $this->id
11✔
6028
        ]);
11✔
6029

6030
        $this->dbhm->preExec("UPDATE users SET forgotten = NOW(), tnuserid = NULL WHERE id = ?;", [
11✔
6031
            $this->id
11✔
6032
        ]);
11✔
6033

6034
        $this->dbhm->preExec("DELETE FROM sessions WHERE userid = ?;", [
11✔
6035
            $this->id
11✔
6036
        ]);
11✔
6037

6038
        $l = new Log($this->dbhr, $this->dbhm);
11✔
6039
        $l->log([
11✔
6040
            'type' => Log::TYPE_USER,
11✔
6041
            'subtype' => Log::SUBTYPE_DELETED,
11✔
6042
            'user' => $this->id,
11✔
6043
            'text' => $reason
11✔
6044
        ]);
11✔
6045
    }
6046

6047
    public function userRetention($userid = NULL)
6048
    {
6049
        # Find users who:
6050
        # - were added six months ago
6051
        # - are not on any groups
6052
        # - have not logged in for six months
6053
        # - are not on the spammer list
6054
        # - do not have mod notes
6055
        # - have no logs for six months
6056
        #
6057
        # We have no good reason to keep any data about them, and should therefore purge them.
6058
        $count = 0;
1✔
6059
        $userq = $userid ? " users.id = $userid AND " : '';
1✔
6060
        $mysqltime = date("Y-m-d", strtotime("6 months ago"));
1✔
6061
        
6062
        # First, delete users with @yahoogroups.com emails (test/old emails)
6063
        $yahoosql = "SELECT DISTINCT users.id FROM users 
1✔
6064
                     INNER JOIN users_emails ON users.id = users_emails.userid 
6065
                     WHERE $userq users_emails.email LIKE '%@yahoogroups.com' AND users.deleted IS NULL;";
1✔
6066
        $yahooUsers = $this->dbhr->preQuery($yahoosql);
1✔
6067
        
6068
        foreach ($yahooUsers as $user) {
1✔
6069
            error_log("Deleting Yahoo Groups user #{$user['id']}");
×
6070
            $u = new User($this->dbhr, $this->dbhm, $user['id']);
×
6071
            $u->delete();
×
6072
            $count++;
×
6073
            
6074
            # Prod garbage collection
6075
            User::clearCache();
×
6076
            gc_collect_cycles();
×
6077
        }
6078
        
6079
        $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✔
6080
        $users = $this->dbhr->preQuery($sql, [
1✔
6081
            User::SYSTEMROLE_USER
1✔
6082
        ]);
1✔
6083

6084
        foreach ($users as $user) {
1✔
6085
            $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✔
6086
                $user['id'],
1✔
6087
                Log::TYPE_USER,
1✔
6088
                Log::SUBTYPE_CREATED,
1✔
6089
                Log::SUBTYPE_DELETED
1✔
6090
            ]);
1✔
6091

6092
            error_log("#{$user['id']} Found logs " . count($logs) . " age " . (count($logs) > 0 ? $logs['0']['logsago'] : ' none '));
1✔
6093

6094
            if (count($logs) == 0 || $logs[0]['logsago'] > 90) {
1✔
6095
                error_log("...forget user #{$user['id']} " . (count($logs) > 0 ? $logs[0]['logsago'] : ''));
1✔
6096
                $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
6097
                $u->forget('Inactive');
1✔
6098
                $count++;
1✔
6099
            }
6100

6101
            # Prod garbage collection, as we've seen high memory usage by this.
6102
            User::clearCache();
1✔
6103
            gc_collect_cycles();
1✔
6104
        }
6105

6106
        # The only reason for preserving deleted users is as a placeholder user for messages they sent.  If they
6107
        # don't have any messages, they can go.
6108
        $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✔
6109
            $mysqltime
1✔
6110
        ]);
1✔
6111

6112
        $total = count($ids);
1✔
6113
        $count = 0;
1✔
6114

6115
        foreach ($ids as $id) {
1✔
6116
            $u = new User($this->dbhr, $this->dbhm, $id['id']);
1✔
6117
            #error_log("...delete user #{$id['id']}");
6118
            $u->delete();
1✔
6119

6120
            $count++;
1✔
6121

6122
            if ($count % 1000 == 0) {
1✔
6123
                error_log("...delete $count / $total");
×
6124
            }
6125

6126

6127
            # Prod garbage collection, as we've seen high memory usage by this.
6128
            User::clearCache();
1✔
6129
            gc_collect_cycles();
1✔
6130
        }
6131

6132
        return ($count);
1✔
6133
    }
6134

6135
    public function recordActive()
6136
    {
6137
        # We record this on an hourly basis.  Avoid pointless mod ops for cluster health.
6138
        $now = date("Y-m-d H:00:00", time());
2✔
6139
        $already = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ? AND timestamp = ?;", [
2✔
6140
            $this->id,
2✔
6141
            $now
2✔
6142
        ]);
2✔
6143

6144
        if (count($already) == 0) {
2✔
6145
            $this->dbhm->background("INSERT IGNORE INTO users_active (userid, timestamp) VALUES ({$this->id}, '$now');");
2✔
6146
        }
6147
    }
6148

6149
    public function getActive()
6150
    {
6151
        $active = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ?;", [$this->id]);
1✔
6152
        return ($active);
1✔
6153
    }
6154

6155
    public function mostActive($gid, $limit = 20)
6156
    {
6157
        $limit = intval($limit);
1✔
6158
        $earliest = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6159

6160
        $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✔
6161
            $gid,
1✔
6162
            User::SYSTEMROLE_USER,
1✔
6163
            $earliest
1✔
6164
        ]);
1✔
6165

6166
        $ret = [];
1✔
6167

6168
        foreach ($users as $user) {
1✔
6169
            $u = User::get($this->dbhr, $this->dbhm, $user['userid']);
1✔
6170
            $thisone = $u->getPublic();
1✔
6171
            $thisone['groupid'] = $gid;
1✔
6172
            $thisone['email'] = $u->getEmailPreferred();
1✔
6173

6174
            if (Utils::pres('memberof', $thisone)) {
1✔
6175
                foreach ($thisone['memberof'] as $group) {
1✔
6176
                    if ($group['id'] == $gid) {
1✔
6177
                        $thisone['joined'] = $group['added'];
1✔
6178
                    }
6179
                }
6180
            }
6181

6182
            $ret[] = $thisone;
1✔
6183
        }
6184

6185
        return ($ret);
1✔
6186
    }
6187

6188
    public function setAboutMe($text) {
6189
        $this->dbhm->preExec("INSERT INTO users_aboutme (userid, text) VALUES (?, ?);", [
3✔
6190
            $this->id,
3✔
6191
            $text
3✔
6192
        ]);
3✔
6193

6194
        $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id = {$this->id};");
3✔
6195

6196
        return($this->dbhm->lastInsertId());
3✔
6197
    }
6198

6199
    public function rate($rater, $ratee, $rating, $reason = NULL, $text = NULL) {
6200
        $ret = NULL;
2✔
6201

6202
        if ($rater != $ratee) {
2✔
6203
            # Can't rate yourself.
6204
            $review = $rating == User::RATING_DOWN && $reason && $text;
2✔
6205
            $this->dbhm->preExec("REPLACE INTO ratings (rater, ratee, rating, reason, text, timestamp, reviewrequired) VALUES (?, ?, ?, ?, ?, NOW(), ?);", [
2✔
6206
                $rater,
2✔
6207
                $ratee,
2✔
6208
                $rating,
2✔
6209
                $reason,
2✔
6210
                $text,
2✔
6211
                $review
2✔
6212
            ]);
2✔
6213

6214
            $ret = $this->dbhm->lastInsertId();
2✔
6215

6216
            $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id IN ($rater, $ratee);");
2✔
6217
        }
6218

6219
        return($ret);
2✔
6220
    }
6221

6222
    public function getRatings($uids) {
6223
        $mysqltime = date("Y-m-d", strtotime("Midnight 182 days ago"));
131✔
6224
        $ret = [];
131✔
6225
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
131✔
6226
        $myid = $me ? $me->getId() : NULL;
131✔
6227

6228
        # We show visible ratings, ones we have made ourselves, or those from TN.
6229
        $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;";
131✔
6230
        $ratings = $this->dbhr->preQuery($sql, [ $myid, $myid ]);
131✔
6231

6232
        foreach ($uids as $uid) {
131✔
6233
            $ret[$uid] = [
131✔
6234
                User::RATING_UP => 0,
131✔
6235
                User::RATING_DOWN => 0,
131✔
6236
                User::RATING_MINE => NULL
131✔
6237
            ];
131✔
6238

6239
            foreach ($ratings as $rate) {
131✔
6240
                if ($rate['ratee'] == $uid) {
1✔
6241
                    $ret[$uid][$rate['rating']] = $rate['count'];
1✔
6242
                }
6243
            }
6244
        }
6245

6246
        $ratings = $this->dbhr->preQuery("SELECT rating, ratee FROM ratings WHERE ratee IN (" . implode(',', $uids) . ") AND rater = ? AND timestamp >= '$mysqltime';", [
131✔
6247
            $myid
131✔
6248
        ]);
131✔
6249

6250
        foreach ($uids as $uid) {
131✔
6251
            if ($myid != $this->id) {
131✔
6252
                # We can't rate ourselves, so don't bother checking.
6253

6254
                foreach ($ratings as $rating) {
80✔
6255
                    if ($rating['ratee'] == $uid) {
1✔
6256
                        $ret[$uid][User::RATING_MINE] = $rating['rating'];
1✔
6257
                    }
6258
                }
6259
            }
6260
        }
6261

6262
        return($ret);
131✔
6263
    }
6264

6265
    public function getAllRatings($since) {
6266
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6267

6268
        $sql = "SELECT * FROM ratings WHERE timestamp >= ? AND visible = 1;";
1✔
6269
        $ratings = $this->dbhr->preQuery($sql, [
1✔
6270
            $mysqltime
1✔
6271
        ]);
1✔
6272

6273
        foreach ($ratings as &$rating) {
1✔
6274
            $rating['timestamp'] = Utils::ISODate($rating['timestamp']);
1✔
6275
        }
6276

6277
        return $ratings;
1✔
6278
    }
6279

6280
    public function getVisibleRatings($unreviewedonly, $since = '7 days ago') {
6281
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
3✔
6282
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
6283

6284
        $modships = $me->getModeratorships(NULL, TRUE);
3✔
6285

6286
        $ret = [];
3✔
6287
        $revq = $unreviewedonly ? " AND reviewrequired = 1" : '';
3✔
6288

6289
        if (count($modships)) {
3✔
6290
            $sql = "SELECT ratings.*, m1.groupid,
3✔
6291
       CASE WHEN u1.fullname IS NOT NULL THEN u1.fullname ELSE CONCAT(u1.firstname, ' ', u1.lastname) END AS raterdisplayname,
6292
       CASE WHEN u2.fullname IS NOT NULL THEN u2.fullname ELSE CONCAT(u2.firstname, ' ', u2.lastname) END AS rateedisplayname
6293
    FROM ratings 
6294
    INNER JOIN memberships m1 ON m1.userid = ratings.rater
6295
    INNER JOIN memberships m2 ON m2.userid = ratings.ratee
6296
    INNER JOIN users u1 ON ratings.rater = u1.id
6297
    INNER JOIN users u2 ON ratings.ratee = u2.id
6298
    WHERE ratings.timestamp >= ? AND 
6299
        m1.groupid IN (" . implode(',', $modships) . ") AND
3✔
6300
        m2.groupid IN (" . implode(',', $modships) . ") AND
3✔
6301
        m1.groupid = m2.groupid AND
6302
        ratings.rating IS NOT NULL 
6303
        $revq    
3✔
6304
        GROUP BY ratings.rater ORDER BY ratings.timestamp DESC;";
3✔
6305

6306
            $ret = $this->dbhr->preQuery($sql, [
3✔
6307
                $mysqltime
3✔
6308
            ]);
3✔
6309

6310
            foreach ($ret as &$r) {
3✔
6311
                $r['timestamp'] = Utils::ISODate($r['timestamp']);
1✔
6312
            }
6313
        }
6314

6315
        return $ret;
3✔
6316
    }
6317

6318
    public function ratingReviewed($ratingid) {
6319
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6320

6321
        $unreviewed = $me->getVisibleRatings(TRUE);
1✔
6322

6323
        foreach ($unreviewed as $r) {
1✔
6324
            if ($r['id'] == $ratingid) {
1✔
6325
                $this->dbhm->preExec("UPDATE ratings SET reviewrequired = 0 WHERE id = ?;", [
1✔
6326
                    $ratingid
1✔
6327
                ]);
1✔
6328
            }
6329
        }
6330
    }
6331

6332
    public function getChanges($since) {
6333
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6334

6335
        $users = $this->dbhr->preQuery("SELECT id, lastupdated FROM users WHERE lastupdated >= ?;", [
1✔
6336
            $mysqltime
1✔
6337
        ]);
1✔
6338

6339
        foreach ($users as &$user) {
1✔
6340
            $user['lastupdated'] = Utils::ISODate($user['lastupdated']);
1✔
6341
        }
6342

6343
        return $users;
1✔
6344
    }
6345

6346
    public function getRated() {
6347
        $rateds = $this->dbhr->preQuery("SELECT * FROM ratings WHERE rater = ?;", [
8✔
6348
            $this->id
8✔
6349
        ]);
8✔
6350

6351
        foreach ($rateds as &$rate) {
8✔
6352
            $rate['timestamp'] = Utils::ISODate($rate['timestamp']);
1✔
6353
        }
6354

6355
        return($rateds);
8✔
6356
    }
6357

6358
    public function getActiveSince($since, $createdbefore, $uid = NULL) {
6359
        $sincetime = date("Y-m-d H:i:s", strtotime($since));
1✔
6360
        $beforetime = date("Y-m-d H:i:s", strtotime($createdbefore));
1✔
6361
        $ids = $uid ? [
1✔
6362
            [
1✔
6363
                'id' => $uid
1✔
6364
            ]
1✔
6365
        ] : $this->dbhr->preQuery("SELECT id FROM users WHERE lastaccess >= ? AND added <= ?;", [
1✔
6366
            $sincetime,
1✔
6367
            $beforetime
1✔
6368
        ]);
1✔
6369

6370
        return(count($ids) ? array_filter(array_column($ids, 'id')) : []);
1✔
6371
    }
6372

6373
    public static function encodeId($id) {
6374
        # We're told that this is affecting our spam rating.  Let's see.
6375
        return '';
9✔
6376
//        $bin = base_convert($id, 10, 2);
6377
//        $bin = str_replace('0', '-', $bin);
6378
//        $bin = str_replace('1', '~', $bin);
6379
//        return($bin);
6380
    }
6381

6382
    public static function decodeId($enc) {
6383
        $enc = trim($enc);
×
6384
        $enc = str_replace('-', '0', $enc);
×
6385
        $enc = str_replace('~', '1', $enc);
×
6386
        $id  = base_convert($enc, 2, 10);
×
6387
        return($id);
×
6388
    }
6389

6390
    public function getCity()
6391
    {
6392
        $city = NULL;
23✔
6393

6394
        # Find the closest town
6395
        list ($lat, $lng, $loc) = $this->getLatLng(FALSE, TRUE);
23✔
6396

6397
        if ($lat || $lng) {
23✔
6398
            $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✔
6399
            #error_log("Get $sql, $lng, $lat");
6400
            $towns = $this->dbhr->preQuery($sql);
1✔
6401

6402
            foreach ($towns as $town) {
1✔
6403
                $city = $town['name'];
1✔
6404
            }
6405
        }
6406

6407
        return([ $city, $lat, $lng ]);
23✔
6408
    }
6409

6410
    public function microVolunteering() {
6411
        // Are we on a group where microvolunteering is enabled.
6412
        $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✔
6413
            $this->id
22✔
6414
        ]);
22✔
6415

6416
        return count($groups);
22✔
6417
    }
6418

6419
    public function getJobAds() {
6420
        # We want to show a few job ads from nearby.
6421
        $search = NULL;
35✔
6422
        $ret = '<span class="jobads">';
35✔
6423

6424
        list ($lat, $lng) = $this->getLatLng();
35✔
6425

6426
        if ($lat || $lng) {
35✔
6427
            $j = new Jobs($this->dbhr, $this->dbhm);
5✔
6428
            $jobs = $j->query($lat, $lng, 4);
5✔
6429

6430
            foreach ($jobs as $job) {
5✔
6431
                $loc = Utils::presdef('location', $job, '');
3✔
6432
                $title = "{$job['title']}" . ($loc !== ' ' ? " ($loc)" : '');
3✔
6433

6434
                # Link via our site to avoid spam trap warnings.
6435
                $url = "https://" . USER_SITE . "/job/{$job['id']}";
3✔
6436
                $ret .= '<a href="' . $url . '" target="_blank" style="color:black; font-weight:bold;">' . htmlentities($title) . '</a><br />';
3✔
6437
            }
6438
        }
6439

6440
        $ret .= '</span>';
35✔
6441

6442
        return([
35✔
6443
            'location' => $search,
35✔
6444
            'jobs' => $ret
35✔
6445
        ]);
35✔
6446
    }
6447

6448
    public function updateModMails($uid = NULL) {
6449
        # We maintain a count of recent modmails by scanning logs regularly, and pruning old ones.  This means we can
6450
        # find the value in a well-indexed way without the disk overhead of having a two-column index on logs.
6451
        #
6452
        # Ignore logs where the user is the same as the byuser - for example a user can delete their own posts, and we are
6453
        # only interested in things where a mod has done something to another user.
6454
        $mysqltime = date("Y-m-d H:i:s", strtotime("10 minutes ago"));
1✔
6455
        $uidq = $uid ? " AND user = $uid " : '';
1✔
6456

6457
        $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✔
6458
            $mysqltime
1✔
6459
        ]);
1✔
6460

6461
        foreach ($logs as $log) {
1✔
6462
            $this->dbhm->preExec("INSERT IGNORE INTO users_modmails (userid, logid, timestamp, groupid) VALUES (?,?,?,?);", [
1✔
6463
                $log['user'],
1✔
6464
                $log['id'],
1✔
6465
                $log['timestamp'],
1✔
6466
                $log['groupid']
1✔
6467
            ]);
1✔
6468
        }
6469

6470
        # Prune old ones.
6471
        $mysqltime = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6472
        $uidq2 = $uid ? " AND userid = $uid " : '';
1✔
6473

6474
        $logs = $this->dbhr->preQuery("SELECT id FROM users_modmails WHERE timestamp < ? $uidq2;", [
1✔
6475
            $mysqltime
1✔
6476
        ]);
1✔
6477

6478
        foreach ($logs as $log) {
1✔
6479
            $this->dbhm->preExec("DELETE FROM users_modmails WHERE id = ?;", [ $log['id'] ], FALSE);
×
6480
        }
6481
    }
6482

6483
    public function getModGroupsByActivity() {
6484
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
6485
        $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✔
6486
        return $this->dbhr->preQuery($sql, [
1✔
6487
            $this->id
1✔
6488
        ]);
1✔
6489
    }
6490

6491
    public function related($userlist) {
6492
        $userlist = array_unique($userlist);
2✔
6493

6494
        foreach ($userlist as $user1) {
2✔
6495
            foreach ($userlist as $user2) {
2✔
6496
                if ($user1 && $user2 && $user1 !== $user2) {
2✔
6497
                    # We may be passed user ids which no longer exist.
6498
                    $u1 = User::get($this->dbhr, $this->dbhm, $user1);
2✔
6499
                    $u2 = User::get($this->dbhr, $this->dbhm, $user2);
2✔
6500

6501
                    if ($u1->getId() && $u2->getId() && !$u1->isAdminOrSupport() && !$u2->isAdminOrSupport()) {
2✔
6502
                        $this->dbhm->background("INSERT INTO users_related (user1, user2) VALUES ($user1, $user2) ON DUPLICATE KEY UPDATE timestamp = NOW();");
2✔
6503
                    }
6504
                }
6505
            }
6506
        }
6507
    }
6508

6509
    public function getRelated($userid, $since = "30 days ago") {
6510
        $starttime = date("Y-m-d H:i:s", strtotime($since));
1✔
6511
        $users = $this->dbhr->preQuery("SELECT * FROM users_related WHERE user1 = ? AND timestamp >= '$starttime';", [
1✔
6512
            $userid
1✔
6513
        ]);
1✔
6514

6515
        return ($users);
1✔
6516
    }
6517

6518
    public function listRelated($groupids, &$ctx, $limit = 10) {
6519
        # The < condition ensures we don't duplicate during a single run.
6520
        $limit = intval($limit);
1✔
6521
        $ret = [];
1✔
6522
        $backstop = 100;
1✔
6523

6524
        do {
6525
            $ctx = $ctx ? $ctx : [ 'id'  => NULL ];
1✔
6526

6527
            if ($groupids && count($groupids)) {
1✔
6528
                $ctxq = ($ctx && intval($ctx['id'])) ? (" WHERE id < " . intval($ctx['id'])) : '';
1✔
6529
                $groupq = "(" . implode(',', $groupids) . ")";
1✔
6530
                $sql = "SELECT DISTINCT id, user1, user2 FROM (
1✔
6531
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6532
INNER JOIN memberships ON users_related.user1 = memberships.userid 
6533
INNER JOIN users u1 ON users_related.user1 = u1.id AND u1.deleted IS NULL AND u1.systemrole = 'User'
6534
WHERE 
6535
user1 < user2 AND
6536
notified = 0 AND
6537
memberships.groupid IN $groupq UNION
1✔
6538
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6539
INNER JOIN memberships ON users_related.user2 = memberships.userid 
6540
INNER JOIN users u2 ON users_related.user2 = u2.id AND u2.deleted IS NULL AND u2.systemrole = 'User'
6541
WHERE 
6542
user1 < user2 AND
6543
notified = 0 AND
6544
memberships.groupid IN $groupq 
1✔
6545
) t $ctxq ORDER BY id DESC LIMIT $limit;";
1✔
6546
                $members = $this->dbhr->preQuery($sql);
1✔
6547
            } else {
6548
                $ctxq = ($ctx && intval($ctx['id'])) ? (" AND users_related.id < " . intval($ctx['id'])) : '';
1✔
6549
                $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✔
6550
                $members = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
1✔
6551
            }
6552

6553
            $uids1 = array_column($members, 'user1');
1✔
6554
            $uids2 = array_column($members, 'user2');
1✔
6555

6556
            $related = [];
1✔
6557
            foreach ($members as $member) {
1✔
6558
                $related[$member['user1']] = $member['user2'];
1✔
6559
                $ctx['id'] = $member['id'];
1✔
6560
            }
6561

6562
            $users = $this->getPublicsById(array_merge($uids1, $uids2));
1✔
6563

6564
            foreach ($users as &$user1) {
1✔
6565
                if (Utils::pres($user1['id'], $related)) {
1✔
6566
                    $thisone = $user1;
1✔
6567

6568
                    foreach ($users as $user2) {
1✔
6569
                        if ($user2['id'] == $related[$user1['id']]) {
1✔
6570
                            $user2['userid'] = $user2['id'];
1✔
6571
                            $thisone['relatedto'] = $user2;
1✔
6572
                            break;
1✔
6573
                        }
6574
                    }
6575

6576
                    $logins = $this->getLogins(FALSE, $thisone['id'], TRUE);
1✔
6577
                    $rellogins = $this->getLogins(FALSE, $thisone['relatedto']['id'], TRUE);
1✔
6578

6579
                    if ($thisone['deleted'] ||
1✔
6580
                        $thisone['relatedto']['deleted'] ||
1✔
6581
                        $thisone['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6582
                        $thisone['relatedto']['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6583
                        !count($logins) ||
1✔
6584
                        !count($rellogins)) {
1✔
6585
                        # No sense in telling people about these.
6586
                        #
6587
                        # If there are n valid login types for one of the users - no way they can log in again so no point notifying.
6588
                        $this->dbhm->preExec("UPDATE users_related SET notified = 1 WHERE (user1 = ? AND user2 = ?) OR (user1 = ? AND user2 = ?);", [
1✔
6589
                            $thisone['id'],
1✔
6590
                            $thisone['relatedto']['id'],
1✔
6591
                            $thisone['relatedto']['id'],
1✔
6592
                            $thisone['id']
1✔
6593
                        ]);
1✔
6594
                    } else {
6595
                        $thisone['userid'] = $thisone['id'];
1✔
6596
                        $thisone['logins'] = $logins;
1✔
6597
                        $thisone['relatedto']['logins'] = $rellogins;
1✔
6598

6599
                        $ret[] = $thisone;
1✔
6600
                    }
6601
                }
6602
            }
6603

6604
            $backstop--;
1✔
6605
        } while ($backstop > 0 && count($ret) < $limit && count($members));
1✔
6606

6607
        return $ret;
1✔
6608
    }
6609

6610
    public function getExpectedReplies($uids, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6611
        # We count replies where the user has been active since the reply was requested, which means they've had
6612
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6613
        #
6614
        # $since here has to match the value in ChatRoom::
6615
        $starttime = date("Y-m-d H:i:s", strtotime($since));
131✔
6616
        $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) >= ?", [
131✔
6617
            $grace
131✔
6618
        ]);
131✔
6619

6620
        return($replies);
131✔
6621
    }
6622

6623
    public function listExpectedReplies($uid, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6624
        # We count replies where the user has been active since the reply was requested, which means they've had
6625
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6626
        #
6627
        # $since here has to match the value in ChatRoom::
6628
        $starttime = date("Y-m-d H:i:s", strtotime($since));
21✔
6629
        $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✔
6630
            $uid,
21✔
6631
            $grace
21✔
6632
        ]);
21✔
6633

6634
        $ret = [];
21✔
6635

6636
        if (count($replies)) {
21✔
6637
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6638
            $myid = $me ? $me->getId() : NULL;
1✔
6639

6640
            $r = new ChatRoom($this->dbhr, $this->dbhm);
1✔
6641
            $rooms = $r->fetchRooms(array_column($replies, 'chatid'), $myid, TRUE);
1✔
6642

6643
            foreach ($rooms as $room) {
1✔
6644
                $ret[] = [
1✔
6645
                    'id' => $room['id'],
1✔
6646
                    'name' => $room['name']
1✔
6647
                ];
1✔
6648
            }
6649
        }
6650

6651
        return $ret;
21✔
6652
    }
6653
    
6654
    public function getWorkCounts($groups = NULL) {
6655
        # Tell them what mod work there is.  Similar code in Notifications.
6656
        $ret = [];
26✔
6657
        $total = 0;
26✔
6658

6659
        $national = $this->hasPermission(User::PERM_NATIONAL_VOLUNTEERS);
26✔
6660

6661
        if ($national) {
26✔
6662
            $v = new Volunteering($this->dbhr, $this->dbhm);
1✔
6663
            $ret['pendingvolunteering'] = $v->systemWideCount();
1✔
6664
        }
6665

6666
        $s = new Spam($this->dbhr, $this->dbhm);
26✔
6667
        $spamcounts = $s->collectionCounts();
26✔
6668
        $ret['spammerpendingadd'] = $spamcounts[Spam::TYPE_PENDING_ADD];
26✔
6669
        $ret['spammerpendingremove'] = $spamcounts[Spam::TYPE_PENDING_REMOVE];
26✔
6670

6671
        # Show social actions from last 4 days.
6672
        $ctx = NULL;
26✔
6673
        $f = new GroupFacebook($this->dbhr, $this->dbhm);
26✔
6674
        $ret['socialactions'] = 0; // FB DISABLED = count($f->listSocialActions($ctx,$this));
26✔
6675

6676
        $g = new Group($this->dbhr, $this->dbhm);
26✔
6677
        $ret['popularposts'] = 0; // FB DISABLED count($g->getPopularMessages());
26✔
6678

6679
        if ($this->hasPermission(User::PERM_GIFTAID)) {
26✔
6680
            $d = new Donations($this->dbhr, $this->dbhm);
1✔
6681
            $ret['giftaid'] = $d->countGiftAidReview();
1✔
6682
        }
6683

6684
        if (!$groups) {
26✔
6685
            $groups = $this->getMemberships(FALSE, NULL, TRUE, TRUE, $this->id, FALSE);
11✔
6686
        }
6687

6688
        foreach ($groups as &$group) {
26✔
6689
            if (Utils::pres('work', $group)) {
20✔
6690
                foreach ($group['work'] as $key => $work) {
18✔
6691
                    if (Utils::pres($key, $ret)) {
18✔
6692
                        $ret[$key] += $work;
2✔
6693
                    } else {
6694
                        $ret[$key] = $work;
18✔
6695
                    }
6696
                }
6697
            }
6698
        }
6699

6700
        $s = new Story($this->dbhr, $this->dbhm);
26✔
6701
        $ret['stories'] = $s->getReviewCount(FALSE, $this, $groups);
26✔
6702
        $ret['newsletterstories'] = $this->hasPermission(User::PERM_NEWSLETTER) ? $s->getReviewCount(TRUE) : 0;
26✔
6703

6704
        // All the types of work which are worth nagging about.
6705
        $worktypes = [
26✔
6706
            'pendingvolunteering',
26✔
6707
            'socialactions',
26✔
6708
            'popularposts',
26✔
6709
            'chatreview',
26✔
6710
            'relatedmembers',
26✔
6711
            'stories',
26✔
6712
            'newsletterstories',
26✔
6713
            'pending',
26✔
6714
            'spam',
26✔
6715
            'pendingmembers',
26✔
6716
            'pendingevents',
26✔
6717
            'spammembers',
26✔
6718
            'editreview',
26✔
6719
            'pendingadmins'
26✔
6720
        ];
26✔
6721

6722
        if ($this->isAdminOrSupport()) {
26✔
6723
            $worktypes[] = 'spammerpendingadd';
1✔
6724
            $worktypes[] = 'spammerpendingremove';
1✔
6725
        }
6726

6727
        foreach ($worktypes as $key) {
26✔
6728
            $total += Utils::presdef($key, $ret, 0);
26✔
6729
        }
6730

6731
        $ret['total'] = $total;
26✔
6732

6733
        return $ret;
26✔
6734
    }
6735

6736
    public function ratingVisibility($since = "1 hour ago") {
6737
        $mysqltime = date("Y-m-d", strtotime($since));
1✔
6738

6739
        $ratings = $this->dbhr->preQuery("SELECT * FROM ratings WHERE timestamp >= ?;", [
1✔
6740
            $mysqltime
1✔
6741
        ]);
1✔
6742

6743
        foreach ($ratings as $rating) {
1✔
6744
            # A rating is visible to others if there is a chat between the two members, and
6745
            # - the ratee replied to a post, or
6746
            # - there is at least one message from each of them.
6747
            # This means that has been an exchange substantial enough for the rating not to just be frivolous.  It
6748
            # deliberately excludes interactions on ChitChat, where we have seen some people go a bit overboard on
6749
            # rating people.
6750
            $visible = FALSE;
1✔
6751
            #error_log("Check {$rating['rater']} rating of {$rating['ratee']}");
6752

6753
            $chats = $this->dbhr->preQuery("SELECT id FROM chat_rooms WHERE (user1 = ? AND user2 = ?) OR (user2 = ? AND user1 = ?)", [
1✔
6754
                $rating['rater'],
1✔
6755
                $rating['ratee'],
1✔
6756
                $rating['rater'],
1✔
6757
                $rating['ratee'],
1✔
6758
            ]);
1✔
6759

6760
            foreach ($chats as $chat) {
1✔
6761
                $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✔
6762
                    $chat['id']
1✔
6763
                ]);
1✔
6764

6765
                if ($distincts[0]['count'] >= 2) {
1✔
6766
                    #error_log("At least one real message from each of them in {$chat['id']}");
6767
                    $visible = TRUE;
1✔
6768
                } else {
6769
                    $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✔
6770
                        $chat['id'],
1✔
6771
                        $rating['ratee']
1✔
6772
                    ]);
1✔
6773

6774
                    if ($replies[0]['count']) {
1✔
6775
                        #error_log("Significant reply from {$rating['ratee']} in {$chat['id']}");
6776
                        $visible = TRUE;
1✔
6777
                    }
6778
                }
6779
            }
6780

6781
            #error_log("Use {$rating['rating']} from {$rating['rater']} ? " . ($visible ? 'yes': 'no'));
6782
            $oldvisible = intval($rating['visible']) ? TRUE : FALSE;
1✔
6783

6784
            if ($visible != $oldvisible) {
1✔
6785
                $this->dbhm->preExec("UPDATE ratings SET visible = ?, timestamp = NOW() WHERE id = ?;", [
1✔
6786
                    $visible,
1✔
6787
                    $rating['id']
1✔
6788
                ]);
1✔
6789
            }
6790
        }
6791
    }
6792

6793
    public function unban($groupid) {
6794
        $this->dbhm->preExec("DELETE FROM users_banned WHERE userid = ? AND groupid = ?;", [
4✔
6795
            $this->id,
4✔
6796
            $groupid
4✔
6797
        ]);
4✔
6798
    }
6799

6800
    public function hasFacebookLogin() {
6801
        $logins = $this->getLogins();
4✔
6802
        $ret = FALSE;
4✔
6803

6804
        foreach ($logins as $login) {
4✔
6805
            if ($login['type'] == User::LOGIN_FACEBOOK) {
4✔
6806
                $ret = TRUE;
1✔
6807
            }
6808
        }
6809

6810
        return $ret;
4✔
6811
    }
6812

6813
    public function memberReview($groupid, $request, $reason) {
6814
        $mysqltime = date('Y-m-d H:i');
6✔
6815

6816
        if ($request) {
6✔
6817
            # Requesting review.  Leave reviewedat unchanged, so that we can use it to avoid asking too
6818
            # frequently.
6819
            $this->setMembershipAtt($groupid, 'reviewreason', $reason);
4✔
6820
            $this->setMembershipAtt($groupid, 'reviewrequestedat', $mysqltime);
4✔
6821
        } else {
6822
            # We have reviewed.  Note that they might have been removed, in which case the set will do nothing.
6823
            $this->setMembershipAtt($groupid, 'reviewrequestedat', NULL);
3✔
6824
            $this->setMembershipAtt($groupid, 'reviewedat', $mysqltime);
3✔
6825
        }
6826
    }
6827

6828
    private function checkSupporterSettings($settings) {
6829
        $ret = TRUE;
78✔
6830

6831
        if ($settings) {
78✔
6832
            $s = json_decode($settings, TRUE);
15✔
6833

6834
            if ($s && array_key_exists('hidesupporter', $s)) {
15✔
6835
                $ret = !$s['hidesupporter'];
1✔
6836
            }
6837
        }
6838

6839
        return $ret;
78✔
6840
    }
6841

6842
    public function getSupporters(&$rets, $users) {
6843
        $idsleft = [];
304✔
6844

6845
        foreach ($rets as $userid => $ret) {
304✔
6846
            if (Utils::pres($userid, $users)) {
266✔
6847
                if (array_key_exists('supporter', $users[$userid])) {
11✔
6848
                    $rets[$userid]['supporter'] = $users[$userid]['supporter'];
3✔
6849
                    $rets[$userid]['donor'] = Utils::presdef('donor', $users[$userid], FALSE);
11✔
6850
                }
6851
            } else {
6852
                $idsleft[] = $userid;
261✔
6853
            }
6854
        }
6855

6856
        if (count($idsleft)) {
304✔
6857
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
261✔
6858
            $myid = $me ? $me->getId() : null;
261✔
6859

6860
            # A supporter is a mod, someone who has donated recently, or done microvolunteering recently.
6861
            if (count($idsleft)) {
261✔
6862
                $start = date('Y-m-d', strtotime("360 days ago"));
261✔
6863
                $info = $this->dbhr->preQuery(
261✔
6864
                    "SELECT DISTINCT users.id AS userid, settings, systemrole FROM users 
261✔
6865
    LEFT JOIN microactions ON users.id = microactions.userid
6866
    LEFT JOIN users_donations ON users_donations.userid = users.id 
6867
    WHERE users.id IN (" . implode(
261✔
6868
                        ',',
261✔
6869
                        $idsleft
261✔
6870
                    ) . ") AND 
261✔
6871
                    (systemrole IN (?, ?, ?) OR microactions.timestamp >= ? OR users_donations.timestamp >= ?);",
261✔
6872
                    [
261✔
6873
                        User::SYSTEMROLE_ADMIN,
261✔
6874
                        User::SYSTEMROLE_SUPPORT,
261✔
6875
                        User::SYSTEMROLE_MODERATOR,
261✔
6876
                        $start,
261✔
6877
                        $start
261✔
6878
                    ]
261✔
6879
                );
261✔
6880

6881
                $found = [];
261✔
6882

6883
                foreach ($info as $i) {
261✔
6884
                    $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
78✔
6885
                    $found[] = $i['userid'];
78✔
6886
                }
6887

6888
                $left = array_diff($idsleft, $found);
261✔
6889

6890
                # If we are one of the users, then we want to return whether we are a donor.
6891
                if (in_array($myid, $idsleft)) {
261✔
6892
                    $left[] = $myid;
146✔
6893
                    $left = array_filter(array_unique($left));
146✔
6894
                }
6895

6896
                if (count($left)) {
261✔
6897
                    $info = $this->dbhr->preQuery(
259✔
6898
                        "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(
259✔
6899
                            ',',
259✔
6900
                            $left
259✔
6901
                        ) . ") GROUP BY TransactionType;",
259✔
6902
                        [
259✔
6903
                            $start
259✔
6904
                        ]
259✔
6905
                    );
259✔
6906

6907
                    foreach ($info as $i) {
259✔
6908
                        $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
3✔
6909

6910
                        if ($i['userid'] == $myid) {
3✔
6911
                            # Only return this info for ourselves, otherwise it's a privacy leak.
6912
                            $rets[$i['userid']]['donor'] = TRUE;
3✔
6913

6914
                            if ($i['TransactionType'] == 'subscr_payment' || $i['TransactionType'] == 'recurring_payment') {
3✔
6915
                                $rets[$i['userid']]['donorrecurring'] = TRUE;
1✔
6916
                            }
6917
                        }
6918
                    }
6919
                }
6920
            }
6921
        }
6922
    }
6923

6924
    public function obfuscateEmail($email) {
6925
        $p = strpos($email, '@');
2✔
6926
        $q = strpos($email, 'privaterelay.appleid.com');
2✔
6927

6928
        if ($q) {
2✔
6929
            $email = 'Your Apple ID';
1✔
6930
        } else {
6931
            # For very short emails, we just show the first character.
6932
            if ($p <= 3) {
2✔
6933
                $email = substr($email, 0, 1) . "***" . substr($email, $p);
1✔
6934
            } else if ($p < 10) {
2✔
6935
                $email = substr($email, 0, 1) . "***" . substr($email, $p - 1);
2✔
6936
            } else {
6937
                $email = substr($email, 0, 3) . "***" . substr($email, $p - 3);
1✔
6938
            }
6939
        }
6940

6941
        return $email;
2✔
6942
    }
6943

6944
    public function setSimpleMail($simplemail) {
6945
        $s = $this->getPrivate('settings');
2✔
6946

6947
        if ($s) {
2✔
6948
            $settings = json_decode($s, TRUE);
1✔
6949
        } else {
6950
            $settings = [];
2✔
6951
        }
6952

6953
        $this->dbhm->beginTransaction();
2✔
6954

6955
        switch ($simplemail) {
6956
            case User::SIMPLE_MAIL_NONE: {
6957
                # No digests, no events/volunteering.
6958
                # No relevant or newsletters.
6959
                # No email notifications.
6960
                # No enagement.
6961
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 0, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
2✔
6962
                    $this->id
2✔
6963
                ]);
2✔
6964

6965
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
2✔
6966
                    $this->id
2✔
6967
                ]);
2✔
6968

6969
                $settings['notifications']['email'] = FALSE;
2✔
6970
                $settings['notifications']['emailmine'] = FALSE;
2✔
6971
                $settings['notificationmails']= FALSE;
2✔
6972
                $settings['engagement'] = FALSE;
2✔
6973
                break;
2✔
6974
            }
6975
            case User::SIMPLE_MAIL_BASIC: {
6976
                # Daily digests, no events/volunteering.
6977
                # No relevant or newsletters.
6978
                # Chat email notifications.
6979
                # No enagement.
6980
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 24, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
1✔
6981
                    $this->id
1✔
6982
                ]);
1✔
6983

6984
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
1✔
6985
                    $this->id
1✔
6986
                ]);
1✔
6987

6988
                $settings['notifications']['email'] = TRUE;
1✔
6989
                $settings['notifications']['emailmine'] = FALSE;
1✔
6990
                $settings['notificationmails']= FALSE;
1✔
6991
                $settings['engagement']= FALSE;
1✔
6992
                break;
1✔
6993
            }
6994
            case User::SIMPLE_MAIL_FULL: {
6995
                # Immediate mails, events/volunteering.
6996
                # Relevant and newsletters.
6997
                # Email notifications.
6998
                # Enagement.
6999
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = -1, eventsallowed = 1, volunteeringallowed = 1 WHERE userid = ?;", [
1✔
7000
                    $this->id
1✔
7001
                ]);
1✔
7002

7003
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 1,  newslettersallowed = 1 WHERE id = ?;", [
1✔
7004
                    $this->id
1✔
7005
                ]);
1✔
7006

7007
                $settings['notifications']['email'] = TRUE;
1✔
7008
                $settings['notifications']['emailmine'] = FALSE;
1✔
7009
                $settings['notificationmails']= TRUE;
1✔
7010
                $settings['engagement']= TRUE;
1✔
7011
                break;
1✔
7012
            }
7013
        }
7014

7015
        $settings['simplemail'] = $simplemail;
2✔
7016

7017
        $this->setPrivate('settings', json_encode($settings));
2✔
7018

7019
        # Holiday no longer exposed so turn off.
7020
        $this->dbhm->preExec("UPDATE users SET onholidaytill = NULL, settings = ? WHERE id = ?;", [
2✔
7021
            json_encode($settings),
2✔
7022
            $this->id
2✔
7023
        ]);
2✔
7024

7025
        $this->dbhm->commit();
2✔
7026
    }
7027

7028
    public function processMemberships($g = NULL) {
7029
        $memberships = $this->dbhr->preQuery("SELECT id FROM `memberships_history` WHERE processingrequired = 1 ORDER BY id ASC;");
6✔
7030

7031
        foreach ($memberships as $membership) {
6✔
7032
            $this->processMembership($membership['id'], $g);
6✔
7033
        }
7034
    }
7035

7036
    public function processMembership($id, $g) {
7037
        $memberships = $this->dbhr->preQuery("SELECT * FROM memberships_history WHERE id = ?;",[
6✔
7038
            $id
6✔
7039
        ]);
6✔
7040

7041
        foreach ($memberships as $membership) {
6✔
7042
            $groupid = $membership['groupid'];
6✔
7043
            $userid = $membership['userid'];
6✔
7044
            $collection = $membership['collection'];
6✔
7045

7046
            $g = $g ? $g : Group::get($this->dbhr, $this->dbhm, $groupid);
6✔
7047

7048
            # The membership didn't already exist.  We might want to send a welcome mail.
7049
            if ($g->getPrivate('welcomemail') && $collection == MembershipCollection::APPROVED && $g->getPrivate('onhere')) {
6✔
7050
                # They are now approved.  We need to send a per-group welcome mail.
7051
                try {
7052
                    $g->sendWelcome($userid, FALSE);
2✔
7053
                } catch (Exception $e) {
×
7054
                    error_log("Welcome failed: " . $e->getMessage());
×
7055
                    \Sentry\captureException($e);
×
7056
                }
7057
            }
7058

7059
            # Check whether this user now counts as a possible spammer.
7060
            $s = new Spam($this->dbhr, $this->dbhm);
6✔
7061
            $s->checkUser($userid, $groupid);
6✔
7062

7063
            # We might have mod notes which require this member to be flagged up.
7064
            $comments = $this->dbhr->preQuery(
6✔
7065
                "SELECT COUNT(*) AS count FROM users_comments WHERE userid = ? AND flag = 1;", [
6✔
7066
                    $userid,
6✔
7067
                ]
6✔
7068
            );
6✔
7069

7070
            if ($comments[0]['count'] > 0) {
6✔
7071
                $this->memberReview($groupid, TRUE, 'Note flagged to other groups');
1✔
7072
            }
7073

7074
            $this->dbhm->preExec("UPDATE memberships_history SET processingrequired = 0 WHERE id = ?", [
6✔
7075
                $id
6✔
7076
            ]);
6✔
7077
        }
7078
    }
7079

7080
    public function getUserKey($id) {
7081
        $key = null;
105✔
7082
        $sql = "SELECT * FROM users_logins WHERE userid = ? AND type = ?;";
105✔
7083
        $logins = $this->dbhr->preQuery($sql, [$id, User::LOGIN_LINK]);
105✔
7084
        foreach ($logins as $login) {
105✔
7085
            $key = $login['credentials'];
31✔
7086
        }
7087

7088
        if (!$key) {
105✔
7089
            $key = Utils::randstr(32);
104✔
7090
            $rc = $this->dbhm->preExec("INSERT INTO users_logins (userid, type, credentials) VALUES (?,?,?);", [
104✔
7091
                $id,
104✔
7092
                User::LOGIN_LINK,
104✔
7093
                $key
104✔
7094
            ]);
104✔
7095
        }
7096

7097
        return $key;
104✔
7098
    }
7099

7100
    public function assignUserToToDonation($email, $userid) {
7101
        $email = trim($email);
540✔
7102

7103
        if (strlen($email)) {
540✔
7104
            # We might have donations made via PayPal using this email address which we can now link to this user.  Do
7105
            # SELECT first to avoid this having to replicate in the cluster.
7106
            $donations = $this->dbhr->preQuery("SELECT id FROM users_donations WHERE Payer = ? AND userid IS NULL;", [
540✔
7107
                $email
540✔
7108
            ]);
540✔
7109

7110
            foreach ($donations as $donation) {
540✔
7111
                // Check if user exists before updating to avoid foreign key constraint violations
7112
                $userExists = $this->dbhr->preQuery("SELECT id FROM users WHERE id = ?;", [$userid]);
156✔
7113
                if (count($userExists) > 0) {
156✔
7114
                    $this->dbhm->preExec("UPDATE users_donations SET userid = ? WHERE id = ?;", [
156✔
7115
                        $userid,
156✔
7116
                        $donation['id']
156✔
7117
                    ]);
156✔
7118
                }
7119
            }
7120
        }
7121
    }
7122
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc