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

Freegle / iznik-server / 49885719-c516-447d-96c8-ce59b253cca2

03 Jan 2025 06:28PM UTC coverage: 92.309% (-0.01%) from 92.321%
49885719-c516-447d-96c8-ce59b253cca2

push

circleci

edwh
Test fixes.

25504 of 27629 relevant lines covered (92.31%)

31.52 hits per line

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

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

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

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

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

19
    const OPEN_AGE = 90;
20

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

251
                        return (true);
273✔
252
                    }
253
                }
254
            }
255
        }
256

257
        return (FALSE);
4✔
258
    }
259

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

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

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

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

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

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

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

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

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

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

305
        $name = NULL;
574✔
306

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

415
        $pw = '';
5✔
416

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

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

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

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

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

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

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

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

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

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

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

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

478
        foreach ($emails as $email) {
361✔
479
            if (!Mail::ourDomain($email['email']) && strpos($email['email'], '@yahoogroups.') === FALSE && strpos($email['email'], GROUP_DOMAIN) === FALSE) {
282✔
480
                $ret = $email['email'];
263✔
481
                break;
263✔
482
            }
483
        }
484

485
        return ($ret);
361✔
486
    }
487

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

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

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

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

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

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

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

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

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

538
        return (NULL);
1✔
539
    }
540

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

547
        $ret = NULL;
1✔
548

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

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

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

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

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

567
        return $ret;
4✔
568
    }
569

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

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

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

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

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

607
        return [ NULL, FALSE ];
168✔
608
    }
609

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

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

622
        return (NULL);
1✔
623
    }
624

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

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

635
        return (NULL);
3✔
636
    }
637

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

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

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

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

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

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

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

674
        return ($email);
464✔
675
    }
676

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

681
        # Invalidate cache.
682
        $this->emails = NULL;
465✔
683

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

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

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

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

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

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

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

744
        $this->assignUserToToDonation($email, $this->id);
465✔
745

746
        return ($rc);
465✔
747
    }
748

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

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

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

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

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

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

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

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

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

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

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

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

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

833
        return FALSE;
459✔
834
    }
835

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

842
        Session::clearSessionCache();
459✔
843

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

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

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

860
        $emailfrequency = 24;
459✔
861
        $eventsallowed = 1;
459✔
862
        $volunteeringallowed = 1;
459✔
863

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

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

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

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

902
        $membershipid = $this->dbhm->lastInsertId();
459✔
903

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

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

916
        $historyid = $this->dbhm->lastInsertId();
459✔
917

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

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

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

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

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

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

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

958
        return ($rc);
459✔
959
    }
960

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

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

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

976
        return ($this->memberships);
350✔
977
    }
978

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

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

992
        return ($val);
181✔
993
    }
994

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

1006
        return ($rc);
233✔
1007
    }
1008

1009
    public function removeMembership($groupid, $ban = FALSE, $spam = FALSE, $byemail = NULL)
1010
    {
1011
        $this->clearMembershipCache();
36✔
1012
        $g = Group::get($this->dbhr, $this->dbhm, $groupid);
36✔
1013
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
36✔
1014
        $meid = $me ? $me->getId() : NULL;
36✔
1015

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

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

1032
            $this->sendIt($mailer, $message);
1033
        }
1034
        // @codeCoverageIgnoreEnd
1035

1036
        if ($ban) {
36✔
1037
            $sql = "INSERT IGNORE INTO users_banned (userid, groupid, byuser) VALUES (?,?,?);";
13✔
1038
            $this->dbhm->preExec($sql, [
13✔
1039
                $this->id,
13✔
1040
                $groupid,
13✔
1041
                $meid
13✔
1042
            ]);
13✔
1043

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

1056
            foreach ($msgs as $msg) {
13✔
1057
                $m = new Message($this->dbhr, $this->dbhm, $msg['msgid']);
1✔
1058
                if (!$m->hasOutcome()) {
1✔
1059
                    $m->mark(Message::OUTCOME_WITHDRAWN, "Marked as withdrawn by ban", NULL, NULL);
1✔
1060
                }
1061
            }
1062
        }
1063

1064
        # Now remove the membership.
1065
        $rc = $this->dbhm->preExec("DELETE FROM memberships WHERE userid = ? AND groupid = ?;",
36✔
1066
            [
36✔
1067
                $this->id,
36✔
1068
                $groupid
36✔
1069
            ]);
36✔
1070

1071
        if ($this->dbhm->rowsAffected() || $ban) {
36✔
1072
            $l = new Log($this->dbhr, $this->dbhm);
36✔
1073
            $l->log([
36✔
1074
                'type' => Log::TYPE_GROUP,
36✔
1075
                'subtype' => Log::SUBTYPE_LEFT,
36✔
1076
                'user' => $this->id,
36✔
1077
                'byuser' => $meid,
36✔
1078
                'groupid' => $groupid,
36✔
1079
                'text' => $spam ? "Autoremoved spammer" : ($ban ? "via ban" : NULL)
36✔
1080
            ]);
36✔
1081
        }
1082

1083
        return ($rc);
36✔
1084
    }
1085

1086
    public function getMembershipGroupIds($modonly = FALSE, $grouptype = NULL, $id = NULL) {
1087
        $id = $id ? $id : $this->id;
5✔
1088

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

1100
    public function getMemberships($modonly = FALSE, $grouptype = NULL, $getwork = FALSE, $pernickety = FALSE, $id = NULL)
1101
    {
1102
        $id = $id ? $id : $this->id;
164✔
1103

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

1112
        $c = new ModConfig($this->dbhr, $this->dbhm);
164✔
1113

1114
        # Get all the groups efficiently.
1115
        $groupids = array_filter(array_column($groups, 'groupid'));
164✔
1116
        $gc = new GroupCollection($this->dbhr, $this->dbhm, $groupids);
164✔
1117
        $groupobjs = $gc->get();
164✔
1118
        $getworkids = [];
164✔
1119
        $groupsettings = [];
164✔
1120

1121
        for ($i = 0; $i < count($groupids); $i++) {
164✔
1122
            $group = $groups[$i];
127✔
1123
            $g = $groupobjs[$i];
127✔
1124
            $one = $g->getPublic();
127✔
1125

1126
            $one['role'] = $group['role'];
127✔
1127
            $one['collection'] = $group['collection'];
127✔
1128
            $amod = ($one['role'] == User::ROLE_MODERATOR || $one['role'] == User::ROLE_OWNER);
127✔
1129
            $one['configid'] = Utils::presdef('configid', $group, NULL);
127✔
1130

1131
            if ($amod && !Utils::pres('configid', $one)) {
127✔
1132
                # Get a config using defaults.
1133
                $one['configid'] = $c->getForGroup($id, $group['groupid']);
31✔
1134
            }
1135

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

1138
            # If we don't have our own email on this group we won't be sending mails.  This is what affects what
1139
            # gets shown on the Settings page for the user, and we only want to check this here
1140
            # for performance reasons.
1141
            $one['mysettings']['emailfrequency'] = ($group['type'] ==  Group::GROUP_FREEGLE &&
127✔
1142
                ($pernickety || $this->sendOurMails($g, FALSE, FALSE))) ?
127✔
1143
                (array_key_exists('emailfrequency', $one['mysettings']) ? $one['mysettings']['emailfrequency'] :  24)
88✔
1144
                : 0;
40✔
1145

1146
            $groupsettings[$group['groupid']] = $one['mysettings'];
127✔
1147

1148
            if ($getwork) {
127✔
1149
                # We need to find out how much work there is whether or not we are an active mod because we need
1150
                # to be able to see that it is there.  The UI shows it less obviously.
1151
                if ($amod) {
21✔
1152
                    $getworkids[] = $group['groupid'];
19✔
1153
                }
1154
            }
1155

1156
            $ret[] = $one;
127✔
1157
        }
1158

1159
        if ($getwork) {
164✔
1160
            # Get all the work.  This is across all groups for performance.
1161
            $g = new Group($this->dbhr, $this->dbhm);
29✔
1162
            $work = $g->getWorkCounts($groupsettings, $groupids);
29✔
1163

1164
            foreach ($getworkids as $groupid) {
29✔
1165
                foreach ($ret as &$group) {
19✔
1166
                    if ($group['id'] == $groupid) {
19✔
1167
                        $group['work'] = $work[$groupid];
19✔
1168
                    }
1169
                }
1170
            }
1171

1172
            # We might have been returned extra group info for wider chat review.  Add any extra groups from the work
1173
            # counts, in a basic way, so that the work counts appear.
1174
            #
1175
            # Find groupids in $work which are not in $ret.
1176
            $existingids = [];
29✔
1177
            foreach ($ret as $r) {
29✔
1178
                $existingids[] = $r['id'];
21✔
1179
            }
1180

1181

1182
            $extraworkids = [];
29✔
1183

1184
            foreach ($work as $gid => $w) {
29✔
1185
                if (!in_array($gid, $existingids)) {
21✔
1186
                    $extraworkids[] = $gid;
1✔
1187
                }
1188
            }
1189

1190
            # We have some subtle and baffling reference thing going on which is trashing the array.
1191
            # Do a json_encode/decode to remove them.
1192
            $ret = json_decode(json_encode($ret), TRUE);
29✔
1193
            foreach ($extraworkids as $groupid) {
29✔
1194
                $g = Group::get($this->dbhr, $this->dbhm, $groupid);
1✔
1195
                $group = $g->getPublic($groupid, TRUE);
1✔
1196
                $group['work'] = $work[$groupid];
1✔
1197
                $group['role'] = User::ROLE_NONMEMBER;
1✔
1198
                $ret[] = $group;
1✔
1199
            }
1200
        }
1201

1202
        return ($ret);
164✔
1203
    }
1204

1205
    public function getConfigs($all)
1206
    {
1207
        $ret = [];
25✔
1208
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
25✔
1209

1210
        if ($all) {
25✔
1211
            # We can see configs which
1212
            # - we created
1213
            # - are used by mods on groups on which we are a mod
1214
            # - defaults
1215
            $modships = $me ? $this->getModeratorships() : [];
24✔
1216
            $modships = count($modships) > 0 ? $modships : [0];
24✔
1217

1218
            $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;";
24✔
1219
            $ids = $this->dbhr->preQuery($sql);
24✔
1220
        } else {
1221
            # We only want to see the configs that we are actively using.  This reduces the size of what we return
1222
            # for people on many groups.
1223
            $sql = "SELECT DISTINCT configid AS id FROM memberships WHERE userid = ? AND configid IS NOT NULL;";
3✔
1224
            $ids = $this->dbhr->preQuery($sql, [ $me->getId() ]);
3✔
1225
        }
1226

1227
        $configids = array_filter(array_column($ids, 'id'));
25✔
1228

1229
        if ($configids) {
25✔
1230
            # Get all the info we need for the modconfig object in a single SELECT for performance.  This is particularly
1231
            # valuable for people on many groups and therefore with access to many modconfigs.
1232
            $sql = "SELECT DISTINCT mod_configs.*, 
4✔
1233
        CASE WHEN users.fullname IS NOT NULL THEN users.fullname ELSE CONCAT(users.firstname, ' ', users.lastname) END AS createdname 
1234
        FROM mod_configs LEFT JOIN users ON users.id = mod_configs.createdby
1235
        WHERE mod_configs.id IN (" . implode(',', $configids) . ");";
4✔
1236
            $configs = $this->dbhr->preQuery($sql);
4✔
1237

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

1242
            foreach ($configs as $config) {
4✔
1243
                $c = new ModConfig($this->dbhr, $this->dbhm, $config['id'], $config, $stdmsgs, $bulkops);
4✔
1244
                $thisone = $c->getPublic(FALSE);
4✔
1245

1246
                if (Utils::pres('createdby', $config)) {
4✔
1247
                    $ctx = NULL;
3✔
1248
                    $thisone['createdby'] = [
3✔
1249
                        'id' => $config['createdby'],
3✔
1250
                        'displayname' => $config['createdname']
3✔
1251
                    ];
3✔
1252
                }
1253

1254
                $ret[] = $thisone;
4✔
1255
            }
1256
        }
1257

1258
        # Return in alphabetical order.
1259
        $rc = usort($ret, function ($a, $b) {
25✔
1260
            return strcasecmp($a['name'], $b['name']);
1✔
1261
        });
25✔
1262

1263
        return ($ret);
25✔
1264
    }
1265

1266
    public function getModeratorships($id = NULL, $activeonly = FALSE)
1267
    {
1268
        $this->cacheMemberships($id);
150✔
1269
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
150✔
1270

1271
        $ret = [];
150✔
1272
        foreach ($this->memberships AS $membership) {
150✔
1273
            if ($membership['role'] == 'Owner' || $membership['role'] == 'Moderator') {
123✔
1274
                if (!$activeonly || $me->activeModForGroup($membership['groupid'])) {
88✔
1275
                    $ret[] = $membership['groupid'];
88✔
1276
                }
1277
            }
1278
        }
1279

1280
        return ($ret);
150✔
1281
    }
1282

1283
    public function isModOrOwner($groupid)
1284
    {
1285
        # Very frequently used.  Cache in session.
1286
        #error_log("modOrOwner " . var_export($_SESSION['modorowner'], TRUE));
1287
        if ((session_status() !== PHP_SESSION_NONE || getenv('UT')) &&
174✔
1288
            array_key_exists('modorowner', $_SESSION) &&
174✔
1289
            array_key_exists($this->id, $_SESSION['modorowner']) &&
174✔
1290
            array_key_exists($groupid, $_SESSION['modorowner'][$this->id])) {
174✔
1291
            return ($_SESSION['modorowner'][$this->id][$groupid]);
26✔
1292
        } else {
1293
            $sql = "SELECT groupid FROM memberships WHERE userid = ? AND role IN ('Moderator', 'Owner') AND groupid = ?;";
174✔
1294
            #error_log("$sql {$this->id}, $groupid");
1295
            $groups = $this->dbhr->preQuery($sql, [
174✔
1296
                $this->id,
174✔
1297
                $groupid
174✔
1298
            ]);
174✔
1299

1300
            foreach ($groups as $group) {
174✔
1301
                $_SESSION['modorowner'][$this->id][$groupid] = TRUE;
42✔
1302
                return TRUE;
42✔
1303
            }
1304

1305
            $_SESSION['modorowner'][$this->id][$groupid] = FALSE;
157✔
1306
            return (FALSE);
157✔
1307
        }
1308
    }
1309

1310
    public function getLogins($credentials = TRUE, $id = NULL, $excludelink = FALSE)
1311
    {
1312
        $excludelinkq = $excludelink ? (" AND type != '" . User::LOGIN_LINK . "'") : '';
284✔
1313

1314
        $logins = $this->dbhr->preQuery("SELECT * FROM users_logins WHERE userid = ? $excludelinkq ORDER BY lastaccess DESC;",
284✔
1315
            [$id ? $id : $this->id]);
284✔
1316

1317
        foreach ($logins as &$login) {
284✔
1318
            if (!$credentials) {
277✔
1319
                unset($login['credentials']);
23✔
1320
            }
1321
            $login['added'] = Utils::ISODate($login['added']);
277✔
1322
            $login['lastaccess'] = Utils::ISODate($login['lastaccess']);
277✔
1323
            $login['uid'] = '' . $login['uid'];
277✔
1324
        }
1325

1326
        return ($logins);
284✔
1327
    }
1328

1329
    public function findByLogin($type, $uid)
1330
    {
1331
        $logins = $this->dbhr->preQuery("SELECT * FROM users_logins WHERE uid = ? AND type = ?;",
5✔
1332
            [$uid, $type]);
5✔
1333

1334
        foreach ($logins as $login) {
5✔
1335
            return ($login['userid']);
4✔
1336
        }
1337

1338
        return (NULL);
5✔
1339
    }
1340

1341
    public function addLogin($type, $uid, $creds = NULL, $salt = PASSWORD_SALT)
1342
    {
1343
        if ($type == User::LOGIN_NATIVE) {
429✔
1344
            # Native login - encrypt the password a bit.  The password salt is global in FD, but per-login for users
1345
            # migrated from Norfolk.
1346
            $creds = $this->hashPassword($creds, $salt);
428✔
1347
            $uid = $this->id;
428✔
1348
        }
1349

1350
        # If the login with this type already exists in the table, that's fine.
1351
        $sql = "INSERT INTO users_logins (userid, uid, type, credentials, salt) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE credentials = ?, salt = ?;";
429✔
1352
        $rc = $this->dbhm->preExec($sql,
429✔
1353
            [$this->id, $uid, $type, $creds, $salt, $creds, $salt]);
429✔
1354

1355
        # If we add a login, we might be about to log in.
1356
        # TODO This is a bit hacky.
1357
        global $sessionPrepared;
429✔
1358
        $sessionPrepared = FALSE;
429✔
1359

1360
        return ($rc);
429✔
1361
    }
1362

1363
    public function removeLogin($type, $uid)
1364
    {
1365
        $rc = $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ? AND type = ? AND uid = ?;",
4✔
1366
            [$this->id, $type, $uid]);
4✔
1367
        return ($rc);
4✔
1368
    }
1369

1370
    public function getRoleForGroup($groupid, $overrides = TRUE)
1371
    {
1372
        # We can have a number of roles on a group
1373
        # - none, we can only see what is member
1374
        # - member, we are a group member and can see some extra info
1375
        # - moderator, we can see most info on a group
1376
        # - owner, we can see everything
1377
        #
1378
        # If our system role is support then we get moderator status; if it's admin we get owner status.
1379
        $role = User::ROLE_NONMEMBER;
75✔
1380

1381
        if ($overrides) {
75✔
1382
            switch ($this->getPrivate('systemrole')) {
35✔
1383
                case User::SYSTEMROLE_SUPPORT:
1384
                    $role = User::ROLE_MODERATOR;
3✔
1385
                    break;
3✔
1386
                case User::SYSTEMROLE_ADMIN:
1387
                    $role = User::ROLE_OWNER;
1✔
1388
                    break;
1✔
1389
            }
1390
        }
1391

1392
        # Now find if we have any membership of the group which might also give us a role.
1393
        $membs = $this->dbhr->preQuery("SELECT role FROM memberships WHERE userid = ? AND groupid = ?;", [
75✔
1394
            $this->id,
75✔
1395
            $groupid
75✔
1396
        ]);
75✔
1397

1398
        foreach ($membs as $memb) {
75✔
1399
            switch ($memb['role']) {
72✔
1400
                case 'Moderator':
72✔
1401
                    # Don't downgrade from owner if we have that by virtue of an override.
1402
                    $role = $role == User::ROLE_OWNER ? $role : User::ROLE_MODERATOR;
36✔
1403
                    break;
36✔
1404
                case 'Owner':
63✔
1405
                    $role = User::ROLE_OWNER;
8✔
1406
                    break;
8✔
1407
                case 'Member':
61✔
1408
                    # Don't downgrade if we already have a role by virtue of an override.
1409
                    $role = $role == User::ROLE_NONMEMBER ? User::ROLE_MEMBER : $role;
61✔
1410
                    break;
61✔
1411
            }
1412
        }
1413

1414
        return ($role);
75✔
1415
    }
1416

1417
    public function moderatorForUser($userid, $allowmod = FALSE)
1418
    {
1419
        # There are times when we want to check whether we can administer a user, but when we are not immediately
1420
        # within the context of a known group.  We can administer a user when:
1421
        # - they're only a user themselves
1422
        # - we are a mod on one of the groups on which they are a member.
1423
        # - it's us
1424
        if ($userid != $this->getId()) {
15✔
1425
            $u = User::get($this->dbhr, $this->dbhm, $userid);
12✔
1426

1427
            $usermemberships = [];
12✔
1428
            $modq = $allowmod ? ", 'Moderator', 'Owner'" : '';
12✔
1429
            $groups = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ? AND role IN ('Member' $modq);", [$userid]);
12✔
1430
            foreach ($groups as $group) {
12✔
1431
                $usermemberships[] = $group['groupid'];
8✔
1432
            }
1433

1434
            $mymodships = $this->getModeratorships();
12✔
1435

1436
            # Is there any group which we mod and which they are a member of?
1437
            $canmod = count(array_intersect($usermemberships, $mymodships)) > 0;
12✔
1438
        } else {
1439
            $canmod = TRUE;
5✔
1440
        }
1441

1442
        return ($canmod);
15✔
1443
    }
1444

1445
    public function getSetting($setting, $default)
1446
    {
1447
        $ret = $default;
461✔
1448
        $s = $this->getPrivate('settings');
461✔
1449

1450
        if ($s) {
461✔
1451
            $settings = json_decode($s, TRUE);
21✔
1452
            $ret = array_key_exists($setting, $settings) ? $settings[$setting] : $default;
21✔
1453
        }
1454

1455
        return ($ret);
461✔
1456
    }
1457

1458
    public function setSetting($setting, $val)
1459
    {
1460
        $s = $this->getPrivate('settings');
30✔
1461

1462
        if ($s) {
30✔
1463
            $settings = json_decode($s, TRUE);
1✔
1464
        } else {
1465
            $settings = [];
30✔
1466
        }
1467

1468
        $settings[$setting] = $val;
30✔
1469
        $this->setPrivate('settings', json_encode($settings));
30✔
1470
    }
1471

1472
    public function setGroupSettings($groupid, $settings)
1473
    {
1474
        $this->clearMembershipCache();
6✔
1475
        $sql = "UPDATE memberships SET settings = ? WHERE userid = ? AND groupid = ?;";
6✔
1476
        return ($this->dbhm->preExec($sql, [
6✔
1477
            json_encode($settings),
6✔
1478
            $this->id,
6✔
1479
            $groupid
6✔
1480
        ]));
6✔
1481
    }
1482

1483
    public function activeModForGroup($groupid, $mysettings = NULL)
1484
    {
1485
        $mysettings = $mysettings ? $mysettings : $this->getGroupSettings($groupid);
35✔
1486

1487
        # If we have the active flag use that; otherwise assume that the legacy showmessages flag tells us.  Default
1488
        # to active.
1489
        # TODO Retire showmessages entirely and remove from user configs.
1490
        $active = array_key_exists('active', $mysettings) ? $mysettings['active'] : (!array_key_exists('showmessages', $mysettings) || $mysettings['showmessages']);
35✔
1491
        return ($active);
35✔
1492
    }
1493

1494
    public function widerReview() {
1495
        # Check if we are participating in wider chat review, i.e. we are a mod on a group with that setting.
1496
        $modships = $this->getModeratorships();
20✔
1497
        $widerreview = FALSE;
20✔
1498

1499
        foreach ($modships as $mod) {
20✔
1500
            if ($this->activeModForGroup($mod)) {
18✔
1501
                $g = Group::get($this->dbhr, $this->dbhm, $mod);
18✔
1502
                if ($g->getSetting('widerchatreview', FALSE)) {
18✔
1503
                    $widerreview = TRUE;
1✔
1504
                    break;
1✔
1505
                }
1506
            }
1507
        }
1508

1509
        return $widerreview;
20✔
1510
    }
1511

1512
    public function getGroupSettings($groupid, $configid = NULL, $id = NULL)
1513
    {
1514
        $id = $id ? $id : $this->id;
154✔
1515

1516
        # We have some parameters which may give us some info which saves queries
1517
        $this->cacheMemberships($id);
154✔
1518

1519
        # Defaults match memberships ones in Group.php.
1520
        $defaults = [
154✔
1521
            'active' => 1,
154✔
1522
            'showchat' => 1,
154✔
1523
            'pushnotify' => 1,
154✔
1524
            'eventsallowed' => 1,
154✔
1525
            'volunteeringallowed' => 1
154✔
1526
        ];
154✔
1527

1528
        $settings = $defaults;
154✔
1529

1530
        if (Utils::pres($groupid, $this->memberships)) {
154✔
1531
            $set = $this->memberships[$groupid];
154✔
1532

1533
            if ($set['settings']) {
154✔
1534
                $settings = json_decode($set['settings'], TRUE);
5✔
1535

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

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

1544
            # Base active setting on legacy showmessages setting if not present.
1545
            $settings['active'] = array_key_exists('active', $settings) ? $settings['active'] : (!array_key_exists('showmessages', $settings) || $settings['showmessages']);
154✔
1546
            $settings['active'] = $settings['active'] ? 1 : 0;
154✔
1547

1548
            foreach ($defaults as $key => $val) {
154✔
1549
                if (!array_key_exists($key, $settings)) {
154✔
1550
                    $settings[$key] = $val;
5✔
1551
                }
1552
            }
1553

1554
            $settings['emailfrequency'] = $set['emailfrequency'];
154✔
1555
            $settings['eventsallowed'] = $set['eventsallowed'];
154✔
1556
            $settings['volunteeringallowed'] = $set['volunteeringallowed'];
154✔
1557
        }
1558

1559
        return ($settings);
154✔
1560
    }
1561

1562
    public function setRole($role, $groupid)
1563
    {
1564
        $rc = TRUE;
46✔
1565
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
46✔
1566

1567
        Session::clearSessionCache();
46✔
1568

1569
        $currentRole = $this->getRoleForGroup($groupid, FALSE);
46✔
1570

1571
        if ($currentRole != $role) {
46✔
1572
            $l = new Log($this->dbhr, $this->dbhm);
46✔
1573
            $l->log([
46✔
1574
                        'type' => Log::TYPE_USER,
46✔
1575
                        'byuser' => $me ? $me->getId() : NULL,
46✔
1576
                        'subtype' => Log::SUBTYPE_ROLE_CHANGE,
46✔
1577
                        'groupid' => $groupid,
46✔
1578
                        'user' => $this->id,
46✔
1579
                        'text' => $role
46✔
1580
                    ]);
46✔
1581

1582
            $this->clearMembershipCache();
46✔
1583
            $sql = "UPDATE memberships SET role = ? WHERE userid = ? AND groupid = ?;";
46✔
1584
            $rc = $this->dbhm->preExec($sql, [
46✔
1585
                $role,
46✔
1586
                $this->id,
46✔
1587
                $groupid
46✔
1588
            ]);
46✔
1589

1590
            # We might need to update the systemrole.
1591
            #
1592
            # Not the end of the world if this fails.
1593
            $this->updateSystemRole($role);
46✔
1594

1595
            if ($currentRole == User::ROLE_MEMBER) {
46✔
1596
                # We have promoted this member.  We want to ensure that they have no unread old chats.
1597
                $r = new ChatRoom($this->dbhr, $this->dbhm);
44✔
1598
                $r->upToDateAll($this->getId(),[
44✔
1599
                    ChatRoom::TYPE_USER2MOD
44✔
1600
                ]);
44✔
1601
            } else if (($currentRole == User::ROLE_MODERATOR || $currentRole == User::ROLE_OWNER) && $role == User::ROLE_MEMBER) {
16✔
1602
                # This member has been demoted.  Mail the other mods.
1603
                $g = Group::get($this->dbhr, $this->dbhm, $groupid);
15✔
1604
                $g->notifyAboutSignificantEvent($this->getName() . " is no longer a moderator on " . $g->getPrivate('nameshort'),
15✔
1605
                    "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✔
1606
                );
15✔
1607
            }
1608

1609
            $this->memberships = NULL;
46✔
1610
        }
1611

1612
        return ($rc);
46✔
1613
    }
1614

1615
    public function getActiveCounts() {
1616
        $users = [
1✔
1617
            $this->id => [
1✔
1618
                'id' => $this->id
1✔
1619
            ]];
1✔
1620

1621
        $this->getActiveCountss($users);
1✔
1622
        return($users[$this->id]['activecounts']);
1✔
1623
    }
1624

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

1629
        if (count($uids)) {
18✔
1630
            $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✔
1631
                $start,
17✔
1632
                MessageCollection::APPROVED
17✔
1633
            ]);
17✔
1634

1635
            foreach ($users as $user) {
17✔
1636
                $offers = 0;
17✔
1637
                $wanteds = 0;
17✔
1638

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

1649
                $users[$user['id']]['activecounts'] = [
17✔
1650
                    'offers' => $offers,
17✔
1651
                    'wanteds' => $wanteds
17✔
1652
                ];
17✔
1653
            }
1654
        }
1655
    }
1656

1657
    public function getInfos(&$users, $grace = ChatRoom::REPLY_GRACE) {
1658
        $uids = array_filter(array_column($users, 'id'));
124✔
1659

1660
        $start = date('Y-m-d', strtotime(User::OPEN_AGE . " days ago"));
124✔
1661
        $days90 = date("Y-m-d", strtotime("90 days ago"));
124✔
1662
        $userq = "userid IN (" . implode(',', $uids) . ")";
124✔
1663

1664
        foreach ($uids as $uid) {
124✔
1665
            $users[$uid]['info']['replies'] = 0;
124✔
1666
            $users[$uid]['info']['taken'] = 0;
124✔
1667
            $users[$uid]['info']['reneged'] = 0;
124✔
1668
            $users[$uid]['info']['collected'] = 0;
124✔
1669
            $users[$uid]['info']['openage'] = User::OPEN_AGE;
124✔
1670
        }
1671

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

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

1686
        $sql .= "(SELECT COUNT(DISTINCT(messages_reneged.msgid)) AS reneged, userid FROM messages_reneged WHERE $userq AND timestamp > ?) t3 ON t3.userid = t0.id LEFT JOIN
124✔
1687
(SELECT COUNT(DISTINCT messages_by.msgid) AS collected, messages_by.userid FROM messages_by INNER JOIN messages ON messages.id = messages_by.msgid INNER JOIN chat_messages ON chat_messages.refmsgid = messages.id AND messages.type = ? AND chat_messages.type = ? INNER JOIN messages_groups ON messages_groups.msgid = messages.id WHERE chat_messages.$userq AND messages_by.$userq AND messages_by.userid != messages.fromuser AND messages_groups.arrival >= '$days90') t4 ON t4.userid = t0.id LEFT JOIN
124✔
1688
(SELECT timestamp AS abouttime, text AS abouttext, userid FROM users_aboutme WHERE $userq ORDER BY timestamp DESC LIMIT 1) t5 ON t5.userid = t0.id
124✔
1689
;";
124✔
1690
        $counts = $this->dbhr->preQuery($sql, [
124✔
1691
            $start,
124✔
1692
            ChatMessage::TYPE_INTERESTED,
124✔
1693
            $start,
124✔
1694
            Message::TYPE_OFFER,
124✔
1695
            ChatMessage::TYPE_INTERESTED
124✔
1696
        ]);
124✔
1697

1698
        foreach ($users as $uid => $user) {
124✔
1699
            foreach ($counts as $count) {
124✔
1700
                if ($count['theuserid'] == $users[$uid]['id']) {
124✔
1701
                    $users[$uid]['info']['replies'] = $count['replycount'] ? $count['replycount'] : 0;
124✔
1702

1703
                    if (Session::modtools()) {
124✔
1704
                        $users[$uid]['info']['repliesoffer'] = $count['replycountoffer'] ? $count['replycountoffer'] : 0;
122✔
1705
                        $users[$uid]['info']['replieswanted'] = $count['replycountwanted'] ? $count['replycountwanted'] : 0;
122✔
1706
                    }
1707

1708
                    $users[$uid]['info']['reneged'] = $count['reneged'] ? $count['reneged'] : 0;
124✔
1709
                    $users[$uid]['info']['collected'] = $count['collected'] ? $count['collected'] : 0;
124✔
1710
                    $users[$uid]['info']['lastaccess'] = $count['lastaccess'] ? Utils::ISODate($count['lastaccess']) : NULL;
124✔
1711
                    $users[$uid]['info']['count'] = $count;
124✔
1712

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

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

1729
        foreach ($users as $uid => $user) {
124✔
1730
            $users[$uid]['info']['offers'] = 0;
124✔
1731
            $users[$uid]['info']['wanteds'] = 0;
124✔
1732
            $users[$uid]['info']['openoffers'] = 0;
124✔
1733
            $users[$uid]['info']['openwanteds'] = 0;
124✔
1734
            $users[$uid]['info']['expectedreply'] = 0;
124✔
1735

1736
            foreach ($counts as $count) {
124✔
1737
                if ($count['userid'] == $users[$uid]['id']) {
57✔
1738
                    if ($count['type'] == Message::TYPE_OFFER) {
57✔
1739
                        $users[$uid]['info']['offers'] += $count['count'];
40✔
1740

1741
                        if (!Utils::pres('outcome', $count)) {
40✔
1742
                            $users[$uid]['info']['openoffers'] += $count['count'];
40✔
1743
                        }
1744
                    } else if ($count['type'] == Message::TYPE_WANTED) {
19✔
1745
                        $users[$uid]['info']['wanteds'] += $count['count'];
7✔
1746

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

1755
        # Distance away.
1756
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
124✔
1757

1758
        if ($me) {
124✔
1759
            list ($mylat, $mylng, $myloc) = $me->getLatLng();
84✔
1760

1761
            if (!is_null($myloc)) {
84✔
1762
                $latlngs = $this->getLatLngs($users, FALSE, TRUE);
14✔
1763

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

1771
            $this->getPublicLocations($users);
84✔
1772
        }
1773

1774
        $r = new ChatRoom($this->dbhr, $this->dbhm);
124✔
1775
        $replytimes = $r->replyTimes($uids);
124✔
1776

1777
        foreach ($replytimes as $uid => $replytime) {
124✔
1778
            $users[$uid]['info']['replytime'] = $replytime;
124✔
1779
        }
1780

1781
        $nudges = $r->nudgeCounts($uids);
124✔
1782

1783
        foreach ($nudges as $uid => $nudgecount) {
124✔
1784
            $users[$uid]['info']['nudges'] = $nudgecount;
124✔
1785
        }
1786

1787
        $ratings = $this->getRatings($uids);
124✔
1788

1789
        foreach ($ratings as $uid => $rating) {
124✔
1790
            $users[$uid]['info']['ratings'] = $rating;
124✔
1791
        }
1792

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

1795
        foreach ($replies as $reply) {
124✔
1796
            if ($reply['expectee']) {
124✔
1797
                $users[$reply['expectee']]['info']['expectedreply'] = $reply['count'];
1✔
1798
            }
1799
        }
1800
    }
1801
    
1802
    public function getInfo($grace = ChatRoom::REPLY_GRACE)
1803
    {
1804
        $users = [
14✔
1805
            $this->id => [
14✔
1806
                'id' => $this->id
14✔
1807
            ]
14✔
1808
        ];
14✔
1809

1810
        $this->getInfos($users, $grace);
14✔
1811

1812
        return ($users[$this->id]['info']);
14✔
1813
    }
1814

1815
    public function getAboutMe() {
1816
        $ret = NULL;
49✔
1817

1818
        $aboutmes = $this->dbhr->preQuery("SELECT * FROM users_aboutme WHERE userid = ? ORDER BY timestamp DESC LIMIT 1;", [
49✔
1819
            $this->id
49✔
1820
        ]);
49✔
1821

1822
        foreach ($aboutmes as $aboutme) {
49✔
1823
            $ret = [
1✔
1824
                'timestamp' => Utils::ISODate($aboutme['timestamp']),
1✔
1825
                'text' => $aboutme['text']
1✔
1826
            ];
1✔
1827
        }
1828

1829
        return($ret);
49✔
1830
    }
1831

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

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

1847
    public function getDistanceBetween($mylat, $mylng, $tlat, $tlng)
1848
    {
1849
        $p1 = new POI($mylat, $mylng);
21✔
1850

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

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

1863
        $p2 = new POI($tlat, $tlng);
21✔
1864
        $metres = $p1->getDistanceInMetersTo($p2);
21✔
1865
        $miles = $metres / 1609.344;
21✔
1866
        $miles = $miles > 2 ? round($miles) : round($miles, 1);
21✔
1867
        return ($miles);
21✔
1868
    }
1869

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

1878
    public function getPublicLocation()
1879
    {
1880
        $users = [
30✔
1881
            $this->id => [
30✔
1882
                'id' => $this->id
30✔
1883
            ]
30✔
1884
        ];
30✔
1885

1886
        $this->getLatLngs($users);
30✔
1887
        $this->getPublicLocations($users);
30✔
1888

1889
        return($users[$this->id]['info']['publiclocation']);
30✔
1890
    }
1891

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

1899
        if ($settings) {
19✔
1900
            if (array_key_exists('useprofile', $settings) && !$settings['useprofile']) {
19✔
1901
                $forcedefault = TRUE;
1✔
1902
            }
1903
        }
1904

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

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

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

1933
                            break;
1✔
1934
                        }
1935
                    }
1936
                }
1937

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

1954
            $hash = NULL;
19✔
1955

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

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

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

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

1988
        if ($data) {
1✔
1989
            $img = @imagecreatefromstring($data);
1✔
1990

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

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

2008
    public function getPublic($groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [ MessageCollection::APPROVED ], $historyfull = FALSE)
2009
    {
2010
        $atts = [];
250✔
2011

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

2018
        return($atts);
250✔
2019
    }
2020

2021
    public function getPublicAtts(&$rets, $users, $me) {
2022
        foreach ($users as &$user) {
255✔
2023
            if (!array_key_exists($user['id'], $rets)) {
255✔
2024
                $rets[$user['id']] = [];
255✔
2025
            }
2026

2027
            $atts = $this->publicatts;
255✔
2028

2029
            if (Session::modtools()) {
255✔
2030
                # We have some extra attributes.
2031
                $atts[] = 'deleted';
238✔
2032
                $atts[] = 'lastaccess';
238✔
2033
                $atts[] = 'trustlevel';
238✔
2034
            }
2035

2036
            foreach ($atts as $att) {
255✔
2037
                $rets[$user['id']][$att] = Utils::presdef($att, $user, NULL);
255✔
2038
            }
2039

2040
            $rets[$user['id']]['settings'] = ['dummy' => TRUE];
255✔
2041

2042
            if (Utils::presdef('settings', $user, NULL)) {
255✔
2043
                # This is a bit of a type guddle.
2044
                if (gettype($user['settings']) == 'string') {
36✔
2045
                    $rets[$user['id']]['settings'] = json_decode($user['settings'], TRUE);
35✔
2046
                } else {
2047
                    $rets[$user['id']]['settings'] = $user['settings'];
1✔
2048
                }
2049
            }
2050

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

2056
            $rets[$user['id']]['settings']['notificationmails'] = array_key_exists('notificationmails', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['notificationmails'] : TRUE;
255✔
2057
            $rets[$user['id']]['settings']['engagement'] = array_key_exists('engagement', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['engagement'] : TRUE;
255✔
2058
            $rets[$user['id']]['settings']['modnotifs'] = array_key_exists('modnotifs', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['modnotifs'] : 4;
255✔
2059
            $rets[$user['id']]['settings']['backupmodnotifs'] = array_key_exists('backupmodnotifs', $rets[$user['id']]['settings']) ? $rets[$user['id']]['settings']['backupmodnotifs'] : 12;
255✔
2060

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

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

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

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

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

2091
            if (Utils::pres('deleted', $rets[$user['id']])) {
255✔
2092
                $rets[$user['id']]['deleted'] = Utils::ISODate($rets[$user['id']]['deleted']);
×
2093
            }
2094

2095
            if (Utils::pres('lastaccess', $rets[$user['id']])) {
255✔
2096
                $rets[$user['id']]['lastaccess'] = Utils::ISODate($rets[$user['id']]['lastaccess']);
238✔
2097
            }
2098
        }
2099
    }
2100
    
2101
    public function getPublicProfiles(&$rets, $users) {
2102
        $idsleft = [];
256✔
2103

2104
        foreach ($rets as $userid => $ret) {
256✔
2105
            if (Utils::pres($userid, $users)) {
256✔
2106
                if (Utils::pres('profile', $users[$userid])) {
8✔
2107
                    $rets[$userid]['profile'] = $users[$userid]['profile'];
1✔
2108
                } else {
2109
                    $idsleft[] = $userid;
8✔
2110
                }
2111
            } else {
2112
                $idsleft[] = $userid;
251✔
2113
            }
2114
        }
2115

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

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

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

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

2153
    public function getPublicHistory($me, &$rets, $users, $historyfull, $systemrole, $msgcoll = [ MessageCollection::APPROVED ]) {
2154
        $idsleft = [];
118✔
2155

2156
        foreach ($rets as $userid => $ret) {
118✔
2157
            if (Utils::pres($userid, $users)) {
118✔
2158
                if (array_key_exists('messagehistory', $users[$userid])) {
6✔
2159
                    $rets[$userid]['messagehistory'] = $users[$userid]['messagehistory'];
1✔
2160
                    $rets[$userid]['modmails'] = $users[$userid]['modmails'];
1✔
2161
                } else {
2162
                    $idsleft[] = $userid;
6✔
2163
                }
2164
            } else {
2165
                $idsleft[] = $userid;
113✔
2166
            }
2167
        }
2168

2169
        if (count($idsleft)) {
118✔
2170
            foreach ($rets as &$atts) {
118✔
2171
                $atts['messagehistory'] = [];
118✔
2172
            }
2173

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

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

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

2190
            if ($sql) {
118✔
2191
                $histories = $this->dbhr->preQuery(
118✔
2192
                    $sql,
118✔
2193
                    [
118✔
2194
                        $earliest
118✔
2195
                    ]
118✔
2196
                );
118✔
2197

2198
                foreach ($rets as $userid => $ret) {
118✔
2199
                    foreach ($histories as $history) {
118✔
2200
                        if ($history['fromuser'] == $ret['id']) {
34✔
2201
                            $history['arrival'] = Utils::pres('repostdate', $history) ? Utils::ISODate(
34✔
2202
                                $history['repostdate']
34✔
2203
                            ) : Utils::ISODate($history['arrival']);
34✔
2204
                            $history['date'] = Utils::ISODate($history['date']);
34✔
2205
                            $rets[$userid]['messagehistory'][] = $history;
34✔
2206
                        }
2207
                    }
2208
                }
2209
            }
2210

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

2220
            foreach ($idsleft as $userid) {
118✔
2221
                $rets[$userid]['modmails'] = 0;
118✔
2222
            }
2223

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

2234
    public function getPublicMemberOf(&$rets, $me, $freeglemod, $memberof, $systemrole) {
2235
        $userids = [];
238✔
2236

2237
        foreach ($rets as $ret) {
238✔
2238
            $ret['activearea'] = NULL;
238✔
2239

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

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

2259
            foreach ($rets as &$ret) {
77✔
2260
                $ret['memberof'] = [];
77✔
2261
                $ourEmailId = NULL;
77✔
2262

2263
                if (Utils::pres('emails', $ret)) {
77✔
2264
                    foreach ($ret['emails'] as $email) {
61✔
2265
                        if (Mail::ourDomain($email['email'])) {
61✔
2266
                            $ourEmailId = $email['id'];
6✔
2267
                        }
2268
                    }
2269
                }
2270

2271
                foreach ($groups as $group) {
77✔
2272
                    if ($ret['id'] ==  $group['userid']) {
65✔
2273
                        $name = $group['namefull'] ? $group['namefull'] : $group['nameshort'];
65✔
2274
                        $added = Utils::ISODate(Utils::pres('yadded', $group) ? $group['yadded'] : $group['added']);
65✔
2275
                        $addedago = floor((time() - strtotime($added)) / 86400);
65✔
2276

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

2296

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

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

2314
    public function getPublicApplied(&$rets, $users, $applied, $systemrole) {
2315
        if ($applied &&
238✔
2316
            $systemrole == User::ROLE_MODERATOR ||
180✔
2317
            $systemrole == User::SYSTEMROLE_ADMIN ||
205✔
2318
            $systemrole == User::SYSTEMROLE_SUPPORT
238✔
2319
        ) {
2320
            $idsleft = [];
78✔
2321

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

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

2344
                foreach ($rets as &$ret) {
78✔
2345
                    $ret['applied'] = [];
78✔
2346
                    $ret['activedistance'] = null;
78✔
2347

2348
                    foreach ($membs as $memb) {
78✔
2349
                        if ($ret['id'] == $memb['userid']) {
67✔
2350
                            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
67✔
2351
                            $memb['namedisplay'] = $name;
67✔
2352
                            $memb['added'] = Utils::ISODate($memb['added']);
67✔
2353
                            $memb['id'] = $memb['groupid'];
67✔
2354
                            unset($memb['groupid']);
67✔
2355

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

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

2366
                                $ret['activearea'] = $box;
2✔
2367

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

2380
                            $ret['applied'][] = $memb;
67✔
2381
                        }
2382
                    }
2383
                }
2384
            }
2385
        }
2386
    }
2387

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

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

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

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

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

2426
    public function getEmailHistory(&$rets) {
2427
        $userids = array_keys($rets);
3✔
2428

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

2431
        foreach ($rets as $retind => $ret) {
3✔
2432
            $rets[$retind]['emailhistory'] = [];
3✔
2433

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

2444
    public function getPublicsById($uids, $groupids = NULL, $history = TRUE, $comments = TRUE, $memberof = TRUE, $applied = TRUE, $modmailsonly = FALSE, $emailhistory = FALSE, $msgcoll = [MessageCollection::APPROVED], $historyfull = FALSE) {
2445
        $rets = [];
134✔
2446

2447
        # We might have some of these in cache, especially ourselves.
2448
        $uidsleft = [];
134✔
2449

2450
        foreach ($uids as $uid) {
134✔
2451
            $u = User::get($this->dbhr, $this->dbhm, $uid, TRUE, TRUE);
131✔
2452

2453
            if ($u) {
131✔
2454
                $rets[$uid] = $u->getPublic($groupids, $history, $comments, $memberof, $applied, $modmailsonly, $emailhistory, $msgcoll, $historyfull);
125✔
2455
            } else {
2456
                $uidsleft[] = $uid;
9✔
2457
            }
2458
        }
2459

2460
        $uidsleft = array_filter($uidsleft);
134✔
2461

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

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

2472
                foreach ($users as $user) {
7✔
2473
                    $rets[$user['id']] = $user;
7✔
2474
                }
2475
            }
2476
        }
2477

2478
        return($rets);
134✔
2479
    }
2480

2481
    public function isTN() {
2482
        return strpos($this->getEmailPreferred(), '@user.trashnothing.com') !== FALSE;
69✔
2483
    }
2484

2485
    public function isLJ() {
2486
        return $this->user['ljuserid'] !== NULL;
24✔
2487
    }
2488

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

2493
        foreach ($rets as &$ret) {
98✔
2494
            if (Utils::pres($ret['id'], $emails)) {
97✔
2495
                $ret['emails'] = $emails[$ret['id']];
69✔
2496
            }
2497
        }
2498
    }
2499

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

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

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

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

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

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

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

2546
                    if (Utils::pres('byuser', $log)) {
17✔
2547
                        if (!Utils::pres($log['byuser'], $users)) {
13✔
2548
                            $u = User::get($this->dbhr, $this->dbhm, $log['byuser']);
10✔
2549

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

2557
                        $log['byuser'] = $users[$log['byuser']];
13✔
2558
                    }
2559

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

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

2571
                        $log['user'] = $users[$log['user']];
16✔
2572
                    }
2573

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

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

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

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

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

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

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

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

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

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

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

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

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

2648
                    $log['timestamp'] = Utils::ISODate($log['timestamp']);
17✔
2649

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

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

2677
        $merges = array_unique($merges, SORT_REGULAR);
18✔
2678

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

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

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

2696
        $rets = [];
255✔
2697

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

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

2706
        if ($history) {
255✔
2707
            $this->getPublicHistory($me, $rets, $users, $historyfull, $systemrole, $msgcoll);
118✔
2708
        }
2709

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

2715
            if ($comments) {
238✔
2716
                $this->getComments($me, $rets);
195✔
2717
            }
2718

2719
            if ($emailhistory) {
238✔
2720
                $this->getEmailHistory($rets);
3✔
2721
            }
2722
        }
2723

2724
        return ($rets);
255✔
2725
    }
2726

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

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

2736
        # Use array_key_exists so that old sessions which predate this fix can continue.  Prevents shock of the new.
2737
        if ($ret && array_key_exists('supportAllowed', $_SESSION) && !$_SESSION['supportAllowed']) {
98✔
2738
            $ret = FALSE;
7✔
2739
        }
2740

2741
        return $ret;
98✔
2742
    }
2743

2744
    public function isModerator()
2745
    {
2746
        return ($this->user['systemrole'] == User::SYSTEMROLE_ADMIN ||
259✔
2747
            $this->user['systemrole'] == User::SYSTEMROLE_SUPPORT ||
259✔
2748
            $this->user['systemrole'] == User::SYSTEMROLE_MODERATOR);
259✔
2749
    }
2750

2751
    public function systemRoleMax($role1, $role2)
2752
    {
2753
        $role = User::SYSTEMROLE_USER;
14✔
2754

2755
        if ($role1 == User::SYSTEMROLE_MODERATOR || $role2 == User::SYSTEMROLE_MODERATOR) {
14✔
2756
            $role = User::SYSTEMROLE_MODERATOR;
4✔
2757
        }
2758

2759
        if ($role1 == User::SYSTEMROLE_SUPPORT || $role2 == User::SYSTEMROLE_SUPPORT) {
14✔
2760
            $role = User::SYSTEMROLE_SUPPORT;
1✔
2761
        }
2762

2763
        if ($role1 == User::SYSTEMROLE_ADMIN || $role2 == User::SYSTEMROLE_ADMIN) {
14✔
2764
            $role = User::SYSTEMROLE_ADMIN;
1✔
2765
        }
2766

2767
        return ($role);
14✔
2768
    }
2769

2770
    public function roleMax($role1, $role2)
2771
    {
2772
        $role = User::ROLE_NONMEMBER;
15✔
2773

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

2778
        if ($role1 == User::ROLE_MODERATOR || $role2 == User::ROLE_MODERATOR) {
15✔
2779
            $role = User::ROLE_MODERATOR;
8✔
2780
        }
2781

2782
        if ($role1 == User::ROLE_OWNER || $role2 == User::ROLE_OWNER) {
15✔
2783
            $role = User::ROLE_OWNER;
2✔
2784
        }
2785

2786
        return ($role);
15✔
2787
    }
2788

2789
    public function roleMin($role1, $role2)
2790
    {
2791
        $role = User::ROLE_OWNER;
6✔
2792

2793
        if ($role1 == User::ROLE_MODERATOR || $role2 == User::ROLE_MODERATOR) {
6✔
2794
            $role = User::ROLE_MODERATOR;
6✔
2795
        }
2796

2797
        if ($role1 == User::ROLE_MEMBER || $role2 == User::ROLE_MEMBER) {
6✔
2798
            $role = User::ROLE_MEMBER;
6✔
2799
        }
2800

2801
        if ($role1 == User::ROLE_NONMEMBER || $role2 == User::ROLE_NONMEMBER) {
6✔
2802
            $role = User::ROLE_NONMEMBER;
1✔
2803
        }
2804

2805
        return ($role);
6✔
2806
    }
2807

2808
    public function merge($id1, $id2, $reason, $forcemerge = FALSE)
2809
    {
2810
        error_log("Merge $id2 into $id1, $reason");
16✔
2811

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

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

2834
            $rc = $this->dbhm->beginTransaction();
14✔
2835
            $rollback = FALSE;
14✔
2836

2837
            if ($rc) {
14✔
2838
                try {
2839
                    #error_log("Started transaction");
2840
                    $rollback = TRUE;
14✔
2841

2842
                    # Merge the top-level memberships
2843
                    $id2membs = $this->dbhr->preQuery("SELECT * FROM memberships WHERE userid = $id2;");
14✔
2844
                    foreach ($id2membs as $id2memb) {
14✔
2845
                        $rc2 = $rc;
8✔
2846
                        # Jiggery-pokery with $rc for UT purposes.
2847
                        #error_log("$id2 member of {$id2memb['groupid']} ");
2848
                        $id1membs = $this->dbhr->preQuery("SELECT * FROM memberships WHERE userid = $id1 AND groupid = {$id2memb['groupid']};");
8✔
2849

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

2855
                            #error_log("Membership UPDATE merge returned $rc2");
2856
                        } else {
2857
                            # id1 is already a member, so we really have to merge.
2858
                            #
2859
                            # Our new membership has the highest role.
2860
                            $id1memb = $id1membs[0];
7✔
2861
                            $role = User::roleMax($id1memb['role'], $id2memb['role']);
7✔
2862
                            #error_log("...as is $id1, roles {$id1memb['role']} vs {$id2memb['role']} => $role");
2863

2864
                            if ($role != $id1memb['role']) {
7✔
2865
                                $rc2 = $this->dbhm->preExec("UPDATE memberships SET role = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
2✔
2866
                                    $role
2✔
2867
                                ]);
2✔
2868
                                #error_log("Set role $rc2");
2869
                            }
2870

2871
                            if ($rc2) {
7✔
2872
                                #  Our added date should be the older of the two.
2873
                                $date = min(strtotime($id1memb['added']), strtotime($id2memb['added']));
7✔
2874
                                $mysqltime = date("Y-m-d H:i:s", $date);
7✔
2875
                                $rc2 = $this->dbhm->preExec("UPDATE memberships SET added = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
7✔
2876
                                    $mysqltime
7✔
2877
                                ]);
7✔
2878
                                #error_log("Added $rc2");
2879
                            }
2880

2881
                            # There are several attributes we want to take the non-NULL version.
2882
                            foreach (['configid', 'settings', 'heldby'] as $key) {
6✔
2883
                                #error_log("Check {$id2memb['groupid']} memb $id2 $key = " . Utils::presdef($key, $id2memb, NULL));
2884
                                if ($id2memb[$key]) {
6✔
2885
                                    if ($rc2) {
1✔
2886
                                        $rc2 = $this->dbhm->preExec("UPDATE memberships SET $key = ? WHERE userid = $id1 AND groupid = {$id2memb['groupid']};", [
1✔
2887
                                            $id2memb[$key]
1✔
2888
                                        ]);
1✔
2889
                                        #error_log("Set att $key = {$id2memb[$key]} $rc2");
2890
                                    }
2891
                                }
2892
                            }
2893
                        }
2894

2895
                        $rc = $rc2 && $rc ? $rc2 : 0;
7✔
2896
                    }
2897

2898
                    # Merge the emails.  Both might have a primary address; if so then id1 wins.
2899
                    # There is a unique index, so there can't be a conflict on email.
2900
                    if ($rc) {
13✔
2901
                        $primary = NULL;
13✔
2902
                        $foundprim = FALSE;
13✔
2903
                        $sql = "SELECT * FROM users_emails WHERE userid = $id2 AND preferred = 1;";
13✔
2904
                        $emails = $this->dbhr->preQuery($sql);
13✔
2905
                        foreach ($emails as $email) {
13✔
2906
                            $primary = $email['id'];
5✔
2907
                            $foundprim = TRUE;
5✔
2908
                        }
2909

2910
                        $sql = "SELECT * FROM users_emails WHERE userid = $id1 AND preferred = 1;";
13✔
2911
                        $emails = $this->dbhr->preQuery($sql);
13✔
2912
                        foreach ($emails as $email) {
13✔
2913
                            $primary = $email['id'];
8✔
2914
                            $foundprim = TRUE;
8✔
2915
                        }
2916

2917
                        if (!$foundprim) {
13✔
2918
                            # No primary.  Whatever we would choose for id1 should become the new one.
2919
                            $pemail = $u1->getEmailPreferred();
4✔
2920
                            $emails = $this->dbhr->preQuery("SELECT * FROM users_emails WHERE email LIKE ?;", [
4✔
2921
                                $pemail
4✔
2922
                            ]);
4✔
2923

2924
                            foreach ($emails as $email) {
4✔
2925
                                $primary = $email['id'];
4✔
2926
                            }
2927
                        }
2928

2929
                        #error_log("Merge emails");
2930
                        $sql = "UPDATE users_emails SET userid = $id1, preferred = 0 WHERE userid = $id2;";
13✔
2931
                        $rc = $this->dbhm->preExec($sql);
13✔
2932

2933
                        if ($primary) {
13✔
2934
                            $sql = "UPDATE users_emails SET preferred = 1 WHERE id = $primary;";
13✔
2935
                            $rc = $this->dbhm->preExec($sql);
13✔
2936
                        }
2937

2938
                        #error_log("Emails now " . var_export($this->dbhm->preQuery("SELECT * FROM users_emails WHERE userid = $id1;"), true));
2939
                        #error_log("Email merge returned $rc");
2940
                    }
2941

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

2989
                        # Handle the bans.
2990
                        $this->dbhm->preExec("UPDATE IGNORE users_banned SET userid = $id1 WHERE userid = $id2;");
13✔
2991
                        $this->dbhm->preExec("UPDATE IGNORE users_banned SET byuser = $id1 WHERE byuser = $id2;");
13✔
2992

2993

2994
                        $bans = $this->dbhm->preQuery("SELECT * FROM users_banned WHERE userid = $id1");
13✔
2995
                        foreach ($bans as $ban) {
13✔
2996
                            # Make sure we are not a member; this could happen if one of the users is banned and
2997
                            # the other is not.
2998
                            $this->dbhm->preExec("DELETE FROM memberships WHERE userid = ? AND groupid = ?", [
1✔
2999
                                $id1,
1✔
3000
                                $ban['groupid']
1✔
3001
                            ]);
1✔
3002
                        }
3003

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

3011
                        foreach ($rooms as $room) {
13✔
3012
                            # Now see if there is already a chat room between the destination user and whatever this
3013
                            # one is.
3014
                            switch ($room['chattype']) {
1✔
3015
                                case ChatRoom::TYPE_USER2MOD;
1✔
3016
                                    $sql = "SELECT id FROM chat_rooms WHERE user1 = $id1 AND groupid = {$room['groupid']};";
1✔
3017
                                    break;
1✔
3018
                                case ChatRoom::TYPE_USER2USER;
1✔
3019
                                    $other = $room['user1'] == $id2 ? $room['user2'] : $room['user1'];
1✔
3020
                                    $sql = "SELECT id FROM chat_rooms WHERE (user1 = $id1 AND user2 = $other) OR (user2 = $id1 AND user1 = $other);";
1✔
3021
                                    break;
1✔
3022
                            }
3023

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

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

3031
                                # Make sure latestmessage is set correctly.
3032
                                $this->dbhm->preExec("UPDATE chat_rooms SET latestmessage = GREATEST(latestmessage, ?) WHERE id = ?", [
1✔
3033
                                    $room['latestmessage'],
1✔
3034
                                    $alreadys[0]['id']
1✔
3035
                                ]);
1✔
3036
                            } else {
3037
                                # No, there isn't, so we can update our old one.
3038
                                $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✔
3039
                                $this->dbhm->preExec($sql);
1✔
3040
                            }
3041
                        }
3042

3043
                        $this->dbhm->preExec("UPDATE chat_messages SET userid = $id1 WHERE userid = $id2;");
13✔
3044
                    }
3045

3046
                    # Merge attributes we want to keep if we have them in id2 but not id1.  Some will have unique
3047
                    # keys, so update to delete them.
3048
                    foreach (['fullname', 'firstname', 'lastname', 'yahooid'] as $att) {
13✔
3049
                        $users = $this->dbhm->preQuery("SELECT $att FROM users WHERE id = $id2;");
13✔
3050
                        foreach ($users as $user) {
13✔
3051
                            $this->dbhm->preExec("UPDATE users SET $att = NULL WHERE id = $id2;");
13✔
3052
                            User::clearCache($id1);
13✔
3053
                            User::clearCache($id2);
13✔
3054

3055
                            if (!$u1->getPrivate($att)) {
13✔
3056
                                if ($att != 'fullname') {
13✔
3057
                                    $this->dbhm->preExec("UPDATE users SET $att = ? WHERE id = $id1 AND $att IS NULL;", [$user[$att]]);
13✔
3058
                                } else if (stripos($user[$att], 'fbuser') === FALSE && stripos($user[$att], '-owner') === FALSE) {
4✔
3059
                                    # We don't want to overwrite a name with FBUser or a -owner address.
3060
                                    $this->dbhm->preExec("UPDATE users SET $att = ? WHERE id = $id1;", [$user[$att]]);
4✔
3061
                                }
3062
                            }
3063
                        }
3064
                    }
3065

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

3070
                        #error_log("Log merge 1 returned $rc");
3071
                    }
3072

3073
                    if ($rc) {
13✔
3074
                        $rc = $this->dbhm->preExec("UPDATE logs SET byuser = $id1 WHERE byuser = $id2;");
13✔
3075

3076
                        #error_log("Log merge 2 returned $rc");
3077
                    }
3078

3079
                    # Merge the fromuser in messages.  There might not be any, and it's not the end of the world
3080
                    # if this info isn't correct, so ignore the rc.
3081
                    #error_log("Merge messages, current rc $rc");
3082
                    if ($rc) {
13✔
3083
                        $this->dbhm->preExec("UPDATE messages SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
3084
                    }
3085

3086
                    # Merge the history
3087
                    #error_log("Merge history, current rc $rc");
3088
                    if ($rc) {
13✔
3089
                        $this->dbhm->preExec("UPDATE messages_history SET fromuser = $id1 WHERE fromuser = $id2;");
13✔
3090
                        $this->dbhm->preExec("UPDATE memberships_history SET userid = $id1 WHERE userid = $id2;");
13✔
3091
                    }
3092

3093
                    # Merge the systemrole.
3094
                    $u1s = $this->dbhr->preQuery("SELECT systemrole FROM users WHERE id = $id1;");
13✔
3095
                    foreach ($u1s as $u1) {
13✔
3096
                        $u2s = $this->dbhr->preQuery("SELECT systemrole FROM users WHERE id = $id2;");
13✔
3097
                        foreach ($u2s as $u2) {
13✔
3098
                            $rc = $this->dbhm->preExec("UPDATE users SET systemrole = ? WHERE id = $id1;", [
13✔
3099
                                $this->systemRoleMax($u1['systemrole'], $u2['systemrole'])
13✔
3100
                            ]);
13✔
3101
                        }
3102
                        User::clearCache($id1);
13✔
3103
                    }
3104

3105
                    # Merge the add date.
3106
                    $u1 = User::get($this->dbhr, $this->dbhm, $id1);
13✔
3107
                    $u2 = User::get($this->dbhr, $this->dbhm, $id2);
13✔
3108
                    $this->dbhm->preExec("UPDATE users SET added = ? WHERE id = $id1;", [
13✔
3109
                        strtotime($u1->getPrivate('added')) < strtotime($u2->getPrivate('added')) ? $u1->getPrivate('added') : $u2->getPrivate('added')
13✔
3110
                    ]);
13✔
3111

3112
                    $this->dbhm->preExec("UPDATE users SET lastupdated = NOW() WHERE id = ?;", [
13✔
3113
                        $id1
13✔
3114
                    ]);
13✔
3115

3116
                    $tnid1 = $u2->getPrivate('tnuserid');
13✔
3117
                    $tnid2 = $u2->getPrivate('tnuserid');
13✔
3118

3119
                    if (!$tnid1 && $tnid2) {
13✔
3120
                        $u2->setPrivate('tnuserid', NULL);
×
3121
                        $u1->setPrivate('tnuserid', $tnid2);
×
3122
                    }
3123

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

3128
                    if (count($giftaids)) {
13✔
3129
                        $weights = [
1✔
3130
                            Donations::PERIOD_PAST_4_YEARS_AND_FUTURE => 0,
1✔
3131
                            Donations::PERIOD_SINCE => 1,
1✔
3132
                            Donations::PERIOD_FUTURE=> 2,
1✔
3133
                            Donations::PERIOD_THIS => 3,
1✔
3134
                            Donations::PERIOD_DECLINED => 4
1✔
3135
                        ];
1✔
3136

3137
                        $best = NULL;
1✔
3138
                        foreach ($giftaids as $giftaid) {
1✔
3139
                            if ($best == NULL ||
1✔
3140
                                $weights[$giftaid['period']] < $weights[$best['period']]) {
1✔
3141
                                $best = $giftaid;
1✔
3142
                            }
3143
                        }
3144

3145
                        foreach ($giftaids as $giftaid) {
1✔
3146
                            if ($giftaid['id'] != $best['id']) {
1✔
3147
                                $this->dbhm->preExec("DELETE FROM giftaid WHERE id = ?;", [
1✔
3148
                                    $giftaid['id']
1✔
3149
                                ]);
1✔
3150
                            }
3151
                        }
3152

3153
                        $this->dbhm->preExec("UPDATE giftaid SET userid = ? WHERE id = ?;", [
1✔
3154
                            $id1,
1✔
3155
                            $best['id']
1✔
3156
                        ]);
1✔
3157
                    }
3158

3159
                    if ($rc) {
13✔
3160
                        # Log the merge - before the delete otherwise we will fail to log it.
3161
                        $l->log([
13✔
3162
                            'type' => Log::TYPE_USER,
13✔
3163
                            'subtype' => Log::SUBTYPE_MERGED,
13✔
3164
                            'user' => $id2,
13✔
3165
                            'byuser' => $me ? $me->getId() : NULL,
13✔
3166
                            'text' => "Merged $id2 into $id1 ($reason)"
13✔
3167
                        ]);
13✔
3168

3169
                        # Log under both users to make sure we can trace it.
3170
                        $l->log([
13✔
3171
                            'type' => Log::TYPE_USER,
13✔
3172
                            'subtype' => Log::SUBTYPE_MERGED,
13✔
3173
                            'user' => $id1,
13✔
3174
                            'byuser' => $me ? $me->getId() : NULL,
13✔
3175
                            'text' => "Merged $id2 into $id1 ($reason)"
13✔
3176
                        ]);
13✔
3177
                    }
3178

3179
                    if ($rc) {
13✔
3180
                        # Everything worked.
3181
                        $rollback = FALSE;
13✔
3182

3183
                        # We might have merged ourself!
3184
                        if (Utils::pres('id', $_SESSION) == $id2) {
13✔
3185
                            $_SESSION['id'] = $id1;
13✔
3186
                        }
3187
                    }
3188
                } catch (\Exception $e) {
1✔
3189
                    error_log("Merge exception " . $e->getMessage());
1✔
3190
                    $rollback = TRUE;
1✔
3191
                }
3192
            }
3193

3194
            if ($rollback) {
14✔
3195
                # Something went wrong.
3196
                #error_log("Merge failed, rollback");
3197
                $this->dbhm->rollBack();
1✔
3198
                $ret = FALSE;
1✔
3199
            } else {
3200
                #error_log("Merge worked, commit");
3201
                $ret = $this->dbhm->commit();
13✔
3202

3203
                if ($ret) {
13✔
3204
                    # Finally, delete id2.  We used to this inside the transaction, but the result was that
3205
                    # fromuser sometimes got set to NULL on messages owned by id2, despite them having been set to
3206
                    # id1 earlier on.  Either we're dumb, or there's a subtle interaction between transactions,
3207
                    # foreign keys and Percona clusters.  This is safer and proves to be more reliable.
3208
                    #
3209
                    # Make sure we don't pick up an old cached version, as we've just changed it quite a bit.
3210
                    error_log("Merged $id1 < $id2, $reason");
13✔
3211
                    $deleteme = new User($this->dbhm, $this->dbhm, $id2);
13✔
3212
                    $rc = $deleteme->delete(NULL, NULL, NULL, FALSE);
13✔
3213
                }
3214
            }
3215
        }
3216

3217
        return ($ret);
16✔
3218
    }
3219

3220
    public function mailer($user, $modmail, $toname, $to, $bcc, $fromname, $from, $subject, $text) {
3221
        try {
3222
            #error_log(session_id() . " mail " . microtime(true));
3223

3224
            list ($transport, $mailer) = Mail::getMailer();
4✔
3225

3226
            $message = \Swift_Message::newInstance()
4✔
3227
                ->setSubject($subject)
4✔
3228
                ->setFrom([$from => $fromname])
4✔
3229
                ->setTo([$to => $toname])
4✔
3230
                ->setBody($text);
4✔
3231

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

3235
            if ($user) {
3✔
3236
                $headers->addTextHeader('X-Iznik-From-User', $user->getId());
3✔
3237
            }
3238

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

3241
            if ($bcc) {
3✔
3242
                $message->setBcc(explode(',', $bcc));
1✔
3243
            }
3244

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

3247
            $this->sendIt($mailer, $message);
3✔
3248

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

3252
            #error_log(session_id() . " mailed " . microtime(true));
3253
        } catch (\Exception $e) {
4✔
3254
            # Not much we can do - shouldn't really happen given the failover transport.
3255
            // @codeCoverageIgnoreStart
3256
            error_log("Send failed with " . $e->getMessage());
3257
            // @codeCoverageIgnoreEnd
3258
        }
3259
    }
3260

3261
    private function maybeMail($groupid, $subject, $body, $action)
3262
    {
3263
        if ($body) {
4✔
3264
            # We have a mail to send.
3265
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3266
            $myid = $me->getId();
4✔
3267

3268
            $g = Group::get($this->dbhr, $this->dbhm, $groupid);
4✔
3269
            $atts = $g->getPublic();
4✔
3270

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

3273
            # Find who to send it from.  If we have a config to use for this group then it will tell us.
3274
            $name = $me->getName();
4✔
3275
            $c = new ModConfig($this->dbhr, $this->dbhm);
4✔
3276
            $cid = $c->getForGroup($me->getId(), $groupid);
4✔
3277
            $c = new ModConfig($this->dbhr, $this->dbhm, $cid);
4✔
3278
            $fromname = $c->getPrivate('fromname');
4✔
3279
            $name = ($fromname == 'Groupname Moderator') ? '$groupname Moderator' : $name;
4✔
3280

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

3284
            $bcc = $c->getBcc($action);
4✔
3285

3286
            if ($bcc) {
4✔
3287
                $bcc = str_replace('$groupname', $atts['nameshort'], $bcc);
1✔
3288
            }
3289

3290
            # We add the message into chat.
3291
            $r = new ChatRoom($this->dbhr, $this->dbhm);
4✔
3292
            $rid = $r->createUser2Mod($this->id, $groupid);
4✔
3293
            $m = NULL;
4✔
3294

3295
            $to = $this->getEmailPreferred();
4✔
3296

3297
            if ($rid) {
4✔
3298
                # Create the message.  Mark it as needing review to prevent timing window.
3299
                $m = new ChatMessage($this->dbhr, $this->dbhm);
4✔
3300
                list ($mid, $banned) = $m->create($rid,
4✔
3301
                    $myid,
4✔
3302
                    "$subject\r\n\r\n$body",
4✔
3303
                    ChatMessage::TYPE_MODMAIL,
4✔
3304
                    NULL,
4✔
3305
                    TRUE,
4✔
3306
                    NULL,
4✔
3307
                    NULL,
4✔
3308
                    NULL,
4✔
3309
                    NULL,
4✔
3310
                    NULL,
4✔
3311
                    TRUE,
4✔
3312
                    TRUE);
4✔
3313

3314
                $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✔
3315
            }
3316

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

3323
                # We've mailed the message out so they are up to date with this chat.
3324
                $r->upToDate($this->id);
3✔
3325
            }
3326

3327
            if ($m) {
4✔
3328
                # We, as a mod, have seen this message - update the roster to show that.  This avoids this message
3329
                # appearing as unread to us.
3330
                $r->updateRoster($myid, $mid);
4✔
3331

3332
                # Ensure that the other mods are present in the roster with the message seen/unseen depending on
3333
                # whether that's what we want.
3334
                $mods = $g->getMods();
4✔
3335
                foreach ($mods as $mod) {
4✔
3336
                    if ($mod != $myid) {
3✔
3337
                        if ($c->getPrivate('chatread')) {
2✔
3338
                            # We want to mark it as seen for all mods.
3339
                            $r->updateRoster($mod, $mid, ChatRoom::STATUS_AWAY);
1✔
3340
                        } else {
3341
                            # Leave it unseen, but make sure they're in the roster.
3342
                            $r->updateRoster($mod, NULL, ChatRoom::STATUS_AWAY);
1✔
3343
                        }
3344
                    }
3345
                }
3346

3347
                if ($c->getPrivate('chatread')) {
4✔
3348
                    $m->setPrivate('mailedtoall', 1);
1✔
3349
                    $m->setPrivate('seenbyall', 1);
1✔
3350
                }
3351

3352
                # Allow mailing to happen.
3353
                $m->setPrivate('reviewrequired', 0);
4✔
3354
            }
3355
        }
3356
    }
3357

3358
    public function mail($groupid, $subject, $body, $stdmsgid, $action = NULL)
3359
    {
3360
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3361

3362
        $this->log->log([
4✔
3363
            'type' => Log::TYPE_USER,
4✔
3364
            'subtype' => Log::SUBTYPE_MAILED,
4✔
3365
            'user' => $this->id,
4✔
3366
            'byuser' => $me ? $me->getId() : NULL,
4✔
3367
            'text' => $subject,
4✔
3368
            'groupid' => $groupid,
4✔
3369
            'stdmsgid' => $stdmsgid
4✔
3370
        ]);
4✔
3371

3372
        $this->maybeMail($groupid, $subject, $body, $action);
4✔
3373
    }
3374

3375
    public function happinessReviewed($happinessid) {
3376
        $this->dbhm->preExec("UPDATE messages_outcomes SET reviewed = 1 WHERE id = ?", [
1✔
3377
            $happinessid
1✔
3378
        ]);
1✔
3379
    }
3380

3381
    public function getCommentsForSingleUser($userid) {
3382
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
3383
        $rets = [
4✔
3384
            $userid => [
4✔
3385
                'id' => $userid
4✔
3386
            ]
4✔
3387
        ];
4✔
3388

3389
        $this->getComments($me, $rets);
4✔
3390

3391
        return Utils::presdef('comments', $rets[$userid], NULL);
4✔
3392
    }
3393

3394
    public function getComments($me, &$rets)
3395
    {
3396
        $userids = array_keys($rets);
197✔
3397

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

3406
            $commentuids = [];
79✔
3407
            foreach ($comments as $comment) {
79✔
3408
                if (Utils::pres('byuserid', $comment)) {
2✔
3409
                    $commentuids[] = $comment['byuserid'];
2✔
3410
                }
3411
            }
3412

3413
            $commentusers = [];
79✔
3414

3415
            if ($commentuids && count($commentuids)) {
79✔
3416
                $commentusers = $this->getPublicsById($commentuids, NULL, FALSE, FALSE, FALSE, FALSE);
2✔
3417

3418
                foreach ($commentusers as &$commentuser) {
2✔
3419
                    $commentuser['settings'] = NULL;
2✔
3420
                }
3421
            }
3422

3423
            foreach ($rets as $retind => $ret) {
79✔
3424
                $rets[$retind]['comments'] = [];
79✔
3425

3426
                for ($commentind = 0; $commentind < count($comments); $commentind++) {
79✔
3427
                    if ($comments[$commentind]['userid'] == $rets[$retind]['id']) {
2✔
3428
                        $comments[$commentind]['date'] = Utils::ISODate($comments[$commentind]['date']);
2✔
3429
                        $comments[$commentind]['reviewed'] = Utils::ISODate($comments[$commentind]['reviewed']);
2✔
3430

3431
                        if (Utils::pres('byuserid', $comments[$commentind])) {
2✔
3432
                            $comments[$commentind]['byuser'] = $commentusers[$comments[$commentind]['byuserid']];
2✔
3433
                        }
3434

3435
                        $rets[$retind]['comments'][] = $comments[$commentind];
2✔
3436
                    }
3437
                }
3438
            }
3439
        }
3440
    }
3441

3442
    public function listComments(&$ctx, $groupid = NULL) {
3443
        $comments = [];
1✔
3444
        $ctxq = '';
1✔
3445

3446
        if ($ctx && Utils::pres('reviewed', $ctx)) {
1✔
3447
            $ctxq = "users_comments.reviewed < " . $this->dbhr->quote($ctx['reviewed']) . " AND ";
1✔
3448
        }
3449

3450
        $groupq = $groupid ? " groupid = $groupid AND " : '';
1✔
3451

3452
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
3453
        $groupids = $me ? $me->getModeratorships() : [];
1✔
3454

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

3460
            $uids = array_unique(array_merge(array_column($comments, 'byuserid'), array_column($comments, 'userid')));
1✔
3461
            $u = new User($this->dbhr, $this->dbhm);
1✔
3462
            $users = $u->getPublicsById($uids, NULL, FALSE, FALSE, FALSE, FALSE);
1✔
3463

3464
            foreach ($comments as &$comment) {
1✔
3465
                $comment['date'] = Utils::ISODate($comment['date']);
1✔
3466
                $comment['reviewed'] = Utils::ISODate($comment['reviewed']);
1✔
3467

3468
                if (Utils::pres('userid', $comment)) {
1✔
3469
                    $comment['user'] = $users[$comment['userid']];
1✔
3470
                    unset($comment['userid']);
1✔
3471
                }
3472

3473
                if (Utils::pres('byuserid', $comment)) {
1✔
3474
                    $comment['byuser'] = $users[$comment['byuserid']];
1✔
3475
                    unset($comment['byuserid']);
1✔
3476
                }
3477

3478
                $ctx['reviewed'] = $comment['reviewed'];
1✔
3479
            }
3480
        }
3481

3482
        return $comments;
1✔
3483
    }
3484

3485
    public function getComment($id)
3486
    {
3487
        # We can only see comments on groups on which we have mod status.
3488
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3489
        $groupids = $me ? $me->getModeratorships() : [];
2✔
3490
        $groupids = count($groupids) == 0 ? [0] : $groupids;
2✔
3491

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

3495
        foreach ($comments as &$comment) {
2✔
3496
            $comment['date'] = Utils::ISODate($comment['date']);
2✔
3497
            $comment['reviewed'] = Utils::ISODate($comment['reviewed']);
2✔
3498

3499
            if (Utils::pres('byuserid', $comment)) {
2✔
3500
                $u = User::get($this->dbhr, $this->dbhm, $comment['byuserid']);
2✔
3501
                $comment['byuser'] = $u->getPublic();
2✔
3502
            }
3503

3504
            return ($comment);
2✔
3505
        }
3506

3507
        return (NULL);
1✔
3508
    }
3509

3510
    public function addComment($groupid, $user1 = NULL, $user2 = NULL, $user3 = NULL, $user4 = NULL, $user5 = NULL,
3511
                               $user6 = NULL, $user7 = NULL, $user8 = NULL, $user9 = NULL, $user10 = NULL,
3512
                               $user11 = NULL, $byuserid = NULL, $checkperms = TRUE, $flag = FALSE)
3513
    {
3514
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
7✔
3515

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

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

3524
        foreach ($groups as $modgroupid) {
7✔
3525
            if ($groupid == $modgroupid) {
7✔
3526
                $sql = "INSERT INTO users_comments (userid, groupid, byuserid, user1, user2, user3, user4, user5, user6, user7, user8, user9, user10, user11, flag) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
6✔
3527
                $this->dbhm->preExec($sql, [
6✔
3528
                    $this->id,
6✔
3529
                    $groupid,
6✔
3530
                    $byuserid,
6✔
3531
                    $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
6✔
3532
                    $flag ? 1 : 0
6✔
3533
                ]);
6✔
3534

3535
                $rc = $this->dbhm->lastInsertId();
6✔
3536

3537
                $added = TRUE;
6✔
3538
            }
3539
        }
3540

3541
        if (!$added && $me && $me->isAdminOrSupport()) {
7✔
3542
            $sql = "INSERT INTO users_comments (userid, groupid, byuserid, user1, user2, user3, user4, user5, user6, user7, user8, user9, user10, user11, flag) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);";
1✔
3543
            $this->dbhm->preExec($sql, [
1✔
3544
                $this->id,
1✔
3545
                NULL,
1✔
3546
                $byuserid,
1✔
3547
                $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
1✔
3548
                $flag ? 1 : 0
1✔
3549
            ]);
1✔
3550

3551
            $rc = $this->dbhm->lastInsertId();
1✔
3552
        }
3553

3554
        if ($rc && $flag) {
7✔
3555
            $this->flagOthers($groupid);
1✔
3556
        }
3557

3558
        return ($rc);
7✔
3559
    }
3560

3561
    private function flagOthers($groupid) {
3562
        # We want to flag this to any other groups that the member is on.
3563
        $membs = $this->dbhr->preQuery("SELECT groupid FROM memberships WHERE userid = ? AND groupid != ?;", [
2✔
3564
            $this->id,
2✔
3565
            $groupid
2✔
3566
        ]);
2✔
3567

3568
        foreach ($membs as $memb) {
2✔
3569
            $this->memberReview($memb['groupid'], TRUE, 'Note flagged to other groups');
2✔
3570
        }
3571
    }
3572

3573
    public function editComment($id, $user1 = NULL, $user2 = NULL, $user3 = NULL, $user4 = NULL, $user5 = NULL,
3574
                                $user6 = NULL, $user7 = NULL, $user8 = NULL, $user9 = NULL, $user10 = NULL,
3575
                                $user11 = NULL, $flag = FALSE)
3576
    {
3577
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
3578

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

3582
        # Can only edit comments for a group on which we're a mod.  This code isn't that efficient but it doesn't
3583
        # happen often.
3584
        $rc = NULL;
3✔
3585
        $comments = $this->dbhr->preQuery("SELECT id, groupid FROM users_comments WHERE id = ?;", [
3✔
3586
            $id
3✔
3587
        ]);
3✔
3588

3589
        foreach ($comments as $comment) {
3✔
3590
            if ($me && ($me->isAdminOrSupport() || $me->isModOrOwner($comment['groupid']))) {
3✔
3591
                $sql = "UPDATE users_comments SET byuserid = ?, user1 = ?, user2 = ?, user3 = ?, user4 = ?, user5 = ?, user6 = ?, user7 = ?, user8 = ?, user9 = ?, user10 = ?, user11 = ?, reviewed = NOW(), flag = ? WHERE id = ?;";
3✔
3592
                $rc = $this->dbhm->preExec($sql, [
3✔
3593
                    $byuserid,
3✔
3594
                    $user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8, $user9, $user10, $user11,
3✔
3595
                    $flag,
3✔
3596
                    $comment['id']
3✔
3597
                ]);
3✔
3598

3599
                if ($rc && $flag) {
3✔
3600
                    $this->flagOthers($comment['groupid']);
1✔
3601
                }
3602
            }
3603
        }
3604

3605
        return ($rc);
3✔
3606
    }
3607

3608
    public function deleteComment($id)
3609
    {
3610
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3611

3612
        # Can only delete comments for a group on which we're a mod.
3613
        $rc = FALSE;
2✔
3614

3615
        $comments = $this->dbhr->preQuery("SELECT id, groupid FROM users_comments WHERE id = ?;", [
2✔
3616
            $id
2✔
3617
        ]);
2✔
3618

3619
        foreach ($comments as $comment) {
2✔
3620
            if ($me && ($me->isAdminOrSupport() || $me->isModOrOwner($comment['groupid']))) {
2✔
3621
                $rc = $this->dbhm->preExec("DELETE FROM users_comments WHERE id = ?;", [$id]);
2✔
3622
            }
3623
        }
3624

3625
        return ($rc);
2✔
3626
    }
3627

3628
    public function deleteComments()
3629
    {
3630
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
3631

3632
        # Can only delete comments for a group on which we're a mod.
3633
        $rc = FALSE;
1✔
3634
        $groups = $me ? $me->getModeratorships() : [];
1✔
3635
        foreach ($groups as $modgroupid) {
1✔
3636
            $rc = $this->dbhm->preExec("DELETE FROM users_comments WHERE userid = ? AND groupid = ?;", [$this->id, $modgroupid]);
1✔
3637
        }
3638

3639
        return ($rc);
1✔
3640
    }
3641

3642
    public function split($email, $name = NULL)
3643
    {
3644
        # We want to ensure that the current user has no reference to these values.
3645
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3646
        $l = new Log($this->dbhr, $this->dbhm);
2✔
3647
        if ($email) {
2✔
3648
            $this->removeEmail($email);
2✔
3649
        }
3650

3651
        $l->log([
2✔
3652
            'type' => Log::TYPE_USER,
2✔
3653
            'subtype' => Log::SUBTYPE_SPLIT,
2✔
3654
            'user' => $this->id,
2✔
3655
            'byuser' => $me ? $me->getId() : NULL,
2✔
3656
            'text' => "Split out $email"
2✔
3657
        ]);
2✔
3658

3659
        $u = new User($this->dbhr, $this->dbhm);
2✔
3660
        $uid2 = $u->create(NULL, NULL, $name);
2✔
3661
        $u->addEmail($email);
2✔
3662

3663
        # We might be able to move some messages over.
3664
        $this->dbhm->preExec("UPDATE messages SET fromuser = ? WHERE fromaddr = ?;", [
2✔
3665
            $uid2,
2✔
3666
            $email
2✔
3667
        ]);
2✔
3668
        $this->dbhm->preExec("UPDATE messages_history SET fromuser = ? WHERE fromaddr = ?;", [
2✔
3669
            $uid2,
2✔
3670
            $email
2✔
3671
        ]);
2✔
3672

3673
        # Chats which reference the messages sent from that email must also be intended for the split user.
3674
        $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✔
3675
            $email
2✔
3676
        ]);
2✔
3677

3678
        foreach ($chats as $chat) {
2✔
3679
            if ($chat['user1'] == $this->id) {
1✔
3680
                $this->dbhm->preExec("UPDATE chat_rooms SET user1 = ? WHERE id = ?;", [
1✔
3681
                    $uid2,
1✔
3682
                    $chat['id']
1✔
3683
                ]);
1✔
3684
            }
3685

3686
            if ($chat['user2'] == $this->id) {
1✔
3687
                $this->dbhm->preExec("UPDATE chat_rooms SET user2 = ? WHERE id = ?;", [
1✔
3688
                    $uid2,
1✔
3689
                    $chat['id']
1✔
3690
                ]);
1✔
3691
            }
3692
        }
3693

3694
        # We might have a name.
3695
        $this->dbhm->preExec("UPDATE users SET fullname = (SELECT fromname FROM messages WHERE fromaddr = ? LIMIT 1) WHERE id = ?;", [
2✔
3696
            $email,
2✔
3697
            $uid2
2✔
3698
        ]);
2✔
3699

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

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

3708
        return ($uid2);
2✔
3709
    }
3710

3711
    public function welcome($email, $password)
3712
    {
3713
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
6✔
3714
        $twig = new \Twig_Environment($loader);
6✔
3715

3716
        $html = $twig->render('welcome/welcome.html', [
6✔
3717
            'email' => $email,
6✔
3718
            'password' => $password
6✔
3719
        ]);
6✔
3720

3721
        $message = \Swift_Message::newInstance()
6✔
3722
            ->setSubject("Welcome to " . SITE_NAME . "!")
6✔
3723
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
6✔
3724
            ->setTo($email)
6✔
3725
            ->setBody("Thanks for joining" . SITE_NAME . "!" . ($password ? "  Here's your password: $password." : ''));
6✔
3726

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

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

3738
        list ($transport, $mailer) = Mail::getMailer();
6✔
3739
        $this->sendIt($mailer, $message);
6✔
3740
    }
3741

3742
    public function FBL()
3743
    {
3744
        $settings = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FBL, TRUE);
1✔
3745
        $unsubscribe = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FBL, TRUE);
1✔
3746

3747
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
1✔
3748
        $twig = new \Twig_Environment($loader);
1✔
3749

3750
        $html = $twig->render('fbl.html', [
1✔
3751
            'email' => $this->getEmailPreferred(),
1✔
3752
            'unsubscribe' => $unsubscribe,
1✔
3753
            'settings' => $settings
1✔
3754
        ]);
1✔
3755

3756
        $message = \Swift_Message::newInstance()
1✔
3757
            ->setSubject("We've turned off emails for you")
1✔
3758
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
1✔
3759
            ->setTo($this->getEmailPreferred())
1✔
3760
//            ->setBcc('log@ehibbert.org.uk')
1✔
3761
            ->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✔
3762

3763
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3764
        # Outlook.
3765
        $htmlPart = \Swift_MimePart::newInstance();
1✔
3766
        $htmlPart->setCharset('utf-8');
1✔
3767
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
3768
        $htmlPart->setContentType('text/html');
1✔
3769
        $htmlPart->setBody($html);
1✔
3770
        $message->attach($htmlPart);
1✔
3771

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

3774
        list ($transport, $mailer) = Mail::getMailer();
1✔
3775
        $this->sendIt($mailer, $message);
1✔
3776
    }
3777

3778
    public function forgotPassword($email)
3779
    {
3780
        $link = $this->loginLink(USER_SITE, $this->id, '/settings', User::SRC_FORGOT_PASSWORD, TRUE);
1✔
3781

3782
        $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/welcome');
1✔
3783
        $twig = new \Twig_Environment($loader);
1✔
3784

3785
        $html = $twig->render('forgotpassword.html', [
1✔
3786
            'email' => $this->getEmailPreferred(),
1✔
3787
            'url' => $link,
1✔
3788
        ]);
1✔
3789

3790
        $message = \Swift_Message::newInstance()
1✔
3791
            ->setSubject("Forgot your password?")
1✔
3792
            ->setFrom([NOREPLY_ADDR => SITE_NAME])
1✔
3793
            ->setTo($email)
1✔
3794
            ->setBody("To set a new password, just log in here: $link");
1✔
3795

3796
        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3797
        # Outlook.
3798
        $htmlPart = \Swift_MimePart::newInstance();
1✔
3799
        $htmlPart->setCharset('utf-8');
1✔
3800
        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
1✔
3801
        $htmlPart->setContentType('text/html');
1✔
3802
        $htmlPart->setBody($html);
1✔
3803
        $message->attach($htmlPart);
1✔
3804

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

3807
        list ($transport, $mailer) = Mail::getMailer();
1✔
3808
        $this->sendIt($mailer, $message);
1✔
3809
    }
3810

3811
    public function verifyEmail($email, $force = false)
3812
    {
3813
        # If this is one of our current emails, then we can just make it the primary.
3814
        $emails = $this->getEmails();
6✔
3815
        $handled = FALSE;
6✔
3816

3817
        if (!$force) {
6✔
3818
            foreach ($emails as $anemail) {
6✔
3819
                if (User::canonMail($anemail['email']) == User::canonMail($email)) {
6✔
3820
                    # It's one of ours already; make sure it's flagged as primary.
3821
                    $this->addEmail($email, 1);
3✔
3822
                    $handled = TRUE;
3✔
3823
                }
3824
            }
3825
        }
3826

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

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

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

3842
            if (!$key) {
5✔
3843
                do {
3844
                    # Loop in case of clash on the key we happen to invent.
3845
                    $key = uniqid();
5✔
3846
                    $sql = "INSERT INTO users_emails (email, canon, validatekey, backwards) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE validatekey = ?;";
5✔
3847
                    $this->dbhm->preExec($sql,
5✔
3848
                                         [$email, $canon, $key, strrev($canon), $key]);
5✔
3849
                } while (!$this->dbhm->rowsAffected());
5✔
3850
            }
3851

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

3854
            list ($transport, $mailer) = Mail::getMailer();
5✔
3855
            $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
5✔
3856
            $twig = new \Twig_Environment($loader);
5✔
3857

3858
            $html = $twig->render('verifymail.html', [
5✔
3859
                'email' => $email,
5✔
3860
                'confirm' => $confirm
5✔
3861
            ]);
5✔
3862

3863
            $message = \Swift_Message::newInstance()
5✔
3864
                ->setSubject("Please verify your email")
5✔
3865
                ->setFrom([NOREPLY_ADDR => SITE_NAME])
5✔
3866
                ->setReturnPath($this->getBounce())
5✔
3867
                ->setTo([$email => $this->getName()])
5✔
3868
                ->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✔
3869

3870
            # Add HTML in base-64 as default quoted-printable encoding leads to problems on
3871
            # Outlook.
3872
            $htmlPart = \Swift_MimePart::newInstance();
5✔
3873
            $htmlPart->setCharset('utf-8');
5✔
3874
            $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
5✔
3875
            $htmlPart->setContentType('text/html');
5✔
3876
            $htmlPart->setBody($html);
5✔
3877
            $message->attach($htmlPart);
5✔
3878

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

3881
            $this->sendIt($mailer, $message);
5✔
3882
        }
3883

3884
        return ($handled);
6✔
3885
    }
3886

3887
    public function confirmEmail($key)
3888
    {
3889
        $rc = FALSE;
2✔
3890
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
2✔
3891

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

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

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

3905
            $rc = $this->id;
2✔
3906
        }
3907

3908
        return ($rc);
2✔
3909
    }
3910

3911
    public function confirmUnsubscribe()
3912
    {
3913
        list ($transport, $mailer) = Mail::getMailer();
2✔
3914

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

3917
        $message = \Swift_Message::newInstance()
2✔
3918
            ->setSubject("Please confirm you want to leave Freegle")
2✔
3919
            ->setFrom(NOREPLY_ADDR)
2✔
3920
            ->setReplyTo(SUPPORT_ADDR)
2✔
3921
            ->setTo($this->getEmailPreferred())
2✔
3922
            ->setDate(time())
2✔
3923
            ->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✔
3924

3925
        Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::UNSUBSCRIBE);
2✔
3926
        $this->sendIt($mailer, $message);
2✔
3927
    }
3928

3929
    public function inventEmail($force = FALSE)
3930
    {
3931
        # An invented email is one on our domain that doesn't give away too much detail, but isn't just a string of
3932
        # numbers (ideally).  We may already have one.
3933
        $email = NULL;
45✔
3934

3935
        if (!$force) {
45✔
3936
            # We want the most recent of our own emails.
3937
            $emails = $this->getEmails(TRUE);
45✔
3938
            foreach ($emails as $thisemail) {
45✔
3939
                if (strpos($thisemail['email'], USER_DOMAIN) !== FALSE) {
28✔
3940
                    $email = $thisemail['email'];
15✔
3941
                    break;
15✔
3942
                }
3943
            }
3944
        }
3945

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

3951
            if (!$force && strlen(str_replace(' ', '', $yahooid)) && strpos($yahooid, '@') === FALSE && strlen($yahooid) <= 16) {
31✔
3952
                $email = str_replace(' ', '', $yahooid) . '-' . $this->id . '@' . USER_DOMAIN;
1✔
3953
            } else {
3954
                # Their own email might already be of that nature, which would be lovely.
3955
                if (!$force) {
31✔
3956
                    $email = $this->getEmailPreferred();
31✔
3957

3958
                    if ($email) {
31✔
3959
                        foreach (['firstname', 'lastname', 'fullname'] as $att) {
14✔
3960
                            $words = explode(' ', $this->user[$att]);
14✔
3961
                            foreach ($words as $word) {
14✔
3962
                                if (strlen($word) && stripos($email, $word) !== FALSE) {
14✔
3963
                                    # Unfortunately not - it has some personal info in it.
3964
                                    $email = NULL;
14✔
3965
                                }
3966
                            }
3967
                        }
3968

3969
                        if (stripos($email, '%') !== FALSE) {
14✔
3970
                            # This may indicate a case where the real email is encoded on the LHS, eg gtempaccount.com
3971
                            $email = NULL;
1✔
3972
                        }
3973
                    }
3974
                }
3975

3976
                if ($email) {
31✔
3977
                    # We have an email which is fairly anonymous.  Use the LHS.
3978
                    $p = strpos($email, '@');
2✔
3979
                    $email = str_replace(' ', '', $p > 0 ? substr($email, 0, $p) : $email) . '-' . $this->id . '@' . USER_DOMAIN;
2✔
3980
                } else {
3981
                    # We can't make up something similar to their existing email address so invent from scratch.
3982
                    $lengths = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/distinct_word_lengths.json'), true);
31✔
3983
                    $bigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/word_start_bigrams.json'), true);
31✔
3984
                    $trigrams = json_decode(file_get_contents(IZNIK_BASE . '/lib/wordle/data/trigrams.json'), true);
31✔
3985

3986
                    do {
3987
                        $length = \Wordle\array_weighted_rand($lengths);
31✔
3988
                        $start = \Wordle\array_weighted_rand($bigrams);
31✔
3989
                        $email = strtolower(\Wordle\fill_word($start, $length, $trigrams)) . '-' . $this->id . '@' . USER_DOMAIN;
31✔
3990

3991
                        # We might just happen to have invented an email with their personal information in it.  This
3992
                        # actually happened in the UT with "test".
3993
                        foreach (['firstname', 'lastname', 'fullname'] as $att) {
31✔
3994
                            $words = explode(' ', $this->user[$att]);
31✔
3995
                            foreach ($words as $word) {
31✔
3996
                                $word = trim($word);
31✔
3997
                                if (strlen($word)) {
31✔
3998
                                    $p = stripos($email, $word);
26✔
3999
                                    $q = strpos($email, '-');
26✔
4000

4001
                                    if ($word !== '-') {
26✔
4002
                                        # Dash is always present, which is fine.
4003
                                        $email = ($p !== FALSE && $p < $q) ? NULL : $email;
26✔
4004
                                    }
4005
                                }
4006
                            }
4007
                        }
4008
                    } while (!$email);
31✔
4009
                }
4010
            }
4011
        }
4012

4013
        return ($email);
45✔
4014
    }
4015

4016
    public function delete($groupid = NULL, $subject = NULL, $body = NULL, $log = TRUE)
4017
    {
4018
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
18✔
4019

4020
        # Delete memberships.  This will remove any Yahoo memberships.
4021
        $membs = $this->getMemberships();
18✔
4022
        #error_log("Members in delete " . var_export($membs, TRUE));
4023
        foreach ($membs as $memb) {
18✔
4024
            $this->removeMembership($memb['id']);
8✔
4025
        }
4026

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

4029
        if ($rc && $log) {
18✔
4030
            $this->log->log([
5✔
4031
                'type' => Log::TYPE_USER,
5✔
4032
                'subtype' => Log::SUBTYPE_DELETED,
5✔
4033
                'user' => $this->id,
5✔
4034
                'byuser' => $me ? $me->getId() : NULL,
5✔
4035
                'text' => $this->getName()
5✔
4036
            ]);
5✔
4037
        }
4038

4039
        return ($rc);
18✔
4040
    }
4041

4042
    public function getUnsubLink($domain, $id, $type = NULL, $auto = FALSE)
4043
    {
4044
        return (User::loginLink($domain, $id, "/unsubscribe/$id", $type, $auto));
24✔
4045
    }
4046

4047
    public function listUnsubscribe($id, $type = NULL) {
4048
        # These are links which will completely unsubscribe the user.  This is necessary because of Yahoo and Gmail
4049
        # changes in 2024, and also useful for CAN-SPAM.  We want them to involve the key to prevent spoof unsubscribes.
4050
        #
4051
        # We only include the web link, because this providers a better user experience - we can tell them
4052
        # things afterwards.  This is valid - RFC8058 the RFC says you MUST include an HTTPS link, and you MAY
4053
        # include others.
4054
        $key = $id ? $this->getUserKey($id) : '1234';
121✔
4055
        $key = $key ? $key : '1234';
121✔
4056
        #$ret = "<mailto:unsubscribe-$id-$key-$type@" . USER_DOMAIN . "?subject=unsubscribe>, <https://" . USER_SITE . "/one-click-unsubscribe/$id/$key>";
4057
        $ret = "<https://" . USER_SITE . "/one-click-unsubscribe/$id/$key>";
121✔
4058
        #$ret = "<http://localhost:3002/one-click-unsubscribe/$id/$key>";
4059
        return $ret;
121✔
4060
    }
4061

4062
    public function loginLink($domain, $id, $url = '/', $type = NULL, $auto = FALSE)
4063
    {
4064
        $p = strpos($url, '?');
56✔
4065
        $ret = $p === FALSE ? "https://$domain$url?u=$id&src=$type" : "https://$domain$url&u=$id&src=$type";
56✔
4066

4067
        if ($auto) {
56✔
4068
            # Get a per-user link we can use to log in without a password.
4069
            $key = $this->getUserKey($id);
11✔
4070

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

4074
            $p = strpos($url, '?');
11✔
4075
            $src = $type ? "&src=$type" : "";
11✔
4076
            $ret = $p === FALSE ? ("https://$domain$url?u=$id&k=$key$src") : ("https://$domain$url&u=$id&k=$key$src");
11✔
4077
        }
4078

4079
        return ($ret);
56✔
4080
    }
4081

4082
    public function sendOurMails($g = NULL, $checkholiday = TRUE, $checkbouncing = TRUE)
4083
    {
4084
        if ($this->getPrivate('deleted')) {
103✔
4085
            return FALSE;
1✔
4086
        }
4087

4088
        # We don't want to send emails to people who haven't been active for more than six months.  This improves
4089
        # our spam reputation, by avoiding honeytraps.
4090
        $sendit = FALSE;
103✔
4091
        $lastaccess = strtotime($this->getPrivate('lastaccess'));
103✔
4092

4093
        // This time is also present on the client in ModMember, and in Engage.
4094
        if (time() - $lastaccess <= Engage::USER_INACTIVE) {
103✔
4095
            $sendit = TRUE;
103✔
4096

4097
            if ($sendit && $checkholiday) {
103✔
4098
                # We might be on holiday.
4099
                $hol = $this->getPrivate('onholidaytill');
22✔
4100
                $till = $hol ? strtotime($hol) : 0;
22✔
4101
                #error_log("Holiday $till vs " . time());
4102

4103
                $sendit = time() > $till;
22✔
4104
            }
4105

4106
            if ($sendit && $checkbouncing) {
103✔
4107
                # And don't send if we're bouncing.
4108
                $sendit = !$this->getPrivate('bouncing');
22✔
4109
                #error_log("After bouncing $sendit");
4110
            }
4111
        }
4112

4113
        #error_log("Sendit? $sendit");
4114
        return ($sendit);
103✔
4115
    }
4116

4117
    public function getMembershipHistory()
4118
    {
4119
        # We get this from our logs.
4120
        $sql = "SELECT * FROM logs WHERE user = ? AND `type` = ? ORDER BY id DESC;";
5✔
4121
        $logs = $this->dbhr->preQuery($sql, [$this->id, Log::TYPE_GROUP]);
5✔
4122

4123
        $ret = [];
5✔
4124
        foreach ($logs as $log) {
5✔
4125
            $thisone = NULL;
3✔
4126
            switch ($log['subtype']) {
3✔
4127
                case Log::SUBTYPE_JOINED:
3✔
4128
                case Log::SUBTYPE_APPROVED:
1✔
4129
                case Log::SUBTYPE_REJECTED:
1✔
4130
                case Log::SUBTYPE_APPLIED:
1✔
4131
                case Log::SUBTYPE_LEFT:
1✔
4132
                    {
3✔
4133
                        $thisone = $log['subtype'];
3✔
4134
                        break;
3✔
4135
                    }
3✔
4136
            }
4137

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

4142
                if ($g->getId() ==  $log['groupid']) {
3✔
4143
                    $ret[] = [
3✔
4144
                        'timestamp' => Utils::ISODate($log['timestamp']),
3✔
4145
                        'type' => $thisone,
3✔
4146
                        'group' => [
3✔
4147
                            'id' => $log['groupid'],
3✔
4148
                            'nameshort' => $g->getPrivate('nameshort'),
3✔
4149
                            'namedisplay' => $g->getName()
3✔
4150
                        ],
3✔
4151
                        'text' => $log['text']
3✔
4152
                    ];
3✔
4153
                }
4154
            }
4155
        }
4156

4157
        return ($ret);
5✔
4158
    }
4159

4160
    public function search($search, $ctx)
4161
    {
4162
        if (preg_replace('/\-|\~/', '', $search) ==  '') {
4✔
4163
            # Most likely an encoded id.
4164
            $search = User::decodeId($search);
×
4165
        }
4166

4167
        if (preg_match('/story-(.*)/', $search, $matches)) {
4✔
4168
            # Story.
4169
            $s = new Story($this->dbhr, $this->dbhm, $matches[1]);
×
4170
            $search = $s->getPrivate('userid');
×
4171
        }
4172

4173
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
4✔
4174
        $id = intval(Utils::presdef('id', $ctx, 0));
4✔
4175
        $ctx = $ctx ? $ctx : [];
4✔
4176
        $q = $this->dbhr->quote("$search%");
4✔
4177
        $backwards = strrev($search);
4✔
4178
        $qb = $this->dbhr->quote("$backwards%");
4✔
4179

4180
        $canon = $this->dbhr->quote(User::canonMail($search) . "%");
4✔
4181

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

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

4189
        $sql = "SELECT DISTINCT userid FROM
4✔
4190
                ((SELECT userid FROM users_emails WHERE canon LIKE $canon OR backwards LIKE $qb) UNION
4✔
4191
                (SELECT userid FROM users_emails WHERE canon LIKE $canon2) UNION
4✔
4192
                (SELECT id AS userid FROM users WHERE fullname LIKE $q) UNION
4✔
4193
                (SELECT id AS userid FROM users WHERE yahooid LIKE $q) UNION
4✔
4194
                (SELECT id AS userid FROM users WHERE id = ?) UNION
4195
                (SELECT userid FROM users_logins WHERE uid LIKE $q)) t WHERE userid > ? ORDER BY userid ASC";
4✔
4196
        $users = $this->dbhr->preQuery($sql, [$search, $id]);
4✔
4197

4198
        $ret = [];
4✔
4199

4200
        foreach ($users as $user) {
4✔
4201
            $ctx['id'] = $user['userid'];
4✔
4202

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

4205
            $thisone = $u->getPublic(NULL, TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, [
4✔
4206
                MessageCollection::PENDING,
4✔
4207
                MessageCollection::APPROVED
4✔
4208
            ], TRUE);
4✔
4209

4210
            # We might not have the emails.
4211
            $thisone['email'] = $u->getEmailPreferred();
4✔
4212
            $thisone['emails'] = $u->getEmails();
4✔
4213

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

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

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

4223
            # Also return the chats for this user.  Can't use ChatRooms::listForUser because that would exclude any
4224
            # chats on groups where we were no longer a member.
4225
            $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✔
4226
                $user['userid'],
4✔
4227
                ChatRoom::TYPE_USER2USER,
4✔
4228
                $user['userid'],
4✔
4229
            ]), 'id'));
4✔
4230

4231
            $thisone['chatrooms'] = [];
4✔
4232

4233
            if ($rooms) {
4✔
4234
                $r = new ChatRoom($this->dbhr, $this->dbhm);
×
4235
                $thisone['chatrooms'] = $r->fetchRooms($rooms, $user['userid'], FALSE);
×
4236
            }
4237

4238
            # Add the public location and best guess lat/lng
4239
            $thisone['info']['publiclocation'] = $u->getPublicLocation();
4✔
4240
            $latlng = $u->getLatLng(FALSE, TRUE);
4✔
4241
            $thisone['privateposition'] = [
4✔
4242
                'lat' => $latlng[0],
4✔
4243
                'lng' => $latlng[1],
4✔
4244
                'name' => $latlng[2]
4✔
4245
            ];
4✔
4246

4247
            $thisone['comments'] = $this->getCommentsForSingleUser($user['userid']);
4✔
4248
            $thisone['tnuserid'] = $u->getPrivate('tnuserid');
4✔
4249

4250
            $push = $this->dbhr->preQuery("SELECT MAX(lastsent) AS lastpush FROM users_push_notifications WHERE userid = ?;", [
4✔
4251
                $user['userid']
4✔
4252
            ]);
4✔
4253

4254
            foreach ($push as $p) {
4✔
4255
                $thisone['lastpush'] = Utils::ISODate($p['lastpush']);
4✔
4256
            }
4257

4258
            $thisone['info'] = $u->getInfo();
4✔
4259
            $thisone['trustlevel'] = $u->getPrivate('trustlevel');
4✔
4260

4261
            $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE userid = ?;", [
4✔
4262
                $u->getId()
4✔
4263
            ]);
4✔
4264

4265
            $thisone['bans'] = [];
4✔
4266

4267
            foreach ($bans as $ban) {
4✔
4268
                $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
4269
                $banner = User::get($this->dbhr, $this->dbhm, $ban['byuser']);
1✔
4270
                $thisone['bans'][] = [
1✔
4271
                    'date' => Utils::ISODate($ban['date']),
1✔
4272
                    'group' => $g->getName(),
1✔
4273
                    'byemail' => $banner->getEmailPreferred(),
1✔
4274
                    'byuserid' => $ban['byuser']
1✔
4275
                ];
1✔
4276
            }
4277

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

4281
            if ($me->hasPermission(User::PERM_GIFTAID)) {
4✔
4282
                $thisone['donations'] = $d->listByUser($user['userid']);
2✔
4283
            }
4284

4285
            $thisone['newsfeedmodstatus'] = $u->getPrivate('newsfeedmodstatus');
4✔
4286
            $thisone['newsfeed'] = $this->dbhr->preQuery("SELECT id, message, timestamp, hidden, hiddenby, deleted, deletedby FROM newsfeed WHERE userid = ? ORDER BY id DESC;", [
4✔
4287
                $user['userid']
4✔
4288
            ]);
4✔
4289

4290
            foreach ($thisone['newsfeed'] as &$nf) {
4✔
4291
                $nf['timestamp'] = Utils::ISODate($nf['timestamp']);
×
4292
                $nf['deleted'] = Utils::ISODate($nf['deleted']);
×
4293
                $nf['hidden'] = Utils::ISODate($nf['hidden']);
×
4294
            }
4295

4296
            $ret[] = $thisone;
4✔
4297
        }
4298

4299
        return ($ret);
4✔
4300
    }
4301

4302
    private function safeGetPostcode($val) {
4303
        $ret = [ NULL, NULL ];
56✔
4304

4305
        $settings = json_decode($val, TRUE);
56✔
4306

4307
        if (Utils::pres('mylocation', $settings) &&
56✔
4308
            Utils::presdef('type', $settings['mylocation'], NULL) == 'Postcode') {
56✔
4309
            $ret = [
14✔
4310
                Utils::presdef('id', $settings['mylocation'], NULL),
14✔
4311
                Utils::presdef('name', $settings['mylocation'], NULL)
14✔
4312
            ];
14✔
4313
        }
4314

4315
        return $ret;
56✔
4316
    }
4317

4318
    public function setPrivate($att, $val)
4319
    {
4320
        if (!strcmp($att, 'settings') && $val) {
180✔
4321
            # Possible location change.
4322
            list ($oldid, $oldloc) = $this->safeGetPostcode($this->getPrivate('settings'));
56✔
4323
            list ($newid, $newloc) = $this->safeGetPostcode($val);
56✔
4324

4325
            if ($oldloc !== $newloc) {
56✔
4326
                # We have changed our location.
4327
                parent::setPrivate('lastlocation', $newid);
14✔
4328
                $i = new Isochrone($this->dbhr, $this->dbhm);
14✔
4329
                $i->deleteForUser($this->id);
14✔
4330

4331
                $this->log->log([
14✔
4332
                            'type' => Log::TYPE_USER,
14✔
4333
                            'subtype' => Log::SUBTYPE_POSTCODECHANGE,
14✔
4334
                            'user' => $this->id,
14✔
4335
                            'text' => $newloc
14✔
4336
                        ]);
14✔
4337
            }
4338

4339
            // Prune the info in the settings to remove any groupsnear info, which would use space and is not needed.
4340
            $val = User::pruneSettings($val);
56✔
4341
        }
4342

4343
        User::clearCache($this->id);
180✔
4344
        parent::setPrivate($att, $val);
180✔
4345
    }
4346

4347
    public static function pruneSettings($val) {
4348
        // Prune info from what we store in the user table to keep it smaller.
4349
        if (strpos($val, 'groupsnear') !== FALSE) {
56✔
4350
            $decoded = json_decode($val, TRUE);
×
4351
            if (Utils::pres('mylocation', $decoded) && Utils::pres('groupsnear', $decoded['mylocation'])) {
×
4352
                unset($decoded['mylocation']['groupsnear']);
×
4353
                $val = json_encode($decoded);
×
4354
            }
4355
        }
4356

4357
        return $val;
56✔
4358
    }
4359

4360
    public function canMerge()
4361
    {
4362
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
16✔
4363
        return (array_key_exists('canmerge', $settings) ? $settings['canmerge'] : TRUE);
16✔
4364
    }
4365

4366
    public function notifsOn($type, $groupid = NULL)
4367
    {
4368
        if ($this->getPrivate('deleted')) {
574✔
4369
            return FALSE;
1✔
4370
        }
4371

4372
        $settings = Utils::pres('settings', $this->user) ? json_decode($this->user['settings'], TRUE) : [];
574✔
4373
        $notifs = Utils::pres('notifications', $settings);
574✔
4374

4375
        $defs = [
574✔
4376
            self::NOTIFS_EMAIL => TRUE,
574✔
4377
            self::NOTIFS_EMAIL_MINE => FALSE,
574✔
4378
            self::NOTIFS_PUSH => TRUE,
574✔
4379
            self::NOTIFS_APP => TRUE
574✔
4380
        ];
574✔
4381

4382
        $ret = ($notifs && array_key_exists($type, $notifs)) ? $notifs[$type] : $defs[$type];
574✔
4383

4384
        if ($ret && $groupid) {
574✔
4385
            # Check we're an active mod on this group - if not then we don't want the notifications.
4386
            $ret = $this->activeModForGroup($groupid);
5✔
4387
        }
4388

4389
        #error_log("Notifs on for user #{$this->id} type $type ? $ret from " . var_export($notifs, TRUE));
4390
        return ($ret);
574✔
4391
    }
4392

4393
    public function getNotificationPayload($modtools)
4394
    {
4395
        # This gets a notification count/title/message for this user.
4396
        $notifcount = 0;
8✔
4397
        $title = '';
8✔
4398
        $message = NULL;
8✔
4399
        $chatids = [];
8✔
4400
        $route = NULL;
8✔
4401

4402
        if (!$modtools) {
8✔
4403
            # User notification.  We want to show a count of chat messages, or some of the message if there is just one.
4404
            $r = new ChatRoom($this->dbhr, $this->dbhm);
5✔
4405
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_USER2USER, ChatRoom::TYPE_USER2MOD], $modtools);
5✔
4406
            $chatcount = count($unseen);
5✔
4407
            $total = $chatcount;
5✔
4408
            foreach ($unseen as $un) {
5✔
4409
                $chatids[] = $un['chatid'];
3✔
4410
            };
4411

4412
            #error_log("Chats with unseen " . var_export($chatids, TRUE));
4413
            $n = new Notifications($this->dbhr, $this->dbhm);
5✔
4414
            $notifcount = $n->countUnseen($this->id);
5✔
4415

4416
            if ($total ==  1) {
5✔
4417
                $r = new ChatRoom($this->dbhr, $this->dbhm, $unseen[0]['chatid']);
2✔
4418
                $atts = $r->getPublic($this);
2✔
4419
                $title = $atts['name'];
2✔
4420
                list($msgs, $users) = $r->getMessages(100, 0);
2✔
4421

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

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

4429
                    $message = strlen($message) > 256 ? (substr($message, 0, 256) . "...") : $message;
2✔
4430
                }
4431

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

4434
                if ($notifcount) {
2✔
4435
                    $total += $notifcount;
2✔
4436
                }
4437
            } else if ($total > 1) {
3✔
4438
                $title = "You have $total new messages";
1✔
4439
                $route = "/chats";
1✔
4440

4441
                if ($notifcount) {
1✔
4442
                    $total += $notifcount;
1✔
4443
                    $title .= " and $notifcount notification" . ($notifcount == 1 ? '' : 's');
1✔
4444
                }
4445
            } else {
4446
                # Add in the notifications you see primarily from the newsfeed.
4447
                if ($notifcount) {
3✔
4448
                    $total += $notifcount;
3✔
4449
                    $ctx = NULL;
3✔
4450
                    $notifs = $n->get($this->id, $ctx);
3✔
4451
                    $title = $n->getNotifTitle($notifs);
3✔
4452
                    $route = '/';
3✔
4453

4454
                    if (count($notifs) > 0) {
3✔
4455
                        # For newsfeed notifications sent a route to the right place.
4456
                        switch ($notifs[0]['type']) {
3✔
4457
                            case Notifications::TYPE_COMMENT_ON_COMMENT:
3✔
4458
                            case Notifications::TYPE_COMMENT_ON_YOUR_POST:
3✔
4459
                            case Notifications::TYPE_LOVED_COMMENT:
3✔
4460
                            case Notifications::TYPE_LOVED_POST:
3✔
4461
                                $route = '/chitchat/' . $notifs[0]['newsfeedid'];
1✔
4462
                                break;
5✔
4463
                        }
4464
                    }
4465
                }
4466
            }
4467
        } else {
4468
            # ModTools notification.  We show the count of work + chats.
4469
            $r = new ChatRoom($this->dbhr, $this->dbhm);
4✔
4470
            $unseen = $r->allUnseenForUser($this->id, [ChatRoom::TYPE_MOD2MOD, ChatRoom::TYPE_USER2MOD], $modtools);
4✔
4471
            $chatcount = count($unseen);
4✔
4472

4473
            $work = $this->getWorkCounts();
4✔
4474
            $total = $work['total'] + $chatcount;
4✔
4475

4476
            // The order of these is important as the route will be the last matching.
4477
            $types = [
4✔
4478
                'pendingvolunteering' => [ 'volunteer op', 'volunteerops', '/modtools/volunteering' ],
4✔
4479
                'pendingevents' => [ 'event', 'events', '/modtools/communityevents' ],
4✔
4480
                'socialactions' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4✔
4481
                'popularposts' => [ 'publicity item', 'publicity items', '/modtools/publicity' ],
4✔
4482
                'stories' => [ 'story', 'stories', '/modtools/members/stories' ],
4✔
4483
                'newsletterstories' => [ 'newsletter story', 'newsletter stories', '/modtools/members/newsletter' ],
4✔
4484
                'chatreview' => [ 'chat message to review', 'chat messages to review', '/modtools/chats/review' ],
4✔
4485
                'pendingadmins' => [ 'admin', 'admins', '/modtools/admins' ],
4✔
4486
                'spammembers' => [ 'member to review', 'members to review', '/modtools/members/review' ],
4✔
4487
                'relatedmembers' => [ 'related member to review', 'related members to review', '/modtools/members/related' ],
4✔
4488
                'editreview' => [ 'edit', 'edits', '/modtools/messages/edits' ],
4✔
4489
                'spam' => [ 'message to review', 'messages to review', '/modtools/messages/pending' ],
4✔
4490
                'pending' => [ 'pending message', 'pending messages', '/modtools/messages/pending' ]
4✔
4491
            ];
4✔
4492

4493
            $title = $chatcount ? ("$chatcount chat message" . ($chatcount != 1 ? 's' : '') . "\n") : '';
4✔
4494
            $route = NULL;
4✔
4495

4496
            foreach ($types as $type => $vals) {
4✔
4497
                if (Utils::presdef($type, $work, 0) > 0) {
4✔
4498
                    $title .= $work[$type] . ' ' . ($work[$type] != 1 ? $vals[1] : $vals[0] ) . "\n";
1✔
4499
                    $route = $vals[2];
1✔
4500
                }
4501
            }
4502

4503
            $title = $title == '' ? NULL : $title;
4✔
4504
        }
4505

4506

4507
        return ([$total, $chatcount, $notifcount, $title, $message, array_unique($chatids), $route]);
8✔
4508
    }
4509

4510
    public function hasPermission($perm)
4511
    {
4512
        $perms = $this->user['permissions'];
41✔
4513
        return ($perms && stripos($perms, $perm) !== FALSE);
41✔
4514
    }
4515

4516
    public function sendIt($mailer, $message)
4517
    {
4518
        $mailer->send($message);
29✔
4519
    }
4520

4521
    public function thankDonation()
4522
    {
4523
        try {
4524
            $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig/donations');
1✔
4525
            $twig = new \Twig_Environment($loader);
1✔
4526
            list ($transport, $mailer) = Mail::getMailer();
1✔
4527

4528
            $message = \Swift_Message::newInstance()
1✔
4529
                ->setSubject("Thank you for supporting Freegle!")
1✔
4530
                ->setFrom(PAYPAL_THANKS_FROM)
1✔
4531
                ->setReplyTo(PAYPAL_THANKS_FROM)
1✔
4532
                ->setTo($this->getEmailPreferred())
1✔
4533
                ->setBody("Thank you for supporting Freegle!");
1✔
4534

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

4537
            $html = $twig->render('thank.html', [
1✔
4538
                'name' => $this->getName(),
1✔
4539
                'email' => $this->getEmailPreferred(),
1✔
4540
                'unsubscribe' => $this->loginLink(USER_SITE, $this->getId(), "/unsubscribe", NULL)
1✔
4541
            ]);
1✔
4542

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

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

4554
            $this->sendIt($mailer, $message);
1✔
4555
        } catch (\Exception $e) { error_log("Failed " . $e->getMessage()); };
×
4556
    }
4557

4558
    public function invite($email)
4559
    {
4560
        $ret = FALSE;
9✔
4561

4562
        # We can only invite logged in.
4563
        if ($this->id) {
9✔
4564
            # ...and only if we have spare.
4565
            if ($this->user['invitesleft'] > 0) {
9✔
4566
                # They might already be using us - but they might also have forgotten.  So allow that case.  However if
4567
                # they have actively declined a previous invitation we suppress this one.
4568
                $previous = $this->dbhr->preQuery("SELECT id FROM users_invitations WHERE email = ? AND outcome = ?;", [
9✔
4569
                    $email,
9✔
4570
                    User::INVITE_DECLINED
9✔
4571
                ]);
9✔
4572

4573
                if (count($previous) == 0) {
9✔
4574
                    # The table has a unique key on userid and email, so that means we can only invite the same person
4575
                    # once.  That avoids us pestering them.
4576
                    try {
4577
                        $this->dbhm->preExec("INSERT INTO users_invitations (userid, email) VALUES (?,?);", [
9✔
4578
                            $this->id,
9✔
4579
                            $email
9✔
4580
                        ]);
9✔
4581

4582
                        # We're ok to invite.
4583
                        $fromname = $this->getName();
9✔
4584
                        $frommail = $this->getEmailPreferred();
9✔
4585
                        $url = "https://" . USER_SITE . "/invite/" . $this->dbhm->lastInsertId();
9✔
4586

4587
                        list ($transport, $mailer) = Mail::getMailer();
9✔
4588
                        $message = \Swift_Message::newInstance()
9✔
4589
                            ->setSubject("$fromname has invited you to try Freegle!")
9✔
4590
                            ->setFrom([NOREPLY_ADDR => SITE_NAME])
9✔
4591
                            ->setReplyTo($frommail)
9✔
4592
                            ->setTo($email)
9✔
4593
                            ->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✔
4594

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

4597
                        $html = invite($fromname, $frommail, $url);
9✔
4598

4599
                        # Add HTML in base-64 as default quoted-printable encoding leads to problems on
4600
                        # Outlook.
4601
                        $htmlPart = \Swift_MimePart::newInstance();
9✔
4602
                        $htmlPart->setCharset('utf-8');
9✔
4603
                        $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
9✔
4604
                        $htmlPart->setContentType('text/html');
9✔
4605
                        $htmlPart->setBody($html);
9✔
4606
                        $message->attach($htmlPart);
9✔
4607

4608
                        $this->sendIt($mailer, $message);
9✔
4609
                        $ret = TRUE;
9✔
4610

4611
                        $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft - 1 WHERE id = ?;", [
9✔
4612
                            $this->id
9✔
4613
                        ]);
9✔
4614
                    } catch (\Exception $e) {
1✔
4615
                        # Probably a duplicate.
4616
                    }
4617
                }
4618
            }
4619
        }
4620

4621
        return ($ret);
9✔
4622
    }
4623

4624
    public function inviteOutcome($id, $outcome)
4625
    {
4626
        $invites = $this->dbhm->preQuery("SELECT * FROM users_invitations WHERE id = ?;", [
1✔
4627
            $id
1✔
4628
        ]);
1✔
4629

4630
        foreach ($invites as $invite) {
1✔
4631
            if ($invite['outcome'] == User::INVITE_PENDING) {
1✔
4632
                $this->dbhm->preExec("UPDATE users_invitations SET outcome = ?, outcometimestamp = NOW() WHERE id = ?;", [
1✔
4633
                    $outcome,
1✔
4634
                    $id
1✔
4635
                ]);
1✔
4636

4637
                if ($outcome == User::INVITE_ACCEPTED) {
1✔
4638
                    # Give the sender two more invites.  This means that if their invitations are unsuccessful, they will
4639
                    # stall, but if they do ok, they won't.  This isn't perfect - someone could fake up emails and do
4640
                    # successful invitations that way.
4641
                    $this->dbhm->preExec("UPDATE users SET invitesleft = invitesleft + 2 WHERE id = ?;", [
1✔
4642
                        $invite['userid']
1✔
4643
                    ]);
1✔
4644
                }
4645
            }
4646
        }
4647
    }
4648

4649
    public function listInvitations($since = "30 days ago")
4650
    {
4651
        $ret = [];
8✔
4652

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

4659
        foreach ($invites as $invite) {
8✔
4660
            # Check if this email is now on the platform.
4661
            $invite['date'] = Utils::ISODate($invite['date']);
7✔
4662
            $invite['outcometimestamp'] = $invite['outcometimestamp'] ? Utils::ISODate($invite['outcometimestamp']) : NULL;
7✔
4663
            $ret[] = $invite;
7✔
4664
        }
4665

4666
        return ($ret);
8✔
4667
    }
4668

4669
    public function getLatLng($usedef = TRUE, $usegroup = TRUE, $blur = Utils::BLUR_NONE)
4670
    {
4671
        $ret = [ 0, 0, NULL ];
175✔
4672

4673
        if ($this->id) {
175✔
4674
            $locs = $this->getLatLngs([ $this->user ], $usedef, $usegroup, FALSE, [ $this->user ]);
175✔
4675
            $loc = $locs[$this->id];
175✔
4676

4677
            if ($loc) {
175✔
4678
                if ($blur && ($loc['lat'] || $loc['lng'])) {
174✔
4679
                    list ($loc['lat'], $loc['lng']) = Utils::blur($loc['lat'], $loc['lng'], $blur);
4✔
4680
                }
4681

4682
                $ret = [ $loc['lat'], $loc['lng'], Utils::presdef('loc', $loc, NULL) ];
174✔
4683
            }
4684
        }
4685

4686
        return $ret;
175✔
4687
    }
4688

4689
    public function getPublicLocations(&$users, $atts = NULL)
4690
    {
4691
        $idsleft = [];
108✔
4692
        
4693
        foreach ($users as $userid => $user) {
108✔
4694
            if (!Utils::pres('info', $user) || !Utils::pres('publiclocation', $user['info'])) {
108✔
4695
                $idsleft[] = $userid;
108✔
4696
            }
4697
        }
4698
        
4699
        $areas = NULL;
108✔
4700
        $membs = NULL;
108✔
4701

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

4706
            foreach ($atts as $att) {
108✔
4707
                $loc = NULL;
108✔
4708
                $grp = NULL;
108✔
4709

4710
                $aid = NULL;
108✔
4711
                $lid = NULL;
108✔
4712
                $lat = NULL;
108✔
4713
                $lng = NULL;
108✔
4714

4715
                # Default to nowhere.
4716
                $users[$att['id']]['info']['publiclocation'] = [
108✔
4717
                    'display' => '',
108✔
4718
                    'location' => NULL,
108✔
4719
                    'groupname' => NULL
108✔
4720
                ];
108✔
4721

4722
                if (Utils::pres('settings', $att)) {
108✔
4723
                    $settings = $att['settings'];
23✔
4724
                    $settings = json_decode($settings, TRUE);
23✔
4725

4726
                    if (Utils::pres('mylocation', $settings) && Utils::pres('area', $settings['mylocation'])) {
23✔
4727
                        $loc = $settings['mylocation']['area']['name'];
7✔
4728
                        $lid = $settings['mylocation']['id'];
7✔
4729
                        $lat = $settings['mylocation']['lat'];
7✔
4730
                        $lng = $settings['mylocation']['lng'];
7✔
4731
                    }
4732
                }
4733

4734
                if (!$loc) {
108✔
4735
                    # Get the name of the last area we used.
4736
                    if (is_null($areas)) {
101✔
4737
                        $areas = $this->dbhr->preQuery("SELECT l2.id, l2.name, l2.lat, l2.lng, users.id AS userid FROM locations l1 
101✔
4738
                            INNER JOIN users ON users.lastlocation = l1.id
4739
                            INNER JOIN locations l2 ON l2.id = l1.areaid
4740
                            WHERE users.id IN (" . implode(',', $idsleft) . ");", NULL, FALSE, FALSE);
101✔
4741
                    }
4742

4743
                    foreach ($areas as $area) {
101✔
4744
                        if ($att['id'] ==  $area['userid']) {
25✔
4745
                            $loc = $area['name'];
25✔
4746
                            $lid = $area['id'];
25✔
4747
                            $lat = $area['lat'];
25✔
4748
                            $lng = $area['lng'];
25✔
4749
                        }
4750
                    }
4751
                }
4752

4753
                if (!$lid) {
108✔
4754
                    # Find the group of which we are a member which is closest to our location.  We do this because generally
4755
                    # the number of groups we're in is small and therefore this will be quick, whereas the groupsNear call is
4756
                    # fairly slow.
4757
                    $closestdist = PHP_INT_MAX;
101✔
4758
                    $closestname = NULL;
101✔
4759

4760
                    # Get all the memberships.
4761
                    if (!$membs) {
101✔
4762
                        $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✔
4763
                                ',',
101✔
4764
                                $idsleft
101✔
4765
                            ) . ") ORDER BY added ASC;";
101✔
4766
                        $membs = $this->dbhr->preQuery($sql);
101✔
4767
                    }
4768

4769
                    foreach ($membs as $memb) {
101✔
4770
                        if ($memb['userid'] == $att['id']) {
90✔
4771
                            $dist = \GreatCircle::getDistance($lat, $lng, $memb['lat'], $memb['lng']);
90✔
4772

4773
                            if ($dist < $closestdist) {
90✔
4774
                                $closestdist = $dist;
90✔
4775
                                $closestname = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
90✔
4776
                            }
4777
                        }
4778
                    }
4779

4780
                    if (!is_null($closestname)) {
101✔
4781
                        $grp = $closestname;
90✔
4782

4783
                        # The location name might be in the group name, in which case just use the group.
4784
                        $loc = stripos($grp, $loc) !== FALSE ? NULL : $loc;
90✔
4785
                    }
4786
                }
4787

4788
                if ($loc) {
108✔
4789
                    $display = $loc ? ($loc . ($grp ? ", $grp" : "")) : ($grp ? $grp : '');
32✔
4790

4791
                    $users[$att['id']]['info']['publiclocation'] = [
32✔
4792
                        'display' => $display,
32✔
4793
                        'location' => $loc,
32✔
4794
                        'groupname' => $grp
32✔
4795
                    ];
32✔
4796

4797
                    $idsleft = array_filter($idsleft, function($val) use ($att) {
32✔
4798
                        return($val != $att['id']);
32✔
4799
                    });
32✔
4800
                }
4801
            }
4802

4803
            if (count($idsleft) > 0) {
108✔
4804
                # We have some left which don't have explicit postcodes.  Try for a group name.
4805
                #
4806
                # First check the group we used most recently.
4807
                #error_log("Look for group name only for {$att['id']}");
4808
                $found = [];
101✔
4809
                foreach ($idsleft as $userid) {
101✔
4810
                    $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✔
4811
                        $userid
101✔
4812
                    ]);
101✔
4813

4814
                    foreach ($messages as $msg) {
101✔
4815
                        list ($type, $item, $location) = Message::parseSubject($msg['subject']);
62✔
4816

4817
                        if ($item) {
62✔
4818
                            $grp = $location;
42✔
4819

4820
                            // Handle some misformed locations which end up with spurious brackets.
4821
                            $grp = preg_replace('/\(|\)/', '', $grp);
42✔
4822

4823
                            $users[$userid]['info']['publiclocation'] = [
42✔
4824
                                'display' => $grp,
42✔
4825
                                'location' => NULL,
42✔
4826
                                'groupname' => $grp
42✔
4827
                            ];
42✔
4828

4829
                            $found[] = $userid;
42✔
4830
                        }
4831
                    }
4832
                }
4833

4834
                $idsleft = array_diff($idsleft, $found);
101✔
4835
                
4836
                # Now check just membership.
4837
                if (count($idsleft)) {
101✔
4838
                    if (!$membs) {
67✔
4839
                        $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✔
4840
                                ',',
22✔
4841
                                $idsleft
22✔
4842
                            ) . ") ORDER BY added ASC;";
22✔
4843
                        $membs = $this->dbhr->preQuery($sql);
22✔
4844
                    }
4845
                    
4846
                    foreach ($idsleft as $userid) {
67✔
4847
                        # Now check the group we joined most recently.
4848
                        foreach ($membs as $memb) {
67✔
4849
                            if ($memb['userid'] == $userid) {
50✔
4850
                                $grp = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
50✔
4851

4852
                                $users[$userid]['info']['publiclocation'] = [
50✔
4853
                                    'display' => $grp,
50✔
4854
                                    'location' => NULL,
50✔
4855
                                    'groupname' => $grp
50✔
4856
                                ];
50✔
4857
                            }
4858
                        }
4859
                    }
4860
                }
4861
            }
4862
        }
4863
    }
4864

4865
    public function getLatLngs($users, $usedef = TRUE, $usegroup = TRUE, $needgroup = FALSE, $atts = NULL, $blur = NULL)
4866
    {
4867
        $userids = array_filter(array_column($users, 'id'));
176✔
4868
        $ret = [];
176✔
4869

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

4873
            foreach ($atts as $att) {
176✔
4874
                $lat = NULL;
176✔
4875
                $lng = NULL;
176✔
4876
                $loc = NULL;
176✔
4877

4878
                if (Utils::pres('settings', $att)) {
176✔
4879
                    $settings = $att['settings'];
37✔
4880
                    $settings = json_decode($settings, TRUE);
37✔
4881

4882
                    if (Utils::pres('mylocation', $settings)) {
37✔
4883
                        $lat = $settings['mylocation']['lat'];
33✔
4884
                        $lng = $settings['mylocation']['lng'];
33✔
4885
                        $loc = Utils::presdef('name', $settings['mylocation'], NULL);
33✔
4886
                        #error_log("Got from mylocation $lat, $lng, $loc");
4887
                    }
4888
                }
4889

4890
                if (is_null($lat)) {
176✔
4891
                    $lid = $att['lastlocation'];
157✔
4892

4893
                    if ($lid) {
157✔
4894
                        $l = new Location($this->dbhr, $this->dbhm, $lid);
23✔
4895
                        $lat = $l->getPrivate('lat');
23✔
4896
                        $lng = $l->getPrivate('lng');
23✔
4897
                        $loc = $l->getPrivate('name');
23✔
4898
                        #error_log("Got from last location $lat, $lng, $loc");
4899
                    }
4900
                }
4901

4902
                if (!is_null($lat)) {
176✔
4903
                    $ret[$att['id']] = [
52✔
4904
                        'lat' => $lat,
52✔
4905
                        'lng' => $lng,
52✔
4906
                        'loc' => $loc,
52✔
4907
                    ];
52✔
4908

4909
                    $userids = array_filter($userids, function($id) use ($att) {
52✔
4910
                        return $id != $att['id'];
52✔
4911
                    });
52✔
4912
                }
4913
            }
4914
        }
4915

4916
        if ($userids && count($userids) && $usegroup) {
176✔
4917
            # Still some we haven't handled.  Get the last message posted on a group with a location, if any.
4918
            $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✔
4919
            foreach ($membs as $memb) {
150✔
4920
                $ret[$memb['userid']] = [
3✔
4921
                    'lat' => $memb['lat'],
3✔
4922
                    'lng' => $memb['lng']
3✔
4923
                ];
3✔
4924

4925
                #error_log("Got from last message posted {$memb['lat']}, {$memb['lng']}");
4926

4927
                $userids = array_filter($userids, function($id) use ($memb) {
3✔
4928
                    return $id != $memb['userid'];
3✔
4929
                });
3✔
4930
            }
4931
        }
4932

4933
        if ($userids && count($userids) && $usegroup) {
176✔
4934
            # Still some we haven't handled.  Get the memberships.  Logic will choose most recently joined.
4935
            $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✔
4936
            foreach ($membs as $memb) {
148✔
4937
                $ret[$memb['userid']] = [
129✔
4938
                    'lat' => $memb['lat'],
129✔
4939
                    'lng' => $memb['lng'],
129✔
4940
                    'group' => Utils::presdef('namefull', $memb, $memb['nameshort'])
129✔
4941
                ];
129✔
4942

4943
                #error_log("Got from membership {$memb['lat']}, {$memb['lng']}, " . Utils::presdef('namefull', $memb, $memb['nameshort']));
4944

4945
                $userids = array_filter($userids, function($id) use ($memb) {
129✔
4946
                    return $id != $memb['userid'];
129✔
4947
                });
129✔
4948
            }
4949
        }
4950

4951
        if ($userids && count($userids)) {
176✔
4952
            # Still some we haven't handled.
4953
            foreach ($userids as $userid) {
26✔
4954
                if ($usedef) {
26✔
4955
                    $ret[$userid] = [
23✔
4956
                        'lat' => 53.9450,
23✔
4957
                        'lng' => -2.5209
23✔
4958
                    ];
23✔
4959
                } else {
4960
                    $ret[$userid] = NULL;
15✔
4961
                }
4962
            }
4963
        }
4964

4965
        if ($needgroup) {
176✔
4966
            # Get a group name.
4967
            $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✔
4968
            foreach ($membs as $memb) {
7✔
4969
                $ret[$memb['userid']]['group'] = Utils::presdef('namefull', $memb, $memb['nameshort']);
7✔
4970
            }
4971
        }
4972

4973
        if ($blur) {
176✔
4974
            foreach ($ret as &$memb) {
7✔
4975
                if ($memb['lat'] || $memb['lng']) {
7✔
4976
                    list ($memb['lat'], $memb['lng']) = Utils::blur($memb['lat'], $memb['lng'], $blur);
7✔
4977
                }
4978
            }
4979
        }
4980

4981
        return ($ret);
176✔
4982
    }
4983

4984
    public function isFreegleMod()
4985
    {
4986
        $ret = FALSE;
173✔
4987

4988
        $this->cacheMemberships();
173✔
4989

4990
        foreach ($this->memberships as $mem) {
173✔
4991
            if ($mem['type'] == Group::GROUP_FREEGLE && ($mem['role'] == User::ROLE_OWNER || $mem['role'] == User::ROLE_MODERATOR)) {
145✔
4992
                $ret = TRUE;
41✔
4993
            }
4994
        }
4995

4996
        return ($ret);
173✔
4997
    }
4998

4999
    public function getKudos($id = NULL)
5000
    {
5001
        $id = $id ? $id : $this->id;
1✔
5002
        $kudos = [
1✔
5003
            'userid' => $id,
1✔
5004
            'posts' => 0,
1✔
5005
            'chats' => 0,
1✔
5006
            'newsfeed' => 0,
1✔
5007
            'events' => 0,
1✔
5008
            'vols' => 0,
1✔
5009
            'facebook' => 0,
1✔
5010
            'platform' => 0,
1✔
5011
            'kudos' => 0,
1✔
5012
        ];
1✔
5013

5014
        $kudi = $this->dbhr->preQuery("SELECT * FROM users_kudos WHERE userid = ?;", [
1✔
5015
            $id
1✔
5016
        ]);
1✔
5017

5018
        foreach ($kudi as $k) {
1✔
5019
            $kudos = $k;
1✔
5020
        }
5021

5022
        return ($kudos);
1✔
5023
    }
5024

5025
    public function updateKudos($id = NULL, $force = FALSE)
5026
    {
5027
        $current = $this->getKudos($id);
1✔
5028

5029
        # Only update if we don't have one or it's older than a day.  This avoids repeatedly updating the entry
5030
        # for the same user in some bulk operations.
5031
        if (!Utils::pres('timestamp', $current) || (time() - strtotime($current['timestamp']) > 24 * 60 * 60)) {
1✔
5032
            # We analyse a user's activity and assign them a level.
5033
            #
5034
            # Only interested in activity in the last year.
5035
            $id = $id ? $id : $this->id;
1✔
5036
            $start = date('Y-m-d', strtotime("365 days ago"));
1✔
5037

5038
            # First, the number of months in which they have posted.
5039
            $posts = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM messages WHERE fromuser = ? AND date >= '$start';", [
1✔
5040
                $id
1✔
5041
            ])[0]['count'];
1✔
5042

5043
            # Ditto communicated with people.
5044
            $chats = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM chat_messages WHERE userid = ? AND date >= '$start';", [
1✔
5045
                $id
1✔
5046
            ])[0]['count'];
1✔
5047

5048
            # Newsfeed posts
5049
            $newsfeed = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(timestamp), '-', MONTH(timestamp)))) AS count FROM newsfeed WHERE userid = ? AND added >= '$start';", [
1✔
5050
                $id
1✔
5051
            ])[0]['count'];
1✔
5052

5053
            # Events
5054
            $events = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM communityevents WHERE userid = ? AND added >= '$start';", [
1✔
5055
                $id
1✔
5056
            ])[0]['count'];
1✔
5057

5058
            # Volunteering
5059
            $vols = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM volunteering WHERE userid = ? AND added >= '$start';", [
1✔
5060
                $id
1✔
5061
            ])[0]['count'];
1✔
5062

5063
            # Do they have a Facebook login?
5064
            $facebook = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_logins WHERE userid = ? AND type = ?", [
1✔
5065
                    $id,
1✔
5066
                    User::LOGIN_FACEBOOK
1✔
5067
                ])[0]['count'] > 0;
1✔
5068

5069
            # Have they posted using the platform?
5070
            $platform = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM messages WHERE fromuser = ? AND arrival >= '$start' AND sourceheader = ?;", [
1✔
5071
                    $id,
1✔
5072
                    Message::PLATFORM
1✔
5073
                ])[0]['count'] > 0;
1✔
5074

5075
            $kudos = $posts + $chats + $newsfeed + $events + $vols;
1✔
5076

5077
            if ($kudos > 0 || $force) {
1✔
5078
                # No sense in creating entries which are blank or the same.
5079
                $current = $this->getKudos($id);
1✔
5080

5081
                if ($current['kudos'] != $kudos || $force) {
1✔
5082
                    $this->dbhm->preExec("REPLACE INTO users_kudos (userid, kudos, posts, chats, newsfeed, events, vols, facebook, platform) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", [
1✔
5083
                        $id,
1✔
5084
                        $kudos,
1✔
5085
                        $posts,
1✔
5086
                        $chats,
1✔
5087
                        $newsfeed,
1✔
5088
                        $events,
1✔
5089
                        $vols,
1✔
5090
                        $facebook,
1✔
5091
                        $platform
1✔
5092
                    ], FALSE);
1✔
5093
                }
5094
            }
5095
        }
5096
    }
5097

5098
    public function topKudos($gid, $limit = 10)
5099
    {
5100
        $limit = intval($limit);
1✔
5101

5102
        $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✔
5103
            $gid,
1✔
5104
            User::ROLE_MEMBER
1✔
5105
        ]);
1✔
5106

5107
        $ret = [];
1✔
5108

5109
        foreach ($kudos as $k) {
1✔
5110
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5111
            $atts = $u->getPublic();
1✔
5112
            $atts['email'] = $u->getEmailPreferred();
1✔
5113

5114
            $thisone = [
1✔
5115
                'user' => $atts,
1✔
5116
                'kudos' => $k
1✔
5117
            ];
1✔
5118

5119
            $ret[] = $thisone;
1✔
5120
        }
5121

5122
        return ($ret);
1✔
5123
    }
5124

5125
    public function possibleMods($gid, $limit = 10)
5126
    {
5127
        # We look for users who are not mods with top kudos who also:
5128
        # - active in last 60 days
5129
        # - not bouncing
5130
        # - using a location which is in the group area
5131
        # - have posted with the platform, as we don't want loyal users of TN or Yahoo.
5132
        # - have a Facebook login, as they are more likely to do publicity.
5133
        $limit = intval($limit);
1✔
5134
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
5135
        $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✔
5136
        $kudos = $this->dbhr->preQuery($sql, [
1✔
5137
            $gid,
1✔
5138
            User::ROLE_MEMBER
1✔
5139
        ]);
1✔
5140

5141
        $ret = [];
1✔
5142

5143
        foreach ($kudos as $k) {
1✔
5144
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5145
            $atts = $u->getPublic();
1✔
5146
            $atts['email'] = $u->getEmailPreferred();
1✔
5147

5148
            $thisone = [
1✔
5149
                'user' => $atts,
1✔
5150
                'kudos' => $k
1✔
5151
            ];
1✔
5152

5153
            $ret[] = $thisone;
1✔
5154
        }
5155

5156
        return ($ret);
1✔
5157
    }
5158

5159
    public function requestExport($sync = FALSE)
5160
    {
5161
        $tag = Utils::randstr(64);
8✔
5162

5163
        # Flag sync ones as started to avoid window with background thread.
5164
        $sync = $sync ? "NOW()" : "NULL";
8✔
5165
        $this->dbhm->preExec("INSERT INTO users_exports (userid, tag, started) VALUES (?, ?, $sync);", [
8✔
5166
            $this->id,
8✔
5167
            $tag
8✔
5168
        ]);
8✔
5169

5170
        return ([$this->dbhm->lastInsertId(), $tag]);
8✔
5171
    }
5172

5173
    public function export($exportid, $tag)
5174
    {
5175
        $this->dbhm->preExec("UPDATE users_exports SET started = NOW() WHERE id = ? AND tag = ?;", [
7✔
5176
            $exportid,
7✔
5177
            $tag
7✔
5178
        ]);
7✔
5179

5180
        # For GDPR we support the ability for a user to export the data we hold about them.  Key points about this:
5181
        #
5182
        # - It needs to be at a high level of abstraction and understandable by the user, not just a cryptic data
5183
        #   dump.
5184
        # - It needs to include data provided by the user and data observed about the user, but not profiling
5185
        #   or categorisation based on that data.  This means that (for example) we need to return which
5186
        #   groups they have joined, but not whether joining those groups has flagged them up as a potential
5187
        #   spammer.
5188
        $ret = [];
7✔
5189
        error_log("...basic info");
7✔
5190

5191
        # Data in user table.
5192
        $d = [];
7✔
5193
        $d['Our_internal_ID_for_you'] = $this->getPrivate('id');
7✔
5194
        $d['Your_full_name'] = $this->getPrivate('fullname');
7✔
5195
        $d['Your_first_name'] = $this->getPrivate('firstname');
7✔
5196
        $d['Your_last_name'] = $this->getPrivate('lastname');
7✔
5197
        $d['Your_Yahoo_ID'] = $this->getPrivate('yahooid');
7✔
5198
        $d['Your_role_on_the_system'] = $this->getPrivate('systemrole');
7✔
5199
        $d['When_you_joined_the_site'] = Utils::ISODate($this->getPrivate('added'));
7✔
5200
        $d['When_you_last_accessed_the_site'] = Utils::ISODate($this->getPrivate('lastaccess'));
7✔
5201
        $d['When_we_last_checked_for_relevant_posts_for_you'] = Utils::ISODate($this->getPrivate('lastrelevantcheck'));
7✔
5202
        $d['Whether_your_email_is_bouncing'] = $this->getPrivate('bouncing') ? 'Yes' : 'No';
7✔
5203
        $d['Permissions_you_have_on_the_site'] = $this->getPrivate('permissions');
7✔
5204
        $d['Number_of_remaining_invitations_you_can_send_to_other_people'] = $this->getPrivate('invitesleft');
7✔
5205

5206
        $lastlocation = $this->user['lastlocation'];
7✔
5207

5208
        if ($lastlocation) {
7✔
5209
            $l = new Location($this->dbhr, $this->dbhm, $lastlocation);
×
5210
            $d['Last_location_you_posted_from'] = $l->getPrivate('name') . " (" . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
×
5211
        }
5212

5213
        $settings = $this->getPrivate('settings');
7✔
5214

5215
        if ($settings) {
7✔
5216
            $settings = json_decode($settings, TRUE);
7✔
5217

5218
            $location = Utils::presdef('id', Utils::presdef('mylocation', $settings, []), NULL);
7✔
5219

5220
            if ($location) {
7✔
5221
                $l = new Location($this->dbhr, $this->dbhm, $location);
6✔
5222
                $d['Last_location_you_entered'] = $l->getPrivate('name') . ' (' . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
6✔
5223
            }
5224

5225
            $notifications = Utils::pres('notifications', $settings);
7✔
5226

5227
            $d['Notifications']['Send_email_notifications_for_chat_messages'] = Utils::presdef('email', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5228
            $d['Notifications']['Send_email_notifications_of_chat_messages_you_send'] = Utils::presdef('emailmine', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5229
            $d['Notifications']['Send_notifications_for_apps'] = Utils::presdef('app', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5230
            $d['Notifications']['Send_push_notifications_to_web_browsers'] = Utils::presdef('push', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5231
            $d['Notifications']['Send_Facebook_notifications'] = Utils::presdef('facebook', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5232
            $d['Notifications']['Send_emails_about_notifications_on_the_site'] = Utils::presdef('notificationmails', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5233

5234
            $d['Hide_profile_picture'] = Utils::presdef('useprofile', $settings, TRUE) ? 'Yes' : 'No';
7✔
5235

5236
            if ($this->isModerator()) {
7✔
5237
                $d['Show_members_that_you_are_a_moderator'] = Utils::pres('showmod', $settings) ? 'Yes' : 'No';
1✔
5238

5239
                switch (Utils::presdef('modnotifs', $settings, 4)) {
1✔
5240
                    case 24:
1✔
5241
                        $d['Send_notifications_of_active_mod_work'] = 'After 24 hours';
×
5242
                        break;
×
5243
                    case 12:
1✔
5244
                        $d['Send_notifications_of_active_mod_work'] = 'After 12 hours';
×
5245
                        break;
×
5246
                    case 4:
1✔
5247
                        $d['Send_notifications_of_active_mod_work'] = 'After 4 hours';
1✔
5248
                        break;
1✔
5249
                    case 2:
×
5250
                        $d['Send_notifications_of_active_mod_work'] = 'After 2 hours';
×
5251
                        break;
×
5252
                    case 1:
×
5253
                        $d['Send_notifications_of_active_mod_work'] = 'After 1 hours';
×
5254
                        break;
×
5255
                    case 0:
×
5256
                        $d['Send_notifications_of_active_mod_work'] = 'Immediately';
×
5257
                        break;
×
5258
                    case -1:
5259
                        $d['Send_notifications_of_active_mod_work'] = 'Never';
×
5260
                        break;
×
5261
                }
5262

5263
                switch (Utils::presdef('backupmodnotifs', $settings, 12)) {
1✔
5264
                    case 24:
1✔
5265
                        $d['Send_notifications_of_backup_mod_work'] = 'After 24 hours';
×
5266
                        break;
×
5267
                    case 12:
1✔
5268
                        $d['Send_notifications_of_backup_mod_work'] = 'After 12 hours';
1✔
5269
                        break;
1✔
5270
                    case 4:
×
5271
                        $d['Send_notifications_of_backup_mod_work'] = 'After 4 hours';
×
5272
                        break;
×
5273
                    case 2:
×
5274
                        $d['Send_notifications_of_backup_mod_work'] = 'After 2 hours';
×
5275
                        break;
×
5276
                    case 1:
×
5277
                        $d['Send_notifications_of_backup_mod_work'] = 'After 1 hours';
×
5278
                        break;
×
5279
                    case 0:
×
5280
                        $d['Send_notifications_of_backup_mod_work'] = 'Immediately';
×
5281
                        break;
×
5282
                    case -1:
5283
                        $d['Send_notifications_of_backup_mod_work'] = 'Never';
×
5284
                        break;
×
5285
                }
5286

5287
                $d['Show_members_that_you_are_a_moderator'] = Utils::presdef('showmod', $settings, TRUE) ? 'Yes' : 'No';
1✔
5288
            }
5289
        }
5290

5291
        # Invitations.  Only show what we sent; the outcome is not this user's business.
5292
        error_log("...invitations");
7✔
5293
        $invites = $this->listInvitations("1970-01-01");
7✔
5294
        $d['invitations'] = [];
7✔
5295

5296
        foreach ($invites as $invite) {
7✔
5297
            $d['invitations'][] = [
6✔
5298
                'email' => $invite['email'],
6✔
5299
                'date' => Utils::ISODate($invite['date'])
6✔
5300
            ];
6✔
5301
        }
5302

5303
        error_log("...emails");
7✔
5304
        $d['emails'] = $this->getEmails();
7✔
5305

5306
        foreach ($d['emails'] as &$email) {
7✔
5307
            $email['added'] = Utils::ISODate($email['added']);
1✔
5308

5309
            if ($email['validated']) {
1✔
5310
                $email['validated'] = Utils::ISODate($email['validated']);
×
5311
            }
5312
        }
5313

5314
        $phones = $this->dbhr->preQuery("SELECT * FROM users_phones WHERE userid = ?;", [
7✔
5315
            $this->id
7✔
5316
        ]);
7✔
5317

5318
        foreach ($phones as $phone) {
7✔
5319
            $d['phone'] = $phone['number'];
6✔
5320
            $d['phonelastsent'] = Utils::ISODate($phone['lastsent']);
6✔
5321
            $d['phonelastclicked'] = Utils::ISODate($phone['lastclicked']);
6✔
5322
        }
5323

5324
        error_log("...logins");
7✔
5325
        $d['logins'] = $this->dbhr->preQuery("SELECT type, uid, added, lastaccess FROM users_logins WHERE userid = ?;", [
7✔
5326
            $this->id
7✔
5327
        ]);
7✔
5328

5329
        foreach ($d['logins'] as &$dd) {
7✔
5330
            $dd['added'] = Utils::ISODate($dd['added']);
7✔
5331
            $dd['lastaccess'] = Utils::ISODate($dd['lastaccess']);
7✔
5332
        }
5333

5334
        error_log("...memberships");
7✔
5335
        $d['memberships'] = $this->getMemberships();
7✔
5336

5337
        error_log("...memberships history");
7✔
5338
        $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✔
5339
        $membs = $this->dbhr->preQuery($sql, [$this->id]);
7✔
5340
        foreach ($membs as &$memb) {
7✔
5341
            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
7✔
5342
            $memb['namedisplay'] = $name;
7✔
5343
            $memb['added'] = Utils::ISODate($memb['added']);
7✔
5344
        }
5345

5346
        $d['membershipshistory'] = $membs;
7✔
5347

5348
        error_log("...searches");
7✔
5349
        $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✔
5350
            $this->id
7✔
5351
        ]);
7✔
5352

5353
        foreach ($d['searches'] as &$s) {
7✔
5354
            $s['date'] = Utils::ISODate($s['date']);
×
5355
        }
5356

5357
        error_log("...alerts");
7✔
5358
        $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✔
5359
            $this->id
7✔
5360
        ]);
7✔
5361

5362
        foreach ($d['alerts'] as &$s) {
7✔
5363
            $s['responded'] = Utils::ISODate($s['responded']);
×
5364
        }
5365

5366
        error_log("...donations");
7✔
5367
        $d['donations'] = $this->dbhr->preQuery("SELECT * FROM users_donations WHERE userid = ? ORDER BY timestamp ASC;", [
7✔
5368
            $this->id
7✔
5369
        ]);
7✔
5370

5371
        foreach ($d['donations'] as &$s) {
7✔
5372
            $s['timestamp'] = Utils::ISODate($s['timestamp']);
1✔
5373
        }
5374

5375
        error_log("...bans");
7✔
5376
        $d['bans'] = [];
7✔
5377

5378
        $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE byuser = ?;", [
7✔
5379
            $this->id
7✔
5380
        ]);
7✔
5381

5382
        foreach ($bans as $ban) {
7✔
5383
            $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
5384
            $u = User::get($this->dbhr, $this->dbhm, $ban['userid']);
1✔
5385
            $d['bans'][] = [
1✔
5386
                'date' => Utils::ISODate($ban['date']),
1✔
5387
                'group' => $g->getName(),
1✔
5388
                'email' => $u->getEmailPreferred(),
1✔
5389
                'userid' => $ban['userid']
1✔
5390
            ];
1✔
5391
        }
5392

5393
        error_log("...spammers");
7✔
5394
        $d['spammers'] = $this->dbhr->preQuery("SELECT * FROM spam_users WHERE byuserid = ? ORDER BY added ASC;", [
7✔
5395
            $this->id
7✔
5396
        ]);
7✔
5397

5398
        foreach ($d['spammers'] as &$s) {
7✔
5399
            $s['added'] = Utils::ISODate($s['added']);
×
5400
            $u = User::get($this->dbhr, $this->dbhm, $s['userid']);
×
5401
            $s['email'] = $u->getEmailPreferred();
×
5402
        }
5403

5404
        $d['spamdomains'] = $this->dbhr->preQuery("SELECT domain, date FROM spam_whitelist_links WHERE userid = ?;", [
7✔
5405
            $this->id
7✔
5406
        ]);
7✔
5407

5408
        foreach ($d['spamdomains'] as &$s) {
7✔
5409
            $s['date'] = Utils::ISODate($s['date']);
×
5410
        }
5411

5412
        error_log("...images");
7✔
5413
        $images = $this->dbhr->preQuery("SELECT id, url FROM users_images WHERE userid = ?;", [
7✔
5414
            $this->id
7✔
5415
        ]);
7✔
5416

5417
        $d['images'] = [];
7✔
5418

5419
        foreach ($images as $image) {
7✔
5420
            if (Utils::pres('url', $image)) {
6✔
5421
                $d['images'][] = [
6✔
5422
                    'id' => $image['id'],
6✔
5423
                    'thumb' => $image['url']
6✔
5424
                ];
6✔
5425
            } else {
5426
                $a = new Attachment($this->dbhr, $this->dbhm, NULL, Attachment::TYPE_USER);
×
5427
                $d['images'][] = [
×
5428
                    'id' => $image['id'],
×
5429
                    'thumb' => $a->getPath(TRUE, $image['id'])
×
5430
                ];
×
5431
            }
5432
        }
5433

5434
        error_log("...notifications");
7✔
5435
        $d['notifications'] = $this->dbhr->preQuery("SELECT timestamp, url FROM users_notifications WHERE touser = ? AND seen = 1;", [
7✔
5436
            $this->id
7✔
5437
        ]);
7✔
5438

5439
        foreach ($d['notifications'] as &$n) {
7✔
5440
            $n['timestamp'] = Utils::ISODate($n['timestamp']);
×
5441
        }
5442

5443
        error_log("...addresses");
7✔
5444
        $d['addresses'] = [];
7✔
5445

5446
        $addrs = $this->dbhr->preQuery("SELECT * FROM users_addresses WHERE userid = ?;", [
7✔
5447
            $this->id
7✔
5448
        ]);
7✔
5449

5450
        foreach ($addrs as $addr) {
7✔
5451
            $a = new Address($this->dbhr, $this->dbhm, $addr['id']);
×
5452
            $d['addresses'][] = $a->getPublic();
×
5453
        }
5454

5455
        error_log("...events");
7✔
5456
        $d['communityevents'] = [];
7✔
5457

5458
        $events = $this->dbhr->preQuery("SELECT id FROM communityevents WHERE userid = ?;", [
7✔
5459
            $this->id
7✔
5460
        ]);
7✔
5461

5462
        foreach ($events as $event) {
7✔
5463
            $e = new CommunityEvent($this->dbhr, $this->dbhm, $event['id']);
×
5464
            $d['communityevents'][] = $e->getPublic();
×
5465
        }
5466

5467
        error_log("...volunteering");
7✔
5468
        $d['volunteering'] = [];
7✔
5469

5470
        $events = $this->dbhr->preQuery("SELECT id FROM volunteering WHERE userid = ?;", [
7✔
5471
            $this->id
7✔
5472
        ]);
7✔
5473

5474
        foreach ($events as $event) {
7✔
5475
            $e = new Volunteering($this->dbhr, $this->dbhm, $event['id']);
×
5476
            $d['volunteering'][] = $e->getPublic();
×
5477
        }
5478

5479
        error_log("...comments");
7✔
5480
        $d['comments'] = [];
7✔
5481
        $comms = $this->dbhr->preQuery("SELECT * FROM users_comments WHERE byuserid = ? ORDER BY date ASC;", [
7✔
5482
            $this->id
7✔
5483
        ]);
7✔
5484

5485
        foreach ($comms as &$comm) {
7✔
5486
            $u = User::get($this->dbhr, $this->dbhm, $comm['userid']);
1✔
5487
            $comm['email'] = $u->getEmailPreferred();
1✔
5488
            $comm['date'] = Utils::ISODate($comm['date']);
1✔
5489
            $d['comments'][] = $comm;
1✔
5490
        }
5491

5492
        error_log("...ratings");
7✔
5493
        $d['ratings'] = $this->getRated();
7✔
5494

5495
        error_log("...locations");
7✔
5496
        $d['locations'] = [];
7✔
5497

5498
        $locs = $this->dbhr->preQuery("SELECT * FROM locations_excluded WHERE userid = ?;", [
7✔
5499
            $this->id
7✔
5500
        ]);
7✔
5501

5502
        foreach ($locs as $loc) {
7✔
5503
            $g = Group::get($this->dbhr, $this->dbhm, $loc['groupid']);
×
5504
            $l = new Location($this->dbhr, $this->dbhm, $loc['locationid']);
×
5505
            $d['locations'][] = [
×
5506
                'group' => $g->getName(),
×
5507
                'location' => $l->getPrivate('name'),
×
5508
                'date' => Utils::ISODate($loc['date'])
×
5509
            ];
×
5510
        }
5511

5512
        error_log("...messages");
7✔
5513
        $msgs = $this->dbhr->preQuery("SELECT id FROM messages WHERE fromuser = ? ORDER BY arrival ASC;", [
7✔
5514
            $this->id
7✔
5515
        ]);
7✔
5516

5517
        $d['messages'] = [];
7✔
5518

5519
        foreach ($msgs as $msg) {
7✔
5520
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
×
5521

5522
            # Show all info here even moderator attributes.  This wouldn't normally be shown to users, but none
5523
            # of it is confidential really.
5524
            $thisone = $m->getPublic(FALSE, FALSE, TRUE);
×
5525

5526
            if (count($thisone['groups']) > 0) {
×
5527
                $g = Group::get($this->dbhr, $this->dbhm, $thisone['groups'][0]['groupid']);
×
5528
                $thisone['groups'][0]['namedisplay'] = $g->getName();
×
5529
            }
5530

5531
            $d['messages'][] = $thisone;
×
5532
        }
5533

5534
        # Chats.  Can't use listForUser as that filters on various things and has a ModTools vs FD distinction, and
5535
        # we're interested in information we have provided.  So we get the chats mentioned in the roster (we have
5536
        # provided information about being online) and where we have sent or reviewed a chat message.
5537
        error_log("...chats");
7✔
5538
        $chatids = $this->dbhr->preQuery("SELECT DISTINCT  id FROM chat_rooms INNER JOIN (SELECT DISTINCT chatid FROM chat_roster WHERE userid = ? UNION SELECT DISTINCT chatid FROM chat_messages WHERE userid = ? OR reviewedby = ?) t ON t.chatid = chat_rooms.id ORDER BY latestmessage ASC;", [
7✔
5539
            $this->id,
7✔
5540
            $this->id,
7✔
5541
            $this->id
7✔
5542
        ]);
7✔
5543

5544
        $d['chatrooms'] = [];
7✔
5545
        $count = 0;
7✔
5546

5547
        foreach ($chatids as $chatid) {
7✔
5548
            # We don't return the chat name because it's too slow to produce.
5549
            $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid['id']);
6✔
5550
            $thisone = [
6✔
5551
                'id' => $chatid['id'],
6✔
5552
                'name' => $r->getPublic($this)['name'],
6✔
5553
                'messages' => []
6✔
5554
            ];
6✔
5555

5556
            $sql = "SELECT date, lastip FROM chat_roster WHERE `chatid` = ? AND userid = ?;";
6✔
5557
            $roster = $this->dbhr->preQuery($sql, [$chatid['id'], $this->id]);
6✔
5558
            foreach ($roster as $rost) {
6✔
5559
                $thisone['lastip'] = $rost['lastip'];
6✔
5560
                $thisone['date'] = Utils::ISODate($rost['date']);
6✔
5561
            }
5562

5563
            # Get the messages we have sent in this chat.
5564
            $msgs = $this->dbhr->preQuery("SELECT id FROM chat_messages WHERE chatid = ? AND (userid = ? OR reviewedby = ?);", [
6✔
5565
                $chatid['id'],
6✔
5566
                $this->id,
6✔
5567
                $this->id
6✔
5568
            ]);
6✔
5569

5570
            $userlist = NULL;
6✔
5571

5572
            foreach ($msgs as $msg) {
6✔
5573
                $cm = new ChatMessage($this->dbhr, $this->dbhm, $msg['id']);
6✔
5574
                $thismsg = $cm->getPublic(FALSE, $userlist);
6✔
5575

5576
                # Strip out most of the refmsg detail - it's not ours and we need to save volume of data.
5577
                $refmsg = Utils::pres('refmsg', $thismsg);
6✔
5578

5579
                if ($refmsg) {
6✔
5580
                    $thismsg['refmsg'] = [
×
5581
                        'id' => $msg['id'],
×
5582
                        'subject' => Utils::presdef('subject', $refmsg, NULL)
×
5583
                    ];
×
5584
                }
5585

5586
                $thismsg['mine'] = Utils::presdef('userid', $thismsg, NULL) == $this->id;
6✔
5587
                $thismsg['date'] = Utils::ISODate($thismsg['date']);
6✔
5588
                $thisone['messages'][] = $thismsg;
6✔
5589

5590
                $count++;
6✔
5591
//
5592
//                if ($count > 200) {
5593
//                    break 2;
5594
//                }
5595
            }
5596

5597
            if (count($thisone['messages']) > 0) {
6✔
5598
                $d['chatrooms'][] = $thisone;
6✔
5599
            }
5600
        }
5601

5602
        error_log("...newsfeed");
7✔
5603
        $newsfeeds = $this->dbhr->preQuery("SELECT * FROM newsfeed WHERE userid = ?;", [
7✔
5604
            $this->id
7✔
5605
        ]);
7✔
5606

5607
        $d['newsfeed'] = [];
7✔
5608

5609
        foreach ($newsfeeds as $newsfeed) {
7✔
5610
            $n = new Newsfeed($this->dbhr, $this->dbhm, $newsfeed['id']);
6✔
5611
            $thisone = $n->getPublic(FALSE, FALSE, FALSE, FALSE);
6✔
5612
            $d['newsfeed'][] = $thisone;
6✔
5613
        }
5614

5615
        $d['newsfeed_unfollows'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_unfollow WHERE userid = ?;", [
7✔
5616
            $this->id
7✔
5617
        ]);
7✔
5618

5619
        foreach ($d['newsfeed_unfollows'] as &$dd) {
7✔
5620
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5621
        }
5622

5623
        $d['newsfeed_likes'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_likes WHERE userid = ?;", [
7✔
5624
            $this->id
7✔
5625
        ]);
7✔
5626

5627
        foreach ($d['newsfeed_likes'] as &$dd) {
7✔
5628
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5629
        }
5630

5631
        $d['newsfeed_reports'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_reports WHERE userid = ?;", [
7✔
5632
            $this->id
7✔
5633
        ]);
7✔
5634

5635
        foreach ($d['newsfeed_reports'] as &$dd) {
7✔
5636
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5637
        }
5638

5639
        $d['aboutme'] = $this->dbhr->preQuery("SELECT timestamp, text FROM users_aboutme WHERE userid = ? AND LENGTH(text) > 5;", [
7✔
5640
            $this->id
7✔
5641
        ]);
7✔
5642

5643
        foreach ($d['aboutme'] as &$dd) {
7✔
5644
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5645
        }
5646

5647
        error_log("...stories");
7✔
5648
        $d['stories'] = $this->dbhr->preQuery("SELECT date, headline, story FROM users_stories WHERE userid = ?;", [
7✔
5649
            $this->id
7✔
5650
        ]);
7✔
5651

5652
        foreach ($d['stories'] as &$dd) {
7✔
5653
            $dd['date'] = Utils::ISODate($dd['date']);
×
5654
        }
5655

5656
        $d['stories_likes'] = $this->dbhr->preQuery("SELECT storyid FROM users_stories_likes WHERE userid = ?;", [
7✔
5657
            $this->id
7✔
5658
        ]);
7✔
5659

5660
        error_log("...exports");
7✔
5661
        $d['exports'] = $this->dbhr->preQuery("SELECT userid, started, completed FROM users_exports WHERE userid = ?;", [
7✔
5662
            $this->id
7✔
5663
        ]);
7✔
5664

5665
        foreach ($d['exports'] as &$dd) {
7✔
5666
            $dd['started'] = Utils::ISODate($dd['started']);
7✔
5667
            $dd['completed'] = Utils::ISODate($dd['completed']);
7✔
5668
        }
5669

5670
        error_log("...logs");
7✔
5671
        $l = new Log($this->dbhr, $this->dbhm);
7✔
5672
        $ctx = NULL;
7✔
5673
        $d['logs'] = $l->get(NULL, NULL, NULL, NULL, NULL, NULL, PHP_INT_MAX, $ctx, $this->id);
7✔
5674

5675
        error_log("...add group to logs");
7✔
5676
        $loggroups = [];
7✔
5677
        foreach ($d['logs'] as &$log) {
7✔
5678
            if (Utils::pres('groupid', $log)) {
7✔
5679
                # Don't put the whole group info in there, as it is slow to get.
5680
                if (!array_key_exists($log['groupid'], $loggroups)) {
7✔
5681
                    $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
7✔
5682

5683
                    if ($g->getId() == $log['groupid']) {
7✔
5684
                        $loggroups[$log['groupid']] = [
7✔
5685
                            'id' => $log['groupid'],
7✔
5686
                            'nameshort' => $g->getPrivate('nameshort'),
7✔
5687
                            'namedisplay' => $g->getName()
7✔
5688
                        ];
7✔
5689
                    } else {
5690
                        $loggroups[$log['groupid']] = [
×
5691
                            'id' => $log['groupid'],
×
5692
                            'nameshort' => "DeletedGroup{$log['groupid']}",
×
5693
                            'namedisplay' => "Deleted group #{$log['groupid']}"
×
5694
                        ];
×
5695
                    }
5696
                }
5697

5698
                $log['group'] = $loggroups[$log['groupid']];
7✔
5699
            }
5700
        }
5701

5702
        # Gift aid
5703
        $don = new Donations($this->dbhr, $this->dbhm);
7✔
5704
        $d['giftaid'] = $don->getGiftAid($this->id);
7✔
5705

5706
        $ret = $d;
7✔
5707

5708
        # There are some other tables with information which we don't return.  Here's what and why:
5709
        # - Not part of the current UI so can't have any user data
5710
        #     polls_users
5711
        # - Covered by data that we do return from other tables
5712
        #     messages_drafts, messages_history, messages_groups, messages_outcomes,
5713
        #     messages_promises, users_modmails, modnotifs, users_dashboard,
5714
        #     users_nudges
5715
        # - Transient logging data
5716
        #     logs_emails, logs_sql, logs_api, logs_errors, logs_src
5717
        # - Not provided by the user themselves
5718
        #     user_comments, messages_reneged, spam_users, users_banned, users_stories_requested,
5719
        #     users_thanks
5720
        # - Inferred or derived data.  These are not considered to be provided by the user (see p10 of
5721
        #   http://ec.europa.eu/newsroom/document.cfm?doc_id=44099)
5722
        #     users_kudos, visualise
5723

5724
        # Compress the data in the DB because it can be huge.
5725
        #
5726
        error_log("...filter");
7✔
5727
        Utils::filterResult($ret);
7✔
5728
        error_log("...encode");
7✔
5729
        $data = json_encode($ret, JSON_PARTIAL_OUTPUT_ON_ERROR);
7✔
5730
        error_log("...encoded length " . strlen($data) . ", now compress");
7✔
5731
        $data = gzdeflate($data);
7✔
5732
        $this->dbhm->preExec("UPDATE users_exports SET completed = NOW(), data = ? WHERE id = ? AND tag = ?;", [
7✔
5733
            $data,
7✔
5734
            $exportid,
7✔
5735
            $tag
7✔
5736
        ]);
7✔
5737
        error_log("...completed, length " . strlen($data));
7✔
5738

5739
        return ($ret);
7✔
5740
    }
5741

5742
    function getExport($userid, $id, $tag)
5743
    {
5744
        $ret = NULL;
2✔
5745

5746
        $exports = $this->dbhr->preQuery("SELECT * FROM users_exports WHERE userid = ? AND id = ? AND tag = ?;", [
2✔
5747
            $userid,
2✔
5748
            $id,
2✔
5749
            $tag
2✔
5750
        ]);
2✔
5751

5752
        foreach ($exports as $export) {
2✔
5753
            $ret = $export;
2✔
5754
            $ret['requested'] = $ret['requested'] ? Utils::ISODate($ret['requested']) : NULL;
2✔
5755
            $ret['started'] = $ret['started'] ? Utils::ISODate($ret['started']) : NULL;
2✔
5756
            $ret['completed'] = $ret['completed'] ? Utils::ISODate($ret['completed']) : NULL;
2✔
5757

5758
            if ($ret['completed']) {
2✔
5759
                # This has completed.  Return the data.  Will be zapped in cron exports..
5760
                $ret['data'] = json_decode(gzinflate($export['data']), TRUE);
2✔
5761
                $ret['infront'] = 0;
2✔
5762
            } else {
5763
                # Find how many are in front of us.
5764
                $infront = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_exports WHERE id < ? AND completed IS NULL;", [
2✔
5765
                    $id
2✔
5766
                ]);
2✔
5767

5768
                $ret['infront'] = $infront[0]['count'];
2✔
5769
            }
5770
        }
5771

5772
        return ($ret);
2✔
5773
    }
5774

5775
    public function limbo() {
5776
        # We set the deleted attribute, which will cause the v2 Go API not to return any personal data.  The user
5777
        # can still log in, and potentially recover their account by calling session with PATCH of deleted = NULL.
5778
        # Otherwise a background script will purge their account after a couple of weeks.
5779
        #
5780
        # This allows us to handle the fairly common case of users deleting their accounts by mistake, or changing
5781
        # their minds.  This often happens because one-click unsubscribe in emails, which we need to have for
5782
        # delivery.
5783
        $this->dbhm->preExec("UPDATE users SET deleted = NOW() WHERE id = ?;", [
3✔
5784
            $this->id
3✔
5785
        ]);
3✔
5786
    }
5787

5788
    public function processForgets($id = NULL) {
5789
        $count = 0;
1✔
5790

5791
        $idq = $id ? "AND id = $id" : "";
1✔
5792
        $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✔
5793

5794
        foreach ($users as $user) {
1✔
5795
            $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
5796
            $u->forget('Grace period');
1✔
5797
            $count++;
1✔
5798
        }
5799

5800
        return $count;
1✔
5801
    }
5802

5803
    public function forget($reason)
5804
    {
5805
        # Wipe a user of personal data, for the GDPR right to be forgotten.  We don't delete the user entirely
5806
        # otherwise it would mess up the stats.
5807

5808
        # Clear name etc.
5809
        $this->setPrivate('firstname', NULL);
7✔
5810
        $this->setPrivate('lastname', NULL);
7✔
5811
        $this->setPrivate('fullname', "Deleted User #" . $this->id);
7✔
5812
        $this->setPrivate('settings', NULL);
7✔
5813
        $this->setPrivate('yahooid', NULL);
7✔
5814

5815
        # Delete emails which aren't ours.
5816
        $emails = $this->getEmails();
7✔
5817

5818
        foreach ($emails as $email) {
7✔
5819
            if (!$email['ourdomain']) {
3✔
5820
                $this->removeEmail($email['email']);
3✔
5821
            }
5822
        }
5823

5824
        # Delete all logins.
5825
        $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ?;", [
7✔
5826
            $this->id
7✔
5827
        ]);
7✔
5828

5829
        # Delete any phone numbers.
5830
        $this->dbhm->preExec("DELETE FROM users_phones WHERE userid = ?;", [
7✔
5831
            $this->id
7✔
5832
        ]);
7✔
5833

5834
        # Delete the content (but not subject) of any messages, and any email header information such as their
5835
        # name and email address.
5836
        $msgs = $this->dbhm->preQuery("SELECT id FROM messages WHERE fromuser = ? AND messages.type IN (?, ?);", [
7✔
5837
            $this->id,
7✔
5838
            Message::TYPE_OFFER,
7✔
5839
            Message::TYPE_WANTED
7✔
5840
        ]);
7✔
5841

5842
        foreach ($msgs as $msg) {
7✔
5843
            $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✔
5844
                $msg['id']
1✔
5845
            ]);
1✔
5846

5847
            $this->dbhm->preExec("UPDATE messages_groups SET deleted = 1 WHERE msgid = ?;", [
1✔
5848
                $msg['id']
1✔
5849
            ]);
1✔
5850

5851
            # Delete outcome comments that they've added - just about might have personal data.
5852
            $this->dbhm->preExec("UPDATE messages_outcomes SET comments = NULL WHERE msgid = ?;", [
1✔
5853
                $msg['id']
1✔
5854
            ]);
1✔
5855

5856
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
1✔
5857

5858
            if (!$m->hasOutcome()) {
1✔
5859
                $m->withdraw('Withdrawn on user unsubscribe', NULL);
1✔
5860
            }
5861
        }
5862

5863
        # Remove all the content of all chat messages which they have sent (but not received).
5864
        $msgs = $this->dbhm->preQuery("SELECT id FROM chat_messages WHERE userid = ?;", [
7✔
5865
            $this->id
7✔
5866
        ]);
7✔
5867

5868
        foreach ($msgs as $msg) {
7✔
5869
            $this->dbhm->preExec("UPDATE chat_messages SET message = NULL WHERE id = ?;", [
1✔
5870
                $msg['id']
1✔
5871
            ]);
1✔
5872
        }
5873

5874
        # Delete completely any community events, volunteering opportunities, newsfeed posts, searches and stories
5875
        # they have created (their personal details might be in there), and any ratings by or about them.
5876
        $this->dbhm->preExec("DELETE FROM communityevents WHERE userid = ?;", [
7✔
5877
            $this->id
7✔
5878
        ]);
7✔
5879
        $this->dbhm->preExec("DELETE FROM volunteering WHERE userid = ?;", [
7✔
5880
            $this->id
7✔
5881
        ]);
7✔
5882
        $this->dbhm->preExec("DELETE FROM newsfeed WHERE userid = ?;", [
7✔
5883
            $this->id
7✔
5884
        ]);
7✔
5885
        $this->dbhm->preExec("DELETE FROM users_stories WHERE userid = ?;", [
7✔
5886
            $this->id
7✔
5887
        ]);
7✔
5888
        $this->dbhm->preExec("DELETE FROM users_searches WHERE userid = ?;", [
7✔
5889
            $this->id
7✔
5890
        ]);
7✔
5891
        $this->dbhm->preExec("DELETE FROM users_aboutme WHERE userid = ?;", [
7✔
5892
            $this->id
7✔
5893
        ]);
7✔
5894
        $this->dbhm->preExec("DELETE FROM ratings WHERE rater = ?;", [
7✔
5895
            $this->id
7✔
5896
        ]);
7✔
5897
        $this->dbhm->preExec("DELETE FROM ratings WHERE ratee = ?;", [
7✔
5898
            $this->id
7✔
5899
        ]);
7✔
5900

5901
        # Remove them from all groups.
5902
        $membs = $this->getMemberships();
7✔
5903

5904
        foreach ($membs as $memb) {
7✔
5905
            $this->removeMembership($memb['id']);
3✔
5906
        }
5907

5908
        # Delete any postal addresses
5909
        $this->dbhm->preExec("DELETE FROM users_addresses WHERE userid = ?;", [
7✔
5910
            $this->id
7✔
5911
        ]);
7✔
5912

5913
        # Delete any profile images
5914
        $this->dbhm->preExec("DELETE FROM users_images WHERE userid = ?;", [
7✔
5915
            $this->id
7✔
5916
        ]);
7✔
5917

5918
        # Remove any promises.
5919
        $this->dbhm->preExec("DELETE FROM messages_promises WHERE userid = ?;", [
7✔
5920
            $this->id
7✔
5921
        ]);
7✔
5922

5923
        $this->dbhm->preExec("UPDATE users SET forgotten = NOW(), tnuserid = NULL WHERE id = ?;", [
7✔
5924
            $this->id
7✔
5925
        ]);
7✔
5926

5927
        $this->dbhm->preExec("DELETE FROM sessions WHERE userid = ?;", [
7✔
5928
            $this->id
7✔
5929
        ]);
7✔
5930

5931
        $l = new Log($this->dbhr, $this->dbhm);
7✔
5932
        $l->log([
7✔
5933
            'type' => Log::TYPE_USER,
7✔
5934
            'subtype' => Log::SUBTYPE_DELETED,
7✔
5935
            'user' => $this->id,
7✔
5936
            'text' => $reason
7✔
5937
        ]);
7✔
5938
    }
5939

5940
    public function userRetention($userid = NULL)
5941
    {
5942
        # Find users who:
5943
        # - were added six months ago
5944
        # - are not on any groups
5945
        # - have not logged in for six months
5946
        # - are not on the spammer list
5947
        # - do not have mod notes
5948
        # - have no logs for six months
5949
        #
5950
        # We have no good reason to keep any data about them, and should therefore purge them.
5951
        $count = 0;
1✔
5952
        $userq = $userid ? " users.id = $userid AND " : '';
1✔
5953
        $mysqltime = date("Y-m-d", strtotime("6 months ago"));
1✔
5954
        $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✔
5955
        $users = $this->dbhr->preQuery($sql, [
1✔
5956
            User::SYSTEMROLE_USER
1✔
5957
        ]);
1✔
5958

5959
        foreach ($users as $user) {
1✔
5960
            $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✔
5961
                $user['id'],
1✔
5962
                Log::TYPE_USER,
1✔
5963
                Log::SUBTYPE_CREATED,
1✔
5964
                Log::SUBTYPE_DELETED
1✔
5965
            ]);
1✔
5966

5967
            error_log("#{$user['id']} Found logs " . count($logs) . " age " . (count($logs) > 0 ? $logs['0']['logsago'] : ' none '));
1✔
5968

5969
            if (count($logs) == 0 || $logs[0]['logsago'] > 90) {
1✔
5970
                error_log("...forget user #{$user['id']} " . (count($logs) > 0 ? $logs[0]['logsago'] : ''));
1✔
5971
                $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
5972
                $u->forget('Inactive');
1✔
5973
                $count++;
1✔
5974
            }
5975

5976
            # Prod garbage collection, as we've seen high memory usage by this.
5977
            User::clearCache();
1✔
5978
            gc_collect_cycles();
1✔
5979
        }
5980

5981
        # The only reason for preserving deleted users is as a placeholder user for messages they sent.  If they
5982
        # don't have any messages, they can go.
5983
        $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✔
5984
            $mysqltime
1✔
5985
        ]);
1✔
5986

5987
        $total = count($ids);
1✔
5988
        $count = 0;
1✔
5989

5990
        foreach ($ids as $id) {
1✔
5991
            $u = new User($this->dbhr, $this->dbhm, $id['id']);
1✔
5992
            #error_log("...delete user #{$id['id']}");
5993
            $u->delete();
1✔
5994

5995
            $count++;
1✔
5996

5997
            if ($count % 1000 == 0) {
1✔
5998
                error_log("...delete $count / $total");
×
5999
            }
6000

6001

6002
            # Prod garbage collection, as we've seen high memory usage by this.
6003
            User::clearCache();
1✔
6004
            gc_collect_cycles();
1✔
6005
        }
6006

6007
        return ($count);
1✔
6008
    }
6009

6010
    public function recordActive()
6011
    {
6012
        # We record this on an hourly basis.  Avoid pointless mod ops for cluster health.
6013
        $now = date("Y-m-d H:00:00", time());
2✔
6014
        $already = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ? AND timestamp = ?;", [
2✔
6015
            $this->id,
2✔
6016
            $now
2✔
6017
        ]);
2✔
6018

6019
        if (count($already) == 0) {
2✔
6020
            $this->dbhm->background("INSERT IGNORE INTO users_active (userid, timestamp) VALUES ({$this->id}, '$now');");
2✔
6021
        }
6022
    }
6023

6024
    public function getActive()
6025
    {
6026
        $active = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ?;", [$this->id]);
1✔
6027
        return ($active);
1✔
6028
    }
6029

6030
    public function mostActive($gid, $limit = 20)
6031
    {
6032
        $limit = intval($limit);
1✔
6033
        $earliest = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6034

6035
        $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✔
6036
            $gid,
1✔
6037
            User::SYSTEMROLE_USER,
1✔
6038
            $earliest
1✔
6039
        ]);
1✔
6040

6041
        $ret = [];
1✔
6042

6043
        foreach ($users as $user) {
1✔
6044
            $u = User::get($this->dbhr, $this->dbhm, $user['userid']);
1✔
6045
            $thisone = $u->getPublic();
1✔
6046
            $thisone['groupid'] = $gid;
1✔
6047
            $thisone['email'] = $u->getEmailPreferred();
1✔
6048

6049
            if (Utils::pres('memberof', $thisone)) {
1✔
6050
                foreach ($thisone['memberof'] as $group) {
1✔
6051
                    if ($group['id'] == $gid) {
1✔
6052
                        $thisone['joined'] = $group['added'];
1✔
6053
                    }
6054
                }
6055
            }
6056

6057
            $ret[] = $thisone;
1✔
6058
        }
6059

6060
        return ($ret);
1✔
6061
    }
6062

6063
    public function formatPhone($num)
6064
    {
6065
        $num = str_replace(' ', '', $num);
11✔
6066
        $num = preg_replace('/^(\+)?[04]+([^4])/', '$2', $num);
11✔
6067

6068
        if (substr($num, 0, 1) ==  '0') {
11✔
6069
            $num = substr($num, 1);
×
6070
        }
6071

6072
        $num = "+44$num";
11✔
6073

6074
        return ($num);
11✔
6075
    }
6076

6077
    public function sms($msg, $url, $from = TWILIO_FROM, $sid = TWILIO_SID, $auth = TWILIO_AUTH, $forcemsg = NULL)
6078
    {
6079
        # We only want to send SMS to people who are clicking on the links.  So if we've sent them one and they've
6080
        # not clicked on it, we stop.  This saves significant amounts of money.
6081
        $phones = $this->dbhr->preQuery("SELECT * FROM users_phones WHERE userid = ? AND valid = 1 AND (lastsent IS NULL OR (lastclicked IS NOT NULL AND DATE(lastclicked) >= DATE(lastsent)));", [
19✔
6082
            $this->id
19✔
6083
        ]);
19✔
6084

6085
        foreach ($phones as $phone) {
19✔
6086
            try {
6087
                $last = Utils::presdef('lastsent', $phone, NULL);
2✔
6088
                $last = $last ? strtotime($last) : NULL;
2✔
6089

6090
                # Only send one SMS per day.  This keeps the cost down.
6091
                if ($forcemsg || !$last || (time() - $last > 24 * 60 * 60)) {
2✔
6092
                    $client = new Client($sid, $auth);
2✔
6093

6094
                    $text = $forcemsg ? $forcemsg : "$msg Click $url Don't reply to this text.  No more texts sent today.";
2✔
6095
                    $rsp = $client->messages->create(
2✔
6096
                        $this->formatPhone($phone['number']),
2✔
6097
                        array(
2✔
6098
                            'from' => $from,
2✔
6099
                            'body' => $text,
2✔
6100
                            'statusCallback' => 'https://' . USER_SITE . '/twilio/status.php'
2✔
6101
                        )
2✔
6102
                    );
2✔
6103

6104
                    $this->dbhr->preExec("UPDATE users_phones SET lastsent = NOW(), count = count + 1, lastresponse = ? WHERE id = ?;", [
1✔
6105
                        $rsp->sid,
1✔
6106
                        $phone['id']
1✔
6107
                    ]);
1✔
6108
                    error_log("Sent SMS to {$phone['number']} result {$rsp->sid}");
1✔
6109
                } else {
6110
                    error_log("Don't send SMS to {$phone['number']}, too recent");
1✔
6111
                }
6112
            } catch (\Exception $e) {
2✔
6113
                error_log("Send to {$phone['number']} failed with " . $e->getMessage());
2✔
6114
                $this->dbhr->preExec("UPDATE users_phones SET lastsent = NOW(), lastresponse = ? WHERE id = ?;", [
2✔
6115
                    $e->getMessage(),
2✔
6116
                    $phone['id']
2✔
6117
                ]);
2✔
6118
            }
6119

6120
        }
6121
    }
6122

6123
    public function addPhone($phone)
6124
    {
6125
        $this->dbhm->preExec("REPLACE INTO users_phones (userid, number, valid) VALUES (?, ?, 1);", [
10✔
6126
            $this->id,
10✔
6127
            $this->formatPhone($phone),
10✔
6128
        ]);
10✔
6129

6130
        return($this->dbhm->lastInsertId());
10✔
6131
    }
6132

6133
    public function removePhone()
6134
    {
6135
        $this->dbhm->preExec("DELETE FROM users_phones WHERE userid = ?;", [
2✔
6136
            $this->id
2✔
6137
        ]);
2✔
6138
    }
6139

6140
    public function getPhone()
6141
    {
6142
        $ret = NULL;
23✔
6143
        $phones = $this->dbhr->preQuery("SELECT *, DATE(lastclicked) AS lastclicked, DATE(lastsent) AS lastsent FROM users_phones WHERE userid = ?;", [
23✔
6144
            $this->id
23✔
6145
        ]);
23✔
6146

6147
        foreach ($phones as $phone) {
23✔
6148
            $ret = [ $phone['number'], Utils::ISODate($phone['lastsent']), Utils::ISODate($phone['lastclicked']) ];
2✔
6149
        }
6150

6151
        return ($ret);
23✔
6152
    }
6153

6154
    public function setAboutMe($text) {
6155
        $this->dbhm->preExec("INSERT INTO users_aboutme (userid, text) VALUES (?, ?);", [
3✔
6156
            $this->id,
3✔
6157
            $text
3✔
6158
        ]);
3✔
6159

6160
        $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id = {$this->id};");
3✔
6161

6162
        return($this->dbhm->lastInsertId());
3✔
6163
    }
6164

6165
    public function rate($rater, $ratee, $rating, $reason = NULL, $text = NULL) {
6166
        $ret = NULL;
2✔
6167

6168
        if ($rater != $ratee) {
2✔
6169
            # Can't rate yourself.
6170
            $review = $rating == User::RATING_DOWN && $reason && $text;
2✔
6171
            $this->dbhm->preExec("REPLACE INTO ratings (rater, ratee, rating, reason, text, timestamp, reviewrequired) VALUES (?, ?, ?, ?, ?, NOW(), ?);", [
2✔
6172
                $rater,
2✔
6173
                $ratee,
2✔
6174
                $rating,
2✔
6175
                $reason,
2✔
6176
                $text,
2✔
6177
                $review
2✔
6178
            ]);
2✔
6179

6180
            $ret = $this->dbhm->lastInsertId();
2✔
6181

6182
            $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id IN ($rater, $ratee);");
2✔
6183
        }
6184

6185
        return($ret);
2✔
6186
    }
6187

6188
    public function getRatings($uids) {
6189
        $mysqltime = date("Y-m-d", strtotime("Midnight 182 days ago"));
124✔
6190
        $ret = [];
124✔
6191
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
124✔
6192
        $myid = $me ? $me->getId() : NULL;
124✔
6193

6194
        # We show visible ratings, ones we have made ourselves, or those from TN.
6195
        $sql = "SELECT ratee, COUNT(*) AS count, rating FROM ratings WHERE ratee IN (" . implode(',', $uids) . ") AND (timestamp >= '$mysqltime' OR rater = ?) AND (tn_rating_id IS NOT NULL OR rater = ? OR visible = 1) GROUP BY rating, ratee;";
124✔
6196
        $ratings = $this->dbhr->preQuery($sql, [ $myid, $myid ]);
124✔
6197

6198
        foreach ($uids as $uid) {
124✔
6199
            $ret[$uid] = [
124✔
6200
                User::RATING_UP => 0,
124✔
6201
                User::RATING_DOWN => 0,
124✔
6202
                User::RATING_MINE => NULL
124✔
6203
            ];
124✔
6204

6205
            foreach ($ratings as $rate) {
124✔
6206
                if ($rate['ratee'] == $uid) {
1✔
6207
                    $ret[$uid][$rate['rating']] = $rate['count'];
1✔
6208
                }
6209
            }
6210
        }
6211

6212
        $ratings = $this->dbhr->preQuery("SELECT rating, ratee FROM ratings WHERE ratee IN (" . implode(',', $uids) . ") AND rater = ? AND timestamp >= '$mysqltime';", [
124✔
6213
            $myid
124✔
6214
        ]);
124✔
6215

6216
        foreach ($uids as $uid) {
124✔
6217
            if ($myid != $this->id) {
124✔
6218
                # We can't rate ourselves, so don't bother checking.
6219

6220
                foreach ($ratings as $rating) {
80✔
6221
                    if ($rating['ratee'] == $uid) {
1✔
6222
                        $ret[$uid][User::RATING_MINE] = $rating['rating'];
1✔
6223
                    }
6224
                }
6225
            }
6226
        }
6227

6228
        return($ret);
124✔
6229
    }
6230

6231
    public function getAllRatings($since) {
6232
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6233

6234
        $sql = "SELECT * FROM ratings WHERE timestamp >= ? AND visible = 1;";
1✔
6235
        $ratings = $this->dbhr->preQuery($sql, [
1✔
6236
            $mysqltime
1✔
6237
        ]);
1✔
6238

6239
        foreach ($ratings as &$rating) {
1✔
6240
            $rating['timestamp'] = Utils::ISODate($rating['timestamp']);
1✔
6241
        }
6242

6243
        return $ratings;
1✔
6244
    }
6245

6246
    public function getVisibleRatings($unreviewedonly, $since = '7 days ago') {
6247
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
3✔
6248
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
6249

6250
        $modships = $me->getModeratorships(NULL, TRUE);
3✔
6251

6252
        $ret = [];
3✔
6253
        $revq = $unreviewedonly ? " AND reviewrequired = 1" : '';
3✔
6254

6255
        if (count($modships)) {
3✔
6256
            $sql = "SELECT ratings.*, m1.groupid,
2✔
6257
       CASE WHEN u1.fullname IS NOT NULL THEN u1.fullname ELSE CONCAT(u1.firstname, ' ', u1.lastname) END AS raterdisplayname,
6258
       CASE WHEN u2.fullname IS NOT NULL THEN u2.fullname ELSE CONCAT(u2.firstname, ' ', u2.lastname) END AS rateedisplayname
6259
    FROM ratings 
6260
    INNER JOIN memberships m1 ON m1.userid = ratings.rater
6261
    INNER JOIN memberships m2 ON m2.userid = ratings.ratee
6262
    INNER JOIN users u1 ON ratings.rater = u1.id
6263
    INNER JOIN users u2 ON ratings.ratee = u2.id
6264
    WHERE ratings.timestamp >= ? AND 
6265
        m1.groupid IN (" . implode(',', $modships) . ") AND
2✔
6266
        m2.groupid IN (" . implode(',', $modships) . ") AND
2✔
6267
        m1.groupid = m2.groupid AND
6268
        ratings.rating IS NOT NULL 
6269
        $revq    
2✔
6270
        GROUP BY ratings.rater ORDER BY ratings.timestamp DESC;";
2✔
6271

6272
            $ret = $this->dbhr->preQuery($sql, [
2✔
6273
                $mysqltime
2✔
6274
            ]);
2✔
6275

6276
            foreach ($ret as &$r) {
2✔
6277
                $r['timestamp'] = Utils::ISODate($r['timestamp']);
1✔
6278
            }
6279
        }
6280

6281
        return $ret;
3✔
6282
    }
6283

6284
    public function ratingReviewed($ratingid) {
6285
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6286

6287
        $unreviewed = $me->getVisibleRatings(TRUE);
1✔
6288

6289
        foreach ($unreviewed as $r) {
1✔
6290
            if ($r['id'] == $ratingid) {
1✔
6291
                $this->dbhm->preExec("UPDATE ratings SET reviewrequired = 0 WHERE id = ?;", [
1✔
6292
                    $ratingid
1✔
6293
                ]);
1✔
6294
            }
6295
        }
6296
    }
6297

6298
    public function getChanges($since) {
6299
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6300

6301
        $users = $this->dbhr->preQuery("SELECT id, lastupdated FROM users WHERE lastupdated >= ?;", [
1✔
6302
            $mysqltime
1✔
6303
        ]);
1✔
6304

6305
        foreach ($users as &$user) {
1✔
6306
            $user['lastupdated'] = Utils::ISODate($user['lastupdated']);
1✔
6307
        }
6308

6309
        return $users;
1✔
6310
    }
6311

6312
    public function getRated() {
6313
        $rateds = $this->dbhr->preQuery("SELECT * FROM ratings WHERE rater = ?;", [
8✔
6314
            $this->id
8✔
6315
        ]);
8✔
6316

6317
        foreach ($rateds as &$rate) {
8✔
6318
            $rate['timestamp'] = Utils::ISODate($rate['timestamp']);
1✔
6319
        }
6320

6321
        return($rateds);
8✔
6322
    }
6323

6324
    public function getActiveSince($since, $createdbefore, $uid = NULL) {
6325
        $sincetime = date("Y-m-d H:i:s", strtotime($since));
1✔
6326
        $beforetime = date("Y-m-d H:i:s", strtotime($createdbefore));
1✔
6327
        $ids = $uid ? [
1✔
6328
            [
1✔
6329
                'id' => $uid
1✔
6330
            ]
1✔
6331
        ] : $this->dbhr->preQuery("SELECT id FROM users WHERE lastaccess >= ? AND added <= ?;", [
1✔
6332
            $sincetime,
1✔
6333
            $beforetime
1✔
6334
        ]);
1✔
6335

6336
        return(count($ids) ? array_filter(array_column($ids, 'id')) : []);
1✔
6337
    }
6338

6339
    public static function encodeId($id) {
6340
        # We're told that this is affecting our spam rating.  Let's see.
6341
        return '';
9✔
6342
//        $bin = base_convert($id, 10, 2);
6343
//        $bin = str_replace('0', '-', $bin);
6344
//        $bin = str_replace('1', '~', $bin);
6345
//        return($bin);
6346
    }
6347

6348
    public static function decodeId($enc) {
6349
        $enc = trim($enc);
×
6350
        $enc = str_replace('-', '0', $enc);
×
6351
        $enc = str_replace('~', '1', $enc);
×
6352
        $id  = base_convert($enc, 2, 10);
×
6353
        return($id);
×
6354
    }
6355

6356
    public function getCity()
6357
    {
6358
        $city = NULL;
25✔
6359

6360
        # Find the closest town
6361
        list ($lat, $lng, $loc) = $this->getLatLng(FALSE, TRUE);
25✔
6362

6363
        if ($lat || $lng) {
25✔
6364
            $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✔
6365
            #error_log("Get $sql, $lng, $lat");
6366
            $towns = $this->dbhr->preQuery($sql);
1✔
6367

6368
            foreach ($towns as $town) {
1✔
6369
                $city = $town['name'];
1✔
6370
            }
6371
        }
6372

6373
        return([ $city, $lat, $lng ]);
25✔
6374
    }
6375

6376
    public function microVolunteering() {
6377
        // Are we on a group where microvolunteering is enabled.
6378
        $groups = $this->dbhr->preQuery("SELECT memberships.id FROM memberships INNER JOIN `groups` ON groups.id = memberships.groupid WHERE userid = ? AND microvolunteering = 1 LIMIT 1;", [
24✔
6379
            $this->id
24✔
6380
        ]);
24✔
6381

6382
        return count($groups);
24✔
6383
    }
6384

6385
    public function getJobAds() {
6386
        # We want to show a few job ads from nearby.
6387
        $search = NULL;
34✔
6388
        $ret = '<span class="jobads">';
34✔
6389

6390
        list ($lat, $lng) = $this->getLatLng();
34✔
6391

6392
        if ($lat || $lng) {
34✔
6393
            $j = new Jobs($this->dbhr, $this->dbhm);
5✔
6394
            $jobs = $j->query($lat, $lng, 4);
5✔
6395

6396
            foreach ($jobs as $job) {
5✔
6397
                $loc = Utils::presdef('location', $job, '');
3✔
6398
                $title = "{$job['title']}" . ($loc !== ' ' ? " ($loc)" : '');
3✔
6399

6400
                # Link via our site to avoid spam trap warnings.
6401
                $url = "https://" . USER_SITE . "/job/{$job['id']}";
3✔
6402
                $ret .= '<a href="' . $url . '" target="_blank" style="color:black; font-weight:bold;">' . htmlentities($title) . '</a><br />';
3✔
6403
            }
6404
        }
6405

6406
        $ret .= '</span>';
34✔
6407

6408
        return([
34✔
6409
            'location' => $search,
34✔
6410
            'jobs' => $ret
34✔
6411
        ]);
34✔
6412
    }
6413

6414
    public function updateModMails($uid = NULL) {
6415
        # We maintain a count of recent modmails by scanning logs regularly, and pruning old ones.  This means we can
6416
        # find the value in a well-indexed way without the disk overhead of having a two-column index on logs.
6417
        #
6418
        # Ignore logs where the user is the same as the byuser - for example a user can delete their own posts, and we are
6419
        # only interested in things where a mod has done something to another user.
6420
        $mysqltime = date("Y-m-d H:i:s", strtotime("10 minutes ago"));
1✔
6421
        $uidq = $uid ? " AND user = $uid " : '';
1✔
6422

6423
        $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✔
6424
            $mysqltime
1✔
6425
        ]);
1✔
6426

6427
        foreach ($logs as $log) {
1✔
6428
            $this->dbhm->preExec("INSERT IGNORE INTO users_modmails (userid, logid, timestamp, groupid) VALUES (?,?,?,?);", [
1✔
6429
                $log['user'],
1✔
6430
                $log['id'],
1✔
6431
                $log['timestamp'],
1✔
6432
                $log['groupid']
1✔
6433
            ]);
1✔
6434
        }
6435

6436
        # Prune old ones.
6437
        $mysqltime = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6438
        $uidq2 = $uid ? " AND userid = $uid " : '';
1✔
6439

6440
        $logs = $this->dbhr->preQuery("SELECT id FROM users_modmails WHERE timestamp < ? $uidq2;", [
1✔
6441
            $mysqltime
1✔
6442
        ]);
1✔
6443

6444
        foreach ($logs as $log) {
1✔
6445
            $this->dbhm->preExec("DELETE FROM users_modmails WHERE id = ?;", [ $log['id'] ], FALSE);
×
6446
        }
6447
    }
6448

6449
    public function getModGroupsByActivity() {
6450
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
6451
        $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✔
6452
        return $this->dbhr->preQuery($sql, [
1✔
6453
            $this->id
1✔
6454
        ]);
1✔
6455
    }
6456

6457
    public function related($userlist) {
6458
        $userlist = array_unique($userlist);
2✔
6459

6460
        foreach ($userlist as $user1) {
2✔
6461
            foreach ($userlist as $user2) {
2✔
6462
                if ($user1 && $user2 && $user1 !== $user2) {
2✔
6463
                    # We may be passed user ids which no longer exist.
6464
                    $u1 = User::get($this->dbhr, $this->dbhm, $user1);
2✔
6465
                    $u2 = User::get($this->dbhr, $this->dbhm, $user2);
2✔
6466

6467
                    if ($u1->getId() && $u2->getId() && !$u1->isAdminOrSupport() && !$u2->isAdminOrSupport()) {
2✔
6468
                        $this->dbhm->background("INSERT INTO users_related (user1, user2) VALUES ($user1, $user2) ON DUPLICATE KEY UPDATE timestamp = NOW();");
2✔
6469
                    }
6470
                }
6471
            }
6472
        }
6473
    }
6474

6475
    public function getRelated($userid, $since = "30 days ago") {
6476
        $starttime = date("Y-m-d H:i:s", strtotime($since));
1✔
6477
        $users = $this->dbhr->preQuery("SELECT * FROM users_related WHERE user1 = ? AND timestamp >= '$starttime';", [
1✔
6478
            $userid
1✔
6479
        ]);
1✔
6480

6481
        return ($users);
1✔
6482
    }
6483

6484
    public function listRelated($groupids, &$ctx, $limit = 10) {
6485
        # The < condition ensures we don't duplicate during a single run.
6486
        $limit = intval($limit);
1✔
6487
        $ret = [];
1✔
6488
        $backstop = 100;
1✔
6489

6490
        do {
6491
            $ctx = $ctx ? $ctx : [ 'id'  => NULL ];
1✔
6492

6493
            if ($groupids && count($groupids)) {
1✔
6494
                $ctxq = ($ctx && intval($ctx['id'])) ? (" WHERE id < " . intval($ctx['id'])) : '';
1✔
6495
                $groupq = "(" . implode(',', $groupids) . ")";
1✔
6496
                $sql = "SELECT DISTINCT id, user1, user2 FROM (
1✔
6497
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6498
INNER JOIN memberships ON users_related.user1 = memberships.userid 
6499
INNER JOIN users u1 ON users_related.user1 = u1.id AND u1.deleted IS NULL AND u1.systemrole = 'User'
6500
WHERE 
6501
user1 < user2 AND
6502
notified = 0 AND
6503
memberships.groupid IN $groupq UNION
1✔
6504
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6505
INNER JOIN memberships ON users_related.user2 = memberships.userid 
6506
INNER JOIN users u2 ON users_related.user2 = u2.id AND u2.deleted IS NULL AND u2.systemrole = 'User'
6507
WHERE 
6508
user1 < user2 AND
6509
notified = 0 AND
6510
memberships.groupid IN $groupq 
1✔
6511
) t $ctxq ORDER BY id DESC LIMIT $limit;";
1✔
6512
                $members = $this->dbhr->preQuery($sql);
1✔
6513
            } else {
6514
                $ctxq = ($ctx && intval($ctx['id'])) ? (" AND users_related.id < " . intval($ctx['id'])) : '';
1✔
6515
                $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✔
6516
                $members = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
1✔
6517
            }
6518

6519
            $uids1 = array_column($members, 'user1');
1✔
6520
            $uids2 = array_column($members, 'user2');
1✔
6521

6522
            $related = [];
1✔
6523
            foreach ($members as $member) {
1✔
6524
                $related[$member['user1']] = $member['user2'];
1✔
6525
                $ctx['id'] = $member['id'];
1✔
6526
            }
6527

6528
            $users = $this->getPublicsById(array_merge($uids1, $uids2));
1✔
6529

6530
            foreach ($users as &$user1) {
1✔
6531
                if (Utils::pres($user1['id'], $related)) {
1✔
6532
                    $thisone = $user1;
1✔
6533

6534
                    foreach ($users as $user2) {
1✔
6535
                        if ($user2['id'] == $related[$user1['id']]) {
1✔
6536
                            $user2['userid'] = $user2['id'];
1✔
6537
                            $thisone['relatedto'] = $user2;
1✔
6538
                            break;
1✔
6539
                        }
6540
                    }
6541

6542
                    $logins = $this->getLogins(FALSE, $thisone['id'], TRUE);
1✔
6543
                    $rellogins = $this->getLogins(FALSE, $thisone['relatedto']['id'], TRUE);
1✔
6544

6545
                    if ($thisone['deleted'] ||
1✔
6546
                        $thisone['relatedto']['deleted'] ||
1✔
6547
                        $thisone['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6548
                        $thisone['relatedto']['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6549
                        !count($logins) ||
1✔
6550
                        !count($rellogins)) {
1✔
6551
                        # No sense in telling people about these.
6552
                        #
6553
                        # If there are n valid login types for one of the users - no way they can log in again so no point notifying.
6554
                        $this->dbhm->preExec("UPDATE users_related SET notified = 1 WHERE (user1 = ? AND user2 = ?) OR (user1 = ? AND user2 = ?);", [
1✔
6555
                            $thisone['id'],
1✔
6556
                            $thisone['relatedto']['id'],
1✔
6557
                            $thisone['relatedto']['id'],
1✔
6558
                            $thisone['id']
1✔
6559
                        ]);
1✔
6560
                    } else {
6561
                        $thisone['userid'] = $thisone['id'];
1✔
6562
                        $thisone['logins'] = $logins;
1✔
6563
                        $thisone['relatedto']['logins'] = $rellogins;
1✔
6564

6565
                        $ret[] = $thisone;
1✔
6566
                    }
6567
                }
6568
            }
6569

6570
            $backstop--;
1✔
6571
        } while ($backstop > 0 && count($ret) < $limit && count($members));
1✔
6572

6573
        return $ret;
1✔
6574
    }
6575

6576
    public function getExpectedReplies($uids, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6577
        # We count replies where the user has been active since the reply was requested, which means they've had
6578
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6579
        #
6580
        # $since here has to match the value in ChatRoom::
6581
        $starttime = date("Y-m-d H:i:s", strtotime($since));
124✔
6582
        $replies = $this->dbhr->preQuery("SELECT COUNT(*) AS count, expectee FROM users_expected INNER JOIN users ON users.id = users_expected.expectee INNER JOIN chat_messages ON chat_messages.id = users_expected.chatmsgid WHERE expectee IN (" . implode(',', $uids) . ") AND chat_messages.date >= '$starttime' AND replyexpected = 1 AND replyreceived = 0 AND TIMESTAMPDIFF(MINUTE, chat_messages.date, users.lastaccess) >= ?", [
124✔
6583
            $grace
124✔
6584
        ]);
124✔
6585

6586
        return($replies);
124✔
6587
    }
6588

6589
    public function listExpectedReplies($uid, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6590
        # We count replies where the user has been active since the reply was requested, which means they've had
6591
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6592
        #
6593
        # $since here has to match the value in ChatRoom::
6594
        $starttime = date("Y-m-d H:i:s", strtotime($since));
23✔
6595
        $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) > ?", [
23✔
6596
            $uid,
23✔
6597
            $grace
23✔
6598
        ]);
23✔
6599

6600
        $ret = [];
23✔
6601

6602
        if (count($replies)) {
23✔
6603
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6604
            $myid = $me ? $me->getId() : NULL;
1✔
6605

6606
            $r = new ChatRoom($this->dbhr, $this->dbhm);
1✔
6607
            $rooms = $r->fetchRooms(array_column($replies, 'chatid'), $myid, TRUE);
1✔
6608

6609
            foreach ($rooms as $room) {
1✔
6610
                $ret[] = [
1✔
6611
                    'id' => $room['id'],
1✔
6612
                    'name' => $room['name']
1✔
6613
                ];
1✔
6614
            }
6615
        }
6616

6617
        return $ret;
23✔
6618
    }
6619
    
6620
    public function getWorkCounts($groups = NULL) {
6621
        # Tell them what mod work there is.  Similar code in Notifications.
6622
        $ret = [];
28✔
6623
        $total = 0;
28✔
6624

6625
        $national = $this->hasPermission(User::PERM_NATIONAL_VOLUNTEERS);
28✔
6626

6627
        if ($national) {
28✔
6628
            $v = new Volunteering($this->dbhr, $this->dbhm);
1✔
6629
            $ret['pendingvolunteering'] = $v->systemWideCount();
1✔
6630
        }
6631

6632
        $s = new Spam($this->dbhr, $this->dbhm);
28✔
6633
        $spamcounts = $s->collectionCounts();
28✔
6634
        $ret['spammerpendingadd'] = $spamcounts[Spam::TYPE_PENDING_ADD];
28✔
6635
        $ret['spammerpendingremove'] = $spamcounts[Spam::TYPE_PENDING_REMOVE];
28✔
6636

6637
        # Show social actions from last 4 days.
6638
        $ctx = NULL;
28✔
6639
        $f = new GroupFacebook($this->dbhr, $this->dbhm);
28✔
6640
        $ret['socialactions'] = 0; // FB DISABLED = count($f->listSocialActions($ctx,$this));
28✔
6641

6642
        $g = new Group($this->dbhr, $this->dbhm);
28✔
6643
        $ret['popularposts'] = 0; // FB DISABLED count($g->getPopularMessages());
28✔
6644

6645
        if ($this->hasPermission(User::PERM_GIFTAID)) {
28✔
6646
            $d = new Donations($this->dbhr, $this->dbhm);
1✔
6647
            $ret['giftaid'] = $d->countGiftAidReview();
1✔
6648
        }
6649

6650
        if (!$groups) {
28✔
6651
            $groups = $this->getMemberships(FALSE, NULL, TRUE, TRUE, $this->id, FALSE);
13✔
6652
        }
6653

6654
        foreach ($groups as &$group) {
28✔
6655
            if (Utils::pres('work', $group)) {
20✔
6656
                foreach ($group['work'] as $key => $work) {
18✔
6657
                    if (Utils::pres($key, $ret)) {
18✔
6658
                        $ret[$key] += $work;
2✔
6659
                    } else {
6660
                        $ret[$key] = $work;
18✔
6661
                    }
6662
                }
6663
            }
6664
        }
6665

6666
        $s = new Story($this->dbhr, $this->dbhm);
28✔
6667
        $ret['stories'] = $s->getReviewCount(FALSE, $this, $groups);
28✔
6668
        $ret['newsletterstories'] = $this->hasPermission(User::PERM_NEWSLETTER) ? $s->getReviewCount(TRUE) : 0;
28✔
6669

6670
        // All the types of work which are worth nagging about.
6671
        $worktypes = [
28✔
6672
            'pendingvolunteering',
28✔
6673
            'socialactions',
28✔
6674
            'popularposts',
28✔
6675
            'chatreview',
28✔
6676
            'relatedmembers',
28✔
6677
            'stories',
28✔
6678
            'newsletterstories',
28✔
6679
            'pending',
28✔
6680
            'spam',
28✔
6681
            'pendingmembers',
28✔
6682
            'pendingevents',
28✔
6683
            'spammembers',
28✔
6684
            'editreview',
28✔
6685
            'pendingadmins'
28✔
6686
        ];
28✔
6687

6688
        if ($this->isAdminOrSupport()) {
28✔
6689
            $worktypes[] = 'spammerpendingadd';
2✔
6690
            $worktypes[] = 'spammerpendingremove';
2✔
6691
        }
6692

6693
        foreach ($worktypes as $key) {
28✔
6694
            $total += Utils::presdef($key, $ret, 0);
28✔
6695
        }
6696

6697
        $ret['total'] = $total;
28✔
6698

6699
        return $ret;
28✔
6700
    }
6701

6702
    public function ratingVisibility($since = "1 hour ago") {
6703
        $mysqltime = date("Y-m-d", strtotime($since));
1✔
6704

6705
        $ratings = $this->dbhr->preQuery("SELECT * FROM ratings WHERE timestamp >= ?;", [
1✔
6706
            $mysqltime
1✔
6707
        ]);
1✔
6708

6709
        foreach ($ratings as $rating) {
1✔
6710
            # A rating is visible to others if there is a chat between the two members, and
6711
            # - the ratee replied to a post, or
6712
            # - there is at least one message from each of them.
6713
            # This means that has been an exchange substantial enough for the rating not to just be frivolous.  It
6714
            # deliberately excludes interactions on ChitChat, where we have seen some people go a bit overboard on
6715
            # rating people.
6716
            $visible = FALSE;
1✔
6717
            #error_log("Check {$rating['rater']} rating of {$rating['ratee']}");
6718

6719
            $chats = $this->dbhr->preQuery("SELECT id FROM chat_rooms WHERE (user1 = ? AND user2 = ?) OR (user2 = ? AND user1 = ?)", [
1✔
6720
                $rating['rater'],
1✔
6721
                $rating['ratee'],
1✔
6722
                $rating['rater'],
1✔
6723
                $rating['ratee'],
1✔
6724
            ]);
1✔
6725

6726
            foreach ($chats as $chat) {
1✔
6727
                $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✔
6728
                    $chat['id']
1✔
6729
                ]);
1✔
6730

6731
                if ($distincts[0]['count'] >= 2) {
1✔
6732
                    #error_log("At least one real message from each of them in {$chat['id']}");
6733
                    $visible = TRUE;
1✔
6734
                } else {
6735
                    $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✔
6736
                        $chat['id'],
1✔
6737
                        $rating['ratee']
1✔
6738
                    ]);
1✔
6739

6740
                    if ($replies[0]['count']) {
1✔
6741
                        #error_log("Significant reply from {$rating['ratee']} in {$chat['id']}");
6742
                        $visible = TRUE;
1✔
6743
                    }
6744
                }
6745
            }
6746

6747
            #error_log("Use {$rating['rating']} from {$rating['rater']} ? " . ($visible ? 'yes': 'no'));
6748
            $oldvisible = intval($rating['visible']) ? TRUE : FALSE;
1✔
6749

6750
            if ($visible != $oldvisible) {
1✔
6751
                $this->dbhm->preExec("UPDATE ratings SET visible = ?, timestamp = NOW() WHERE id = ?;", [
1✔
6752
                    $visible,
1✔
6753
                    $rating['id']
1✔
6754
                ]);
1✔
6755
            }
6756
        }
6757
    }
6758

6759
    public function unban($groupid) {
6760
        $this->dbhm->preExec("DELETE FROM users_banned WHERE userid = ? AND groupid = ?;", [
4✔
6761
            $this->id,
4✔
6762
            $groupid
4✔
6763
        ]);
4✔
6764
    }
6765

6766
    public function hasFacebookLogin() {
6767
        $logins = $this->getLogins();
4✔
6768
        $ret = FALSE;
4✔
6769

6770
        foreach ($logins as $login) {
4✔
6771
            if ($login['type'] == User::LOGIN_FACEBOOK) {
4✔
6772
                $ret = TRUE;
1✔
6773
            }
6774
        }
6775

6776
        return $ret;
4✔
6777
    }
6778

6779
    public function memberReview($groupid, $request, $reason) {
6780
        $mysqltime = date('Y-m-d H:i');
6✔
6781

6782
        if ($request) {
6✔
6783
            # Requesting review.  Leave reviewedat unchanged, so that we can use it to avoid asking too
6784
            # frequently.
6785
            $this->setMembershipAtt($groupid, 'reviewreason', $reason);
4✔
6786
            $this->setMembershipAtt($groupid, 'reviewrequestedat', $mysqltime);
4✔
6787
        } else {
6788
            # We have reviewed.  Note that they might have been removed, in which case the set will do nothing.
6789
            $this->setMembershipAtt($groupid, 'reviewrequestedat', NULL);
3✔
6790
            $this->setMembershipAtt($groupid, 'reviewedat', $mysqltime);
3✔
6791
        }
6792
    }
6793

6794
    private function checkSupporterSettings($settings) {
6795
        $ret = TRUE;
79✔
6796

6797
        if ($settings) {
79✔
6798
            $s = json_decode($settings, TRUE);
15✔
6799

6800
            if ($s && array_key_exists('hidesupporter', $s)) {
15✔
6801
                $ret = !$s['hidesupporter'];
1✔
6802
            }
6803
        }
6804

6805
        return $ret;
79✔
6806
    }
6807

6808
    public function getSupporters(&$rets, $users) {
6809
        $idsleft = [];
288✔
6810

6811
        foreach ($rets as $userid => $ret) {
288✔
6812
            if (Utils::pres($userid, $users)) {
255✔
6813
                if (array_key_exists('supporter', $users[$userid])) {
11✔
6814
                    $rets[$userid]['supporter'] = $users[$userid]['supporter'];
3✔
6815
                    $rets[$userid]['donor'] = Utils::presdef('donor', $users[$userid], FALSE);
11✔
6816
                }
6817
            } else {
6818
                $idsleft[] = $userid;
250✔
6819
            }
6820
        }
6821

6822
        if (count($idsleft)) {
288✔
6823
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
250✔
6824
            $myid = $me ? $me->getId() : null;
250✔
6825

6826
            # A supporter is a mod, someone who has donated recently, or done microvolunteering recently.
6827
            if (count($idsleft)) {
250✔
6828
                $start = date('Y-m-d', strtotime("360 days ago"));
250✔
6829
                $info = $this->dbhr->preQuery(
250✔
6830
                    "SELECT DISTINCT users.id AS userid, settings, systemrole FROM users 
250✔
6831
    LEFT JOIN microactions ON users.id = microactions.userid
6832
    LEFT JOIN users_donations ON users_donations.userid = users.id 
6833
    WHERE users.id IN (" . implode(
250✔
6834
                        ',',
250✔
6835
                        $idsleft
250✔
6836
                    ) . ") AND 
250✔
6837
                    (systemrole IN (?, ?, ?) OR microactions.timestamp >= ? OR users_donations.timestamp >= ?);",
250✔
6838
                    [
250✔
6839
                        User::SYSTEMROLE_ADMIN,
250✔
6840
                        User::SYSTEMROLE_SUPPORT,
250✔
6841
                        User::SYSTEMROLE_MODERATOR,
250✔
6842
                        $start,
250✔
6843
                        $start
250✔
6844
                    ]
250✔
6845
                );
250✔
6846

6847
                $found = [];
250✔
6848

6849
                foreach ($info as $i) {
250✔
6850
                    $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
79✔
6851
                    $found[] = $i['userid'];
79✔
6852
                }
6853

6854
                $left = array_diff($idsleft, $found);
250✔
6855

6856
                # If we are one of the users, then we want to return whether we are a donor.
6857
                if (in_array($myid, $idsleft)) {
250✔
6858
                    $left[] = $myid;
144✔
6859
                    $left = array_filter(array_unique($left));
144✔
6860
                }
6861

6862
                if (count($left)) {
250✔
6863
                    $info = $this->dbhr->preQuery(
248✔
6864
                        "SELECT userid, settings, TransactionType FROM users_donations INNER JOIN users ON users_donations.userid = users.id WHERE users_donations.timestamp >= ? AND users_donations.userid IN (" . implode(
248✔
6865
                            ',',
248✔
6866
                            $left
248✔
6867
                        ) . ") GROUP BY TransactionType;",
248✔
6868
                        [
248✔
6869
                            $start
248✔
6870
                        ]
248✔
6871
                    );
248✔
6872

6873
                    foreach ($info as $i) {
248✔
6874
                        $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
3✔
6875

6876
                        if ($i['userid'] == $myid) {
3✔
6877
                            # Only return this info for ourselves, otherwise it's a privacy leak.
6878
                            $rets[$i['userid']]['donor'] = TRUE;
3✔
6879

6880
                            if ($i['TransactionType'] == 'subscr_payment' || $i['TransactionType'] == 'recurring_payment') {
3✔
6881
                                $rets[$i['userid']]['donorrecurring'] = TRUE;
1✔
6882
                            }
6883
                        }
6884
                    }
6885
                }
6886
            }
6887
        }
6888
    }
6889

6890
    public function obfuscateEmail($email) {
6891
        $p = strpos($email, '@');
2✔
6892
        $q = strpos($email, 'privaterelay.appleid.com');
2✔
6893

6894
        if ($q) {
2✔
6895
            $email = 'Your Apple ID';
1✔
6896
        } else {
6897
            # For very short emails, we just show the first character.
6898
            if ($p <= 3) {
2✔
6899
                $email = substr($email, 0, 1) . "***" . substr($email, $p);
1✔
6900
            } else if ($p < 10) {
2✔
6901
                $email = substr($email, 0, 1) . "***" . substr($email, $p - 1);
2✔
6902
            } else {
6903
                $email = substr($email, 0, 3) . "***" . substr($email, $p - 3);
1✔
6904
            }
6905
        }
6906

6907
        return $email;
2✔
6908
    }
6909

6910
    public function setSimpleMail($simplemail) {
6911
        $s = $this->getPrivate('settings');
2✔
6912

6913
        if ($s) {
2✔
6914
            $settings = json_decode($s, TRUE);
×
6915
        } else {
6916
            $settings = [];
2✔
6917
        }
6918

6919
        $this->dbhm->beginTransaction();
2✔
6920

6921
        switch ($simplemail) {
6922
            case User::SIMPLE_MAIL_NONE: {
6923
                # No digests, no events/volunteering.
6924
                # No relevant or newsletters.
6925
                # No email notifications.
6926
                # No enagement.
6927
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 0, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
2✔
6928
                    $this->id
2✔
6929
                ]);
2✔
6930

6931
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
2✔
6932
                    $this->id
2✔
6933
                ]);
2✔
6934

6935
                $settings['notifications']['email'] = false;
2✔
6936
                $settings['notifications']['emailmine'] = false;
2✔
6937
                $settings['notificationmails']= false;
2✔
6938
                $settings['engagement'] = false;
2✔
6939
                break;
2✔
6940
            }
6941
            case User::SIMPLE_MAIL_BASIC: {
6942
                # Daily digests, no events/volunteering.
6943
                # No relevant or newsletters.
6944
                # Chat email notifications.
6945
                # No enagement.
6946
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 24, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
1✔
6947
                    $this->id
1✔
6948
                ]);
1✔
6949

6950
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
1✔
6951
                    $this->id
1✔
6952
                ]);
1✔
6953

6954
                $settings['notifications']['email'] = true;
1✔
6955
                $settings['notifications']['emailmine'] = false;
1✔
6956
                $settings['notificationmails']= false;
1✔
6957
                $settings['engagement']= false;
1✔
6958
                break;
1✔
6959
            }
6960
            case User::SIMPLE_MAIL_FULL: {
6961
                # Immediate mails, events/volunteering.
6962
                # Relevant and newsletters.
6963
                # Email notifications.
6964
                # Enagement.
6965
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = -1, eventsallowed = 1, volunteeringallowed = 1 WHERE userid = ?;", [
1✔
6966
                    $this->id
1✔
6967
                ]);
1✔
6968

6969
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 1,  newslettersallowed = 1 WHERE id = ?;", [
1✔
6970
                    $this->id
1✔
6971
                ]);
1✔
6972

6973
                $settings['notifications']['email'] = true;
1✔
6974
                $settings['notifications']['emailmine'] = false;
1✔
6975
                $settings['notificationmails']= true;
1✔
6976
                $settings['engagement']= true;
1✔
6977
                break;
1✔
6978
            }
6979
        }
6980

6981
        $settings['simplemail'] = $simplemail;
2✔
6982

6983
        # Holiday no longer exposed so turn off.
6984
        $this->dbhm->preExec("UPDATE users SET onholidaytill = NULL, settings = ? WHERE id = ?;", [
2✔
6985
            json_encode($settings),
2✔
6986
            $this->id
2✔
6987
        ]);
2✔
6988

6989
        $this->dbhm->commit();
2✔
6990
    }
6991

6992
    public function processMemberships($g = NULL) {
6993
        $memberships = $this->dbhr->preQuery("SELECT id FROM `memberships_history` WHERE processingrequired = 1 ORDER BY id ASC;");
6✔
6994

6995
        foreach ($memberships as $membership) {
6✔
6996
            $this->processMembership($membership['id'], $g);
6✔
6997
        }
6998
    }
6999

7000
    public function processMembership($id, $g) {
7001
        $memberships = $this->dbhr->preQuery("SELECT * FROM memberships_history WHERE id = ?;",[
6✔
7002
            $id
6✔
7003
        ]);
6✔
7004

7005
        foreach ($memberships as $membership) {
6✔
7006
            $groupid = $membership['groupid'];
6✔
7007
            $userid = $membership['userid'];
6✔
7008
            $collection = $membership['collection'];
6✔
7009

7010
            $g = $g ? $g : Group::get($this->dbhr, $this->dbhm, $groupid);
6✔
7011

7012
            # The membership didn't already exist.  We might want to send a welcome mail.
7013
            if ($g->getPrivate('welcomemail') && $collection == MembershipCollection::APPROVED && $g->getPrivate('onhere')) {
6✔
7014
                # They are now approved.  We need to send a per-group welcome mail.
7015
                try {
7016
                    error_log(date("Y-m-d H:i:s") . "Send welcome to $userid for membership $id\n");
2✔
7017
                    $g->sendWelcome($userid, false);
2✔
7018
                } catch (Exception $e) {
×
7019
                    error_log("Welcome failed: " . $e->getMessage());
×
7020
                    \Sentry\captureException($e);
×
7021
                }
7022
            }
7023

7024
            # Check whether this user now counts as a possible spammer.
7025
            $s = new Spam($this->dbhr, $this->dbhm);
6✔
7026
            $s->checkUser($userid, $groupid);
6✔
7027

7028
            # We might have mod notes which require this member to be flagged up.
7029
            $comments = $this->dbhr->preQuery(
6✔
7030
                "SELECT COUNT(*) AS count FROM users_comments WHERE userid = ? AND flag = 1;", [
6✔
7031
                    $userid,
6✔
7032
                ]
6✔
7033
            );
6✔
7034

7035
            if ($comments[0]['count'] > 0) {
6✔
7036
                $this->memberReview($groupid, true, 'Note flagged to other groups');
1✔
7037
            }
7038

7039
            $this->dbhm->preExec("UPDATE memberships_history SET processingrequired = 0 WHERE id = ?", [
6✔
7040
                $id
6✔
7041
            ]);
6✔
7042
        }
7043
    }
7044

7045
    public function getUserKey($id) {
7046
        $key = null;
95✔
7047
        $sql = "SELECT * FROM users_logins WHERE userid = ? AND type = ?;";
95✔
7048
        $logins = $this->dbhr->preQuery($sql, [$id, User::LOGIN_LINK]);
95✔
7049
        foreach ($logins as $login) {
95✔
7050
            $key = $login['credentials'];
29✔
7051
        }
7052

7053
        if (!$key) {
95✔
7054
            $key = Utils::randstr(32);
95✔
7055
            $rc = $this->dbhm->preExec("INSERT INTO users_logins (userid, type, credentials) VALUES (?,?,?);", [
95✔
7056
                $id,
95✔
7057
                User::LOGIN_LINK,
95✔
7058
                $key
95✔
7059
            ]);
95✔
7060
        }
7061

7062
        return $key;
95✔
7063
    }
7064

7065
    public function assignUserToToDonation($email, $userid) {
7066
        $email = trim($email);
465✔
7067

7068
        if (strlen($email)) {
465✔
7069
            # We might have donations made via PayPal using this email address which we can now link to this user.  Do
7070
            # SELECT first to avoid this having to replicate in the cluster.
7071
            $donations = $this->dbhr->preQuery("SELECT id FROM users_donations WHERE Payer = ? AND userid IS NULL;", [
465✔
7072
                $email
465✔
7073
            ]);
465✔
7074

7075
            foreach ($donations as $donation) {
465✔
7076
                $this->dbhm->preExec("UPDATE users_donations SET userid = ? WHERE id = ?;", [
158✔
7077
                    $userid,
158✔
7078
                    $donation['id']
158✔
7079
                ]);
158✔
7080
            }
7081
        }
7082
    }
7083
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc