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

Freegle / iznik-server / #2469

30 Nov 2025 09:48AM UTC coverage: 90.08% (-0.3%) from 90.386%
#2469

push

php-coveralls

edwh
Skip testMessageIsochrones - requires external Mapbox API

26478 of 29394 relevant lines covered (90.08%)

31.7 hits per line

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

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

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

8
use Jenssegers\ImageHash\ImageHash;
9

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

18
    const OPEN_AGE = 90;
19

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

251
                        return (TRUE);
289✔
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;
608✔
304

305
        $name = NULL;
608✔
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)) {
608✔
310
            $name = $atts['fullname'];
506✔
311
        } else if (Utils::pres('firstname', $atts) || Utils::pres('lastname', $atts)) {
129✔
312
            $first = Utils::pres('firstname', $atts);
122✔
313
            $last = Utils::pres('lastname', $atts);
122✔
314

315
            $name = $first && $last ? "$first $last" : ($first ? $first : $last);
122✔
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;
608✔
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;
608✔
323

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

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

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

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

353
        return ($name);
608✔
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);
609✔
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);
608✔
372

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

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

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

397
            return ($id);
607✔
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';
395✔
436

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

540
        return (NULL);
1✔
541
    }
542

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

549
        $ret = NULL;
1✔
550

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

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

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

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

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

569
        return $ret;
4✔
570
    }
571

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

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

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

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

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

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

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

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

624
        return (NULL);
1✔
625
    }
626

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

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

637
        return (NULL);
3✔
638
    }
639

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

835
        return FALSE;
480✔
836
    }
837

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

844
        Session::clearSessionCache();
480✔
845

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1021
        return ($rc);
×
1022
    }
1023

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1196

1197
            $extraworkids = [];
26✔
1198

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1353
        return (NULL);
5✔
1354
    }
1355

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1524
        return $widerreview;
19✔
1525
    }
1526

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

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

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

1543
        $settings = $defaults;
154✔
1544

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

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

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

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

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

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

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

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

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

1582
        Session::clearSessionCache();
46✔
1583

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1971
            $hash = NULL;
19✔
1972

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2320

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2720
        $rets = [];
263✔
2721

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

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

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

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

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

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

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

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

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

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

2766
        return $ret;
96✔
2767
    }
2768

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3017

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3437
            $commentusers = [];
78✔
3438

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3506
        return $comments;
1✔
3507
    }
3508

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

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

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

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

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

3531
        return (NULL);
1✔
3532
    }
3533

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4258
        $ret = [];
5✔
4259

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4375
        return $ret;
59✔
4376
    }
4377

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

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

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

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

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

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

4417
        return $val;
59✔
4418
    }
4419

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

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

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

4435
        $defs = [
607✔
4436
            self::NOTIFS_EMAIL => TRUE,
607✔
4437
            self::NOTIFS_EMAIL_MINE => FALSE,
607✔
4438
            self::NOTIFS_PUSH => TRUE,
607✔
4439
            self::NOTIFS_APP => TRUE
607✔
4440
        ];
607✔
4441

4442
        $ret = ($notifs && array_key_exists($type, $notifs)) ? $notifs[$type] : $defs[$type];
607✔
4443

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

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

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

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

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

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

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

4488
                    # Decode emoji escape sequences to actual emojis for display.
4489
                    $message = Utils::decodeEmojis($message);
3✔
4490

4491
                    $message = strlen($message) > 256 ? (substr($message, 0, 256) . "...") : $message;
3✔
4492
                }
4493

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

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

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

4520
                    if ($title) {
4✔
4521
                        $route = '/';
4✔
4522

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

4558
            $work = $this->getWorkCounts();
3✔
4559
            $total = $work['total'] + $chatcount;
3✔
4560

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

4578
            $title = $chatcount ? ("$chatcount chat message" . ($chatcount != 1 ? 's' : '') . "\n") : '';
3✔
4579
            $route = NULL;
3✔
4580

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

4588
            $title = $title == '' ? NULL : $title;
3✔
4589
        }
4590

4591

4592
        return ([$total, $chatcount, $notifcount, $title, $message, array_unique($chatids), $route, $category, $threadId, $image]);
9✔
4593
    }
4594

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4682
                        $html = invite($fromname, $frommail, $url);
9✔
4683

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

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

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

4706
        return ($ret);
9✔
4707
    }
4708

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

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

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

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

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

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

4751
        return ($ret);
8✔
4752
    }
4753

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

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

4762
            if ($loc) {
175✔
4763
                if ($blur && ($loc['lat'] || $loc['lng'])) {
173✔
4764
                    list ($loc['lat'], $loc['lng']) = Utils::blur($loc['lat'], $loc['lng'], $blur);
4✔
4765
                }
4766

4767
                $ret = [ $loc['lat'], $loc['lng'], Utils::presdef('loc', $loc, NULL) ];
173✔
4768
            }
4769
        }
4770

4771
        return $ret;
175✔
4772
    }
4773

4774
    public function getPublicLocations(&$users, $atts = NULL)
4775
    {
4776
        $idsleft = [];
108✔
4777
        
4778
        foreach ($users as $userid => $user) {
108✔
4779
            if (!Utils::pres('info', $user) || !Utils::pres('publiclocation', $user['info'])) {
108✔
4780
                $idsleft[] = $userid;
108✔
4781
            }
4782
        }
4783
        
4784
        $areas = NULL;
108✔
4785
        $membs = NULL;
108✔
4786

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

4791
            foreach ($atts as $att) {
108✔
4792
                $loc = NULL;
108✔
4793
                $grp = NULL;
108✔
4794

4795
                $aid = NULL;
108✔
4796
                $lid = NULL;
108✔
4797
                $lat = NULL;
108✔
4798
                $lng = NULL;
108✔
4799

4800
                # Default to nowhere.
4801
                $users[$att['id']]['info']['publiclocation'] = [
108✔
4802
                    'display' => '',
108✔
4803
                    'location' => NULL,
108✔
4804
                    'groupname' => NULL
108✔
4805
                ];
108✔
4806

4807
                if (Utils::pres('settings', $att)) {
108✔
4808
                    $settings = $att['settings'];
23✔
4809
                    $settings = json_decode($settings, TRUE);
23✔
4810

4811
                    if (Utils::pres('mylocation', $settings) && Utils::pres('area', $settings['mylocation'])) {
23✔
4812
                        $loc = $settings['mylocation']['area']['name'];
7✔
4813
                        $lid = $settings['mylocation']['id'];
7✔
4814
                        $lat = $settings['mylocation']['lat'];
7✔
4815
                        $lng = $settings['mylocation']['lng'];
7✔
4816
                    }
4817
                }
4818

4819
                if (!$loc) {
108✔
4820
                    # Get the name of the last area we used.
4821
                    if (is_null($areas)) {
101✔
4822
                        $areas = $this->dbhr->preQuery("SELECT l2.id, l2.name, l2.lat, l2.lng, users.id AS userid FROM locations l1 
101✔
4823
                            INNER JOIN users ON users.lastlocation = l1.id
4824
                            INNER JOIN locations l2 ON l2.id = l1.areaid
4825
                            WHERE users.id IN (" . implode(',', $idsleft) . ");", NULL, FALSE, FALSE);
101✔
4826
                    }
4827

4828
                    foreach ($areas as $area) {
101✔
4829
                        if ($att['id'] ==  $area['userid']) {
25✔
4830
                            $loc = $area['name'];
25✔
4831
                            $lid = $area['id'];
25✔
4832
                            $lat = $area['lat'];
25✔
4833
                            $lng = $area['lng'];
25✔
4834
                        }
4835
                    }
4836
                }
4837

4838
                if (!$lid) {
108✔
4839
                    # Find the group of which we are a member which is closest to our location.  We do this because generally
4840
                    # the number of groups we're in is small and therefore this will be quick, whereas the groupsNear call is
4841
                    # fairly slow.
4842
                    $closestdist = PHP_INT_MAX;
101✔
4843
                    $closestname = NULL;
101✔
4844

4845
                    # Get all the memberships.
4846
                    if (!$membs) {
101✔
4847
                        $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✔
4848
                                ',',
101✔
4849
                                $idsleft
101✔
4850
                            ) . ") ORDER BY added ASC;";
101✔
4851
                        $membs = $this->dbhr->preQuery($sql);
101✔
4852
                    }
4853

4854
                    foreach ($membs as $memb) {
101✔
4855
                        if ($memb['userid'] == $att['id']) {
90✔
4856
                            $dist = \GreatCircle::getDistance($lat, $lng, $memb['lat'], $memb['lng']);
90✔
4857

4858
                            if ($dist < $closestdist) {
90✔
4859
                                $closestdist = $dist;
90✔
4860
                                $closestname = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
90✔
4861
                            }
4862
                        }
4863
                    }
4864

4865
                    if (!is_null($closestname)) {
101✔
4866
                        $grp = $closestname;
90✔
4867

4868
                        # The location name might be in the group name, in which case just use the group.
4869
                        $loc = stripos($grp, $loc) !== FALSE ? NULL : $loc;
90✔
4870
                    }
4871
                }
4872

4873
                if ($loc) {
108✔
4874
                    $display = $loc ? ($loc . ($grp ? ", $grp" : "")) : ($grp ? $grp : '');
32✔
4875

4876
                    $users[$att['id']]['info']['publiclocation'] = [
32✔
4877
                        'display' => $display,
32✔
4878
                        'location' => $loc,
32✔
4879
                        'groupname' => $grp
32✔
4880
                    ];
32✔
4881

4882
                    $idsleft = array_filter($idsleft, function($val) use ($att) {
32✔
4883
                        return($val != $att['id']);
32✔
4884
                    });
32✔
4885
                }
4886
            }
4887

4888
            if (count($idsleft) > 0) {
108✔
4889
                # We have some left which don't have explicit postcodes.  Try for a group name.
4890
                #
4891
                # First check the group we used most recently.
4892
                #error_log("Look for group name only for {$att['id']}");
4893
                $found = [];
101✔
4894
                foreach ($idsleft as $userid) {
101✔
4895
                    $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✔
4896
                        $userid
101✔
4897
                    ]);
101✔
4898

4899
                    foreach ($messages as $msg) {
101✔
4900
                        list ($type, $item, $location) = Message::parseSubject($msg['subject']);
63✔
4901

4902
                        if ($item) {
63✔
4903
                            $grp = $location;
44✔
4904

4905
                            // Handle some misformed locations which end up with spurious brackets.
4906
                            $grp = preg_replace('/\(|\)/', '', $grp);
44✔
4907

4908
                            $users[$userid]['info']['publiclocation'] = [
44✔
4909
                                'display' => $grp,
44✔
4910
                                'location' => NULL,
44✔
4911
                                'groupname' => $grp
44✔
4912
                            ];
44✔
4913

4914
                            $found[] = $userid;
44✔
4915
                        }
4916
                    }
4917
                }
4918

4919
                $idsleft = array_diff($idsleft, $found);
101✔
4920
                
4921
                # Now check just membership.
4922
                if (count($idsleft)) {
101✔
4923
                    if (!$membs) {
67✔
4924
                        $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✔
4925
                                ',',
22✔
4926
                                $idsleft
22✔
4927
                            ) . ") ORDER BY added ASC;";
22✔
4928
                        $membs = $this->dbhr->preQuery($sql);
22✔
4929
                    }
4930
                    
4931
                    foreach ($idsleft as $userid) {
67✔
4932
                        # Now check the group we joined most recently.
4933
                        foreach ($membs as $memb) {
67✔
4934
                            if ($memb['userid'] == $userid) {
49✔
4935
                                $grp = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
49✔
4936

4937
                                $users[$userid]['info']['publiclocation'] = [
49✔
4938
                                    'display' => $grp,
49✔
4939
                                    'location' => NULL,
49✔
4940
                                    'groupname' => $grp
49✔
4941
                                ];
49✔
4942
                            }
4943
                        }
4944
                    }
4945
                }
4946
            }
4947
        }
4948
    }
4949

4950
    public function getLatLngs($users, $usedef = TRUE, $usegroup = TRUE, $needgroup = FALSE, $atts = NULL, $blur = NULL)
4951
    {
4952
        $userids = array_filter(array_column($users, 'id'));
176✔
4953
        $ret = [];
176✔
4954

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

4958
            foreach ($atts as $att) {
176✔
4959
                $lat = NULL;
176✔
4960
                $lng = NULL;
176✔
4961
                $loc = NULL;
176✔
4962

4963
                if (Utils::pres('settings', $att)) {
176✔
4964
                    $settings = $att['settings'];
37✔
4965
                    $settings = json_decode($settings, TRUE);
37✔
4966

4967
                    if (Utils::pres('mylocation', $settings)) {
37✔
4968
                        $lat = $settings['mylocation']['lat'];
33✔
4969
                        $lng = $settings['mylocation']['lng'];
33✔
4970
                        $loc = Utils::presdef('name', $settings['mylocation'], NULL);
33✔
4971
                        #error_log("Got from mylocation $lat, $lng, $loc");
4972
                    }
4973
                }
4974

4975
                if (is_null($lat)) {
176✔
4976
                    $lid = $att['lastlocation'];
157✔
4977

4978
                    if ($lid) {
157✔
4979
                        $l = new Location($this->dbhr, $this->dbhm, $lid);
23✔
4980
                        $lat = $l->getPrivate('lat');
23✔
4981
                        $lng = $l->getPrivate('lng');
23✔
4982
                        $loc = $l->getPrivate('name');
23✔
4983
                        #error_log("Got from last location $lat, $lng, $loc");
4984
                    }
4985
                }
4986

4987
                if (!is_null($lat)) {
176✔
4988
                    $ret[$att['id']] = [
52✔
4989
                        'lat' => $lat,
52✔
4990
                        'lng' => $lng,
52✔
4991
                        'loc' => $loc,
52✔
4992
                    ];
52✔
4993

4994
                    $userids = array_filter($userids, function($id) use ($att) {
52✔
4995
                        return $id != $att['id'];
52✔
4996
                    });
52✔
4997
                }
4998
            }
4999
        }
5000

5001
        if ($userids && count($userids) && $usegroup) {
176✔
5002
            # Still some we haven't handled.  Get the last message posted on a group with a location, if any.
5003
            $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✔
5004
            foreach ($membs as $memb) {
150✔
5005
                $ret[$memb['userid']] = [
3✔
5006
                    'lat' => $memb['lat'],
3✔
5007
                    'lng' => $memb['lng']
3✔
5008
                ];
3✔
5009

5010
                #error_log("Got from last message posted {$memb['lat']}, {$memb['lng']}");
5011

5012
                $userids = array_filter($userids, function($id) use ($memb) {
3✔
5013
                    return $id != $memb['userid'];
3✔
5014
                });
3✔
5015
            }
5016
        }
5017

5018
        if ($userids && count($userids) && $usegroup) {
176✔
5019
            # Still some we haven't handled.  Get the memberships.  Logic will choose most recently joined.
5020
            $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✔
5021
            foreach ($membs as $memb) {
148✔
5022
                $ret[$memb['userid']] = [
130✔
5023
                    'lat' => $memb['lat'],
130✔
5024
                    'lng' => $memb['lng'],
130✔
5025
                    'group' => Utils::presdef('namefull', $memb, $memb['nameshort'])
130✔
5026
                ];
130✔
5027

5028
                #error_log("Got from membership {$memb['lat']}, {$memb['lng']}, " . Utils::presdef('namefull', $memb, $memb['nameshort']));
5029

5030
                $userids = array_filter($userids, function($id) use ($memb) {
130✔
5031
                    return $id != $memb['userid'];
130✔
5032
                });
130✔
5033
            }
5034
        }
5035

5036
        if ($userids && count($userids)) {
176✔
5037
            # Still some we haven't handled.
5038
            foreach ($userids as $userid) {
26✔
5039
                if ($usedef) {
26✔
5040
                    $ret[$userid] = [
21✔
5041
                        'lat' => 53.9450,
21✔
5042
                        'lng' => -2.5209
21✔
5043
                    ];
21✔
5044
                } else {
5045
                    $ret[$userid] = NULL;
15✔
5046
                }
5047
            }
5048
        }
5049

5050
        if ($needgroup) {
176✔
5051
            # Get a group name.
5052
            $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✔
5053
            foreach ($membs as $memb) {
7✔
5054
                $ret[$memb['userid']]['group'] = Utils::presdef('namefull', $memb, $memb['nameshort']);
7✔
5055
            }
5056
        }
5057

5058
        if ($blur) {
176✔
5059
            foreach ($ret as &$memb) {
7✔
5060
                if ($memb['lat'] || $memb['lng']) {
7✔
5061
                    list ($memb['lat'], $memb['lng']) = Utils::blur($memb['lat'], $memb['lng'], $blur);
7✔
5062
                }
5063
            }
5064
        }
5065

5066
        return ($ret);
176✔
5067
    }
5068

5069
    public function isFreegleMod()
5070
    {
5071
        $ret = FALSE;
173✔
5072

5073
        $this->cacheMemberships();
173✔
5074

5075
        foreach ($this->memberships as $mem) {
173✔
5076
            if ($mem['type'] == Group::GROUP_FREEGLE && ($mem['role'] == User::ROLE_OWNER || $mem['role'] == User::ROLE_MODERATOR)) {
145✔
5077
                $ret = TRUE;
43✔
5078
            }
5079
        }
5080

5081
        return ($ret);
173✔
5082
    }
5083

5084
    public function getKudos($id = NULL)
5085
    {
5086
        $id = $id ? $id : $this->id;
1✔
5087
        $kudos = [
1✔
5088
            'userid' => $id,
1✔
5089
            'posts' => 0,
1✔
5090
            'chats' => 0,
1✔
5091
            'newsfeed' => 0,
1✔
5092
            'events' => 0,
1✔
5093
            'vols' => 0,
1✔
5094
            'facebook' => 0,
1✔
5095
            'platform' => 0,
1✔
5096
            'kudos' => 0,
1✔
5097
        ];
1✔
5098

5099
        $kudi = $this->dbhr->preQuery("SELECT * FROM users_kudos WHERE userid = ?;", [
1✔
5100
            $id
1✔
5101
        ]);
1✔
5102

5103
        foreach ($kudi as $k) {
1✔
5104
            $kudos = $k;
1✔
5105
        }
5106

5107
        return ($kudos);
1✔
5108
    }
5109

5110
    public function updateKudos($id = NULL, $force = FALSE)
5111
    {
5112
        $current = $this->getKudos($id);
1✔
5113

5114
        # Only update if we don't have one or it's older than a day.  This avoids repeatedly updating the entry
5115
        # for the same user in some bulk operations.
5116
        if (!Utils::pres('timestamp', $current) || (time() - strtotime($current['timestamp']) > 24 * 60 * 60)) {
1✔
5117
            # We analyse a user's activity and assign them a level.
5118
            #
5119
            # Only interested in activity in the last year.
5120
            $id = $id ? $id : $this->id;
1✔
5121
            $start = date('Y-m-d', strtotime("365 days ago"));
1✔
5122

5123
            # First, the number of months in which they have posted.
5124
            $posts = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM messages WHERE fromuser = ? AND date >= '$start';", [
1✔
5125
                $id
1✔
5126
            ])[0]['count'];
1✔
5127

5128
            # Ditto communicated with people.
5129
            $chats = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(date), '-', MONTH(date)))) AS count FROM chat_messages WHERE userid = ? AND date >= '$start';", [
1✔
5130
                $id
1✔
5131
            ])[0]['count'];
1✔
5132

5133
            # Newsfeed posts
5134
            $newsfeed = $this->dbhr->preQuery("SELECT COUNT(DISTINCT(CONCAT(YEAR(timestamp), '-', MONTH(timestamp)))) AS count FROM newsfeed WHERE userid = ? AND added >= '$start';", [
1✔
5135
                $id
1✔
5136
            ])[0]['count'];
1✔
5137

5138
            # Events
5139
            $events = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM communityevents WHERE userid = ? AND added >= '$start';", [
1✔
5140
                $id
1✔
5141
            ])[0]['count'];
1✔
5142

5143
            # Volunteering
5144
            $vols = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM volunteering WHERE userid = ? AND added >= '$start';", [
1✔
5145
                $id
1✔
5146
            ])[0]['count'];
1✔
5147

5148
            # Do they have a Facebook login?
5149
            $facebook = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_logins WHERE userid = ? AND type = ?", [
1✔
5150
                    $id,
1✔
5151
                    User::LOGIN_FACEBOOK
1✔
5152
                ])[0]['count'] > 0;
1✔
5153

5154
            # Have they posted using the platform?
5155
            $platform = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM messages WHERE fromuser = ? AND arrival >= '$start' AND sourceheader = ?;", [
1✔
5156
                    $id,
1✔
5157
                    Message::PLATFORM
1✔
5158
                ])[0]['count'] > 0;
1✔
5159

5160
            $kudos = $posts + $chats + $newsfeed + $events + $vols;
1✔
5161

5162
            if ($kudos > 0 || $force) {
1✔
5163
                # No sense in creating entries which are blank or the same.
5164
                $current = $this->getKudos($id);
1✔
5165

5166
                if ($current['kudos'] != $kudos || $force) {
1✔
5167
                    $this->dbhm->preExec("REPLACE INTO users_kudos (userid, kudos, posts, chats, newsfeed, events, vols, facebook, platform) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", [
1✔
5168
                        $id,
1✔
5169
                        $kudos,
1✔
5170
                        $posts,
1✔
5171
                        $chats,
1✔
5172
                        $newsfeed,
1✔
5173
                        $events,
1✔
5174
                        $vols,
1✔
5175
                        $facebook,
1✔
5176
                        $platform
1✔
5177
                    ], FALSE);
1✔
5178
                }
5179
            }
5180
        }
5181
    }
5182

5183
    public function topKudos($gid, $limit = 10)
5184
    {
5185
        $limit = intval($limit);
1✔
5186

5187
        $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✔
5188
            $gid,
1✔
5189
            User::ROLE_MEMBER
1✔
5190
        ]);
1✔
5191

5192
        $ret = [];
1✔
5193

5194
        foreach ($kudos as $k) {
1✔
5195
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5196
            $atts = $u->getPublic();
1✔
5197
            $atts['email'] = $u->getEmailPreferred();
1✔
5198

5199
            $thisone = [
1✔
5200
                'user' => $atts,
1✔
5201
                'kudos' => $k
1✔
5202
            ];
1✔
5203

5204
            $ret[] = $thisone;
1✔
5205
        }
5206

5207
        return ($ret);
1✔
5208
    }
5209

5210
    public function possibleMods($gid, $limit = 10)
5211
    {
5212
        # We look for users who are not mods with top kudos who also:
5213
        # - active in last 60 days
5214
        # - not bouncing
5215
        # - using a location which is in the group area
5216
        # - have posted with the platform, as we don't want loyal users of TN or Yahoo.
5217
        # - have a Facebook login, as they are more likely to do publicity.
5218
        $limit = intval($limit);
1✔
5219
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
5220
        $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✔
5221
        $kudos = $this->dbhr->preQuery($sql, [
1✔
5222
            $gid,
1✔
5223
            User::ROLE_MEMBER
1✔
5224
        ]);
1✔
5225

5226
        $ret = [];
1✔
5227

5228
        foreach ($kudos as $k) {
1✔
5229
            $u = new User($this->dbhr, $this->dbhm, $k['userid']);
1✔
5230
            $atts = $u->getPublic();
1✔
5231
            $atts['email'] = $u->getEmailPreferred();
1✔
5232

5233
            $thisone = [
1✔
5234
                'user' => $atts,
1✔
5235
                'kudos' => $k
1✔
5236
            ];
1✔
5237

5238
            $ret[] = $thisone;
1✔
5239
        }
5240

5241
        return ($ret);
1✔
5242
    }
5243

5244
    public function requestExport($sync = FALSE)
5245
    {
5246
        $tag = Utils::randstr(64);
8✔
5247

5248
        # Flag sync ones as started to avoid window with background thread.
5249
        $sync = $sync ? "NOW()" : "NULL";
8✔
5250
        $this->dbhm->preExec("INSERT INTO users_exports (userid, tag, started) VALUES (?, ?, $sync);", [
8✔
5251
            $this->id,
8✔
5252
            $tag
8✔
5253
        ]);
8✔
5254

5255
        return ([$this->dbhm->lastInsertId(), $tag]);
8✔
5256
    }
5257

5258
    public function export($exportid, $tag)
5259
    {
5260
        $this->dbhm->preExec("UPDATE users_exports SET started = NOW() WHERE id = ? AND tag = ?;", [
7✔
5261
            $exportid,
7✔
5262
            $tag
7✔
5263
        ]);
7✔
5264

5265
        # For GDPR we support the ability for a user to export the data we hold about them.  Key points about this:
5266
        #
5267
        # - It needs to be at a high level of abstraction and understandable by the user, not just a cryptic data
5268
        #   dump.
5269
        # - It needs to include data provided by the user and data observed about the user, but not profiling
5270
        #   or categorisation based on that data.  This means that (for example) we need to return which
5271
        #   groups they have joined, but not whether joining those groups has flagged them up as a potential
5272
        #   spammer.
5273
        $ret = [];
7✔
5274
        error_log("...basic info");
7✔
5275

5276
        # Data in user table.
5277
        $d = [];
7✔
5278
        $d['Our_internal_ID_for_you'] = $this->getPrivate('id');
7✔
5279
        $d['Your_full_name'] = $this->getPrivate('fullname');
7✔
5280
        $d['Your_first_name'] = $this->getPrivate('firstname');
7✔
5281
        $d['Your_last_name'] = $this->getPrivate('lastname');
7✔
5282
        $d['Your_Yahoo_ID'] = $this->getPrivate('yahooid');
7✔
5283
        $d['Your_role_on_the_system'] = $this->getPrivate('systemrole');
7✔
5284
        $d['When_you_joined_the_site'] = Utils::ISODate($this->getPrivate('added'));
7✔
5285
        $d['When_you_last_accessed_the_site'] = Utils::ISODate($this->getPrivate('lastaccess'));
7✔
5286
        $d['When_we_last_checked_for_relevant_posts_for_you'] = Utils::ISODate($this->getPrivate('lastrelevantcheck'));
7✔
5287
        $d['Whether_your_email_is_bouncing'] = $this->getPrivate('bouncing') ? 'Yes' : 'No';
7✔
5288
        $d['Permissions_you_have_on_the_site'] = $this->getPrivate('permissions');
7✔
5289
        $d['Number_of_remaining_invitations_you_can_send_to_other_people'] = $this->getPrivate('invitesleft');
7✔
5290

5291
        $lastlocation = $this->user['lastlocation'];
7✔
5292

5293
        if ($lastlocation) {
7✔
5294
            $l = new Location($this->dbhr, $this->dbhm, $lastlocation);
×
5295
            $d['Last_location_you_posted_from'] = $l->getPrivate('name') . " (" . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
×
5296
        }
5297

5298
        $settings = $this->getPrivate('settings');
7✔
5299

5300
        if ($settings) {
7✔
5301
            $settings = json_decode($settings, TRUE);
7✔
5302

5303
            $location = Utils::presdef('id', Utils::presdef('mylocation', $settings, []), NULL);
7✔
5304

5305
            if ($location) {
7✔
5306
                $l = new Location($this->dbhr, $this->dbhm, $location);
6✔
5307
                $d['Last_location_you_entered'] = $l->getPrivate('name') . ' (' . $l->getPrivate('lat') . ', ' . $l->getPrivate('lng') . ')';
6✔
5308
            }
5309

5310
            $notifications = Utils::pres('notifications', $settings);
7✔
5311

5312
            $d['Notifications']['Send_email_notifications_for_chat_messages'] = Utils::presdef('email', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5313
            $d['Notifications']['Send_email_notifications_of_chat_messages_you_send'] = Utils::presdef('emailmine', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5314
            $d['Notifications']['Send_notifications_for_apps'] = Utils::presdef('app', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5315
            $d['Notifications']['Send_push_notifications_to_web_browsers'] = Utils::presdef('push', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5316
            $d['Notifications']['Send_Facebook_notifications'] = Utils::presdef('facebook', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5317
            $d['Notifications']['Send_emails_about_notifications_on_the_site'] = Utils::presdef('notificationmails', $notifications, TRUE) ? 'Yes' : 'No';
7✔
5318

5319
            $d['Hide_profile_picture'] = Utils::presdef('useprofile', $settings, TRUE) ? 'Yes' : 'No';
7✔
5320

5321
            if ($this->isModerator()) {
7✔
5322
                $d['Show_members_that_you_are_a_moderator'] = Utils::pres('showmod', $settings) ? 'Yes' : 'No';
1✔
5323

5324
                switch (Utils::presdef('modnotifs', $settings, 4)) {
1✔
5325
                    case 24:
1✔
5326
                        $d['Send_notifications_of_active_mod_work'] = 'After 24 hours';
×
5327
                        break;
×
5328
                    case 12:
1✔
5329
                        $d['Send_notifications_of_active_mod_work'] = 'After 12 hours';
×
5330
                        break;
×
5331
                    case 4:
1✔
5332
                        $d['Send_notifications_of_active_mod_work'] = 'After 4 hours';
1✔
5333
                        break;
1✔
5334
                    case 2:
×
5335
                        $d['Send_notifications_of_active_mod_work'] = 'After 2 hours';
×
5336
                        break;
×
5337
                    case 1:
×
5338
                        $d['Send_notifications_of_active_mod_work'] = 'After 1 hours';
×
5339
                        break;
×
5340
                    case 0:
×
5341
                        $d['Send_notifications_of_active_mod_work'] = 'Immediately';
×
5342
                        break;
×
5343
                    case -1:
5344
                        $d['Send_notifications_of_active_mod_work'] = 'Never';
×
5345
                        break;
×
5346
                }
5347

5348
                switch (Utils::presdef('backupmodnotifs', $settings, 12)) {
1✔
5349
                    case 24:
1✔
5350
                        $d['Send_notifications_of_backup_mod_work'] = 'After 24 hours';
×
5351
                        break;
×
5352
                    case 12:
1✔
5353
                        $d['Send_notifications_of_backup_mod_work'] = 'After 12 hours';
1✔
5354
                        break;
1✔
5355
                    case 4:
×
5356
                        $d['Send_notifications_of_backup_mod_work'] = 'After 4 hours';
×
5357
                        break;
×
5358
                    case 2:
×
5359
                        $d['Send_notifications_of_backup_mod_work'] = 'After 2 hours';
×
5360
                        break;
×
5361
                    case 1:
×
5362
                        $d['Send_notifications_of_backup_mod_work'] = 'After 1 hours';
×
5363
                        break;
×
5364
                    case 0:
×
5365
                        $d['Send_notifications_of_backup_mod_work'] = 'Immediately';
×
5366
                        break;
×
5367
                    case -1:
5368
                        $d['Send_notifications_of_backup_mod_work'] = 'Never';
×
5369
                        break;
×
5370
                }
5371

5372
                $d['Show_members_that_you_are_a_moderator'] = Utils::presdef('showmod', $settings, TRUE) ? 'Yes' : 'No';
1✔
5373
            }
5374
        }
5375

5376
        # Invitations.  Only show what we sent; the outcome is not this user's business.
5377
        error_log("...invitations");
7✔
5378
        $invites = $this->listInvitations("1970-01-01");
7✔
5379
        $d['invitations'] = [];
7✔
5380

5381
        foreach ($invites as $invite) {
7✔
5382
            $d['invitations'][] = [
6✔
5383
                'email' => $invite['email'],
6✔
5384
                'date' => Utils::ISODate($invite['date'])
6✔
5385
            ];
6✔
5386
        }
5387

5388
        error_log("...emails");
7✔
5389
        $d['emails'] = $this->getEmails();
7✔
5390

5391
        foreach ($d['emails'] as &$email) {
7✔
5392
            $email['added'] = Utils::ISODate($email['added']);
1✔
5393

5394
            if ($email['validated']) {
1✔
5395
                $email['validated'] = Utils::ISODate($email['validated']);
×
5396
            }
5397
        }
5398

5399
        error_log("...logins");
7✔
5400
        $d['logins'] = $this->dbhr->preQuery("SELECT type, uid, added, lastaccess FROM users_logins WHERE userid = ?;", [
7✔
5401
            $this->id
7✔
5402
        ]);
7✔
5403

5404
        foreach ($d['logins'] as &$dd) {
7✔
5405
            $dd['added'] = Utils::ISODate($dd['added']);
7✔
5406
            $dd['lastaccess'] = Utils::ISODate($dd['lastaccess']);
7✔
5407
        }
5408

5409
        error_log("...memberships");
7✔
5410
        $d['memberships'] = $this->getMemberships();
7✔
5411

5412
        error_log("...memberships history");
7✔
5413
        $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✔
5414
        $membs = $this->dbhr->preQuery($sql, [$this->id]);
7✔
5415
        foreach ($membs as &$memb) {
7✔
5416
            $name = $memb['namefull'] ? $memb['namefull'] : $memb['nameshort'];
7✔
5417
            $memb['namedisplay'] = $name;
7✔
5418
            $memb['added'] = Utils::ISODate($memb['added']);
7✔
5419
        }
5420

5421
        $d['membershipshistory'] = $membs;
7✔
5422

5423
        error_log("...searches");
7✔
5424
        $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✔
5425
            $this->id
7✔
5426
        ]);
7✔
5427

5428
        foreach ($d['searches'] as &$s) {
7✔
5429
            $s['date'] = Utils::ISODate($s['date']);
×
5430
        }
5431

5432
        error_log("...alerts");
7✔
5433
        $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✔
5434
            $this->id
7✔
5435
        ]);
7✔
5436

5437
        foreach ($d['alerts'] as &$s) {
7✔
5438
            $s['responded'] = Utils::ISODate($s['responded']);
×
5439
        }
5440

5441
        error_log("...donations");
7✔
5442
        $d['donations'] = $this->dbhr->preQuery("SELECT * FROM users_donations WHERE userid = ? ORDER BY timestamp ASC;", [
7✔
5443
            $this->id
7✔
5444
        ]);
7✔
5445

5446
        foreach ($d['donations'] as &$s) {
7✔
5447
            $s['timestamp'] = Utils::ISODate($s['timestamp']);
1✔
5448
        }
5449

5450
        error_log("...bans");
7✔
5451
        $d['bans'] = [];
7✔
5452

5453
        $bans = $this->dbhr->preQuery("SELECT * FROM users_banned WHERE byuser = ?;", [
7✔
5454
            $this->id
7✔
5455
        ]);
7✔
5456

5457
        foreach ($bans as $ban) {
7✔
5458
            $g = Group::get($this->dbhr, $this->dbhm, $ban['groupid']);
1✔
5459
            $u = User::get($this->dbhr, $this->dbhm, $ban['userid']);
1✔
5460
            $d['bans'][] = [
1✔
5461
                'date' => Utils::ISODate($ban['date']),
1✔
5462
                'group' => $g->getName(),
1✔
5463
                'email' => $u->getEmailPreferred(),
1✔
5464
                'userid' => $ban['userid']
1✔
5465
            ];
1✔
5466
        }
5467

5468
        error_log("...spammers");
7✔
5469
        $d['spammers'] = $this->dbhr->preQuery("SELECT * FROM spam_users WHERE byuserid = ? ORDER BY added ASC;", [
7✔
5470
            $this->id
7✔
5471
        ]);
7✔
5472

5473
        foreach ($d['spammers'] as &$s) {
7✔
5474
            $s['added'] = Utils::ISODate($s['added']);
×
5475
            $u = User::get($this->dbhr, $this->dbhm, $s['userid']);
×
5476
            $s['email'] = $u->getEmailPreferred();
×
5477
        }
5478

5479
        $d['spamdomains'] = $this->dbhr->preQuery("SELECT domain, date FROM spam_whitelist_links WHERE userid = ?;", [
7✔
5480
            $this->id
7✔
5481
        ]);
7✔
5482

5483
        foreach ($d['spamdomains'] as &$s) {
7✔
5484
            $s['date'] = Utils::ISODate($s['date']);
×
5485
        }
5486

5487
        error_log("...images");
7✔
5488
        $images = $this->dbhr->preQuery("SELECT id, url FROM users_images WHERE userid = ?;", [
7✔
5489
            $this->id
7✔
5490
        ]);
7✔
5491

5492
        $d['images'] = [];
7✔
5493

5494
        foreach ($images as $image) {
7✔
5495
            if (Utils::pres('url', $image)) {
6✔
5496
                $d['images'][] = [
6✔
5497
                    'id' => $image['id'],
6✔
5498
                    'thumb' => $image['url']
6✔
5499
                ];
6✔
5500
            } else {
5501
                $a = new Attachment($this->dbhr, $this->dbhm, NULL, Attachment::TYPE_USER);
×
5502
                $d['images'][] = [
×
5503
                    'id' => $image['id'],
×
5504
                    'thumb' => $a->getPath(TRUE, $image['id'])
×
5505
                ];
×
5506
            }
5507
        }
5508

5509
        error_log("...notifications");
7✔
5510
        $d['notifications'] = $this->dbhr->preQuery("SELECT timestamp, url FROM users_notifications WHERE touser = ? AND seen = 1;", [
7✔
5511
            $this->id
7✔
5512
        ]);
7✔
5513

5514
        foreach ($d['notifications'] as &$n) {
7✔
5515
            $n['timestamp'] = Utils::ISODate($n['timestamp']);
×
5516
        }
5517

5518
        error_log("...addresses");
7✔
5519
        $d['addresses'] = [];
7✔
5520

5521
        $addrs = $this->dbhr->preQuery("SELECT * FROM users_addresses WHERE userid = ?;", [
7✔
5522
            $this->id
7✔
5523
        ]);
7✔
5524

5525
        foreach ($addrs as $addr) {
7✔
5526
            $a = new Address($this->dbhr, $this->dbhm, $addr['id']);
×
5527
            $d['addresses'][] = $a->getPublic();
×
5528
        }
5529

5530
        error_log("...events");
7✔
5531
        $d['communityevents'] = [];
7✔
5532

5533
        $events = $this->dbhr->preQuery("SELECT id FROM communityevents WHERE userid = ?;", [
7✔
5534
            $this->id
7✔
5535
        ]);
7✔
5536

5537
        foreach ($events as $event) {
7✔
5538
            $e = new CommunityEvent($this->dbhr, $this->dbhm, $event['id']);
×
5539
            $d['communityevents'][] = $e->getPublic();
×
5540
        }
5541

5542
        error_log("...volunteering");
7✔
5543
        $d['volunteering'] = [];
7✔
5544

5545
        $events = $this->dbhr->preQuery("SELECT id FROM volunteering WHERE userid = ?;", [
7✔
5546
            $this->id
7✔
5547
        ]);
7✔
5548

5549
        foreach ($events as $event) {
7✔
5550
            $e = new Volunteering($this->dbhr, $this->dbhm, $event['id']);
×
5551
            $d['volunteering'][] = $e->getPublic();
×
5552
        }
5553

5554
        error_log("...comments");
7✔
5555
        $d['comments'] = [];
7✔
5556
        $comms = $this->dbhr->preQuery("SELECT * FROM users_comments WHERE byuserid = ? ORDER BY date ASC;", [
7✔
5557
            $this->id
7✔
5558
        ]);
7✔
5559

5560
        foreach ($comms as &$comm) {
7✔
5561
            $u = User::get($this->dbhr, $this->dbhm, $comm['userid']);
1✔
5562
            $comm['email'] = $u->getEmailPreferred();
1✔
5563
            $comm['date'] = Utils::ISODate($comm['date']);
1✔
5564
            $d['comments'][] = $comm;
1✔
5565
        }
5566

5567
        error_log("...ratings");
7✔
5568
        $d['ratings'] = $this->getRated();
7✔
5569

5570
        error_log("...locations");
7✔
5571
        $d['locations'] = [];
7✔
5572

5573
        $locs = $this->dbhr->preQuery("SELECT * FROM locations_excluded WHERE userid = ?;", [
7✔
5574
            $this->id
7✔
5575
        ]);
7✔
5576

5577
        foreach ($locs as $loc) {
7✔
5578
            $g = Group::get($this->dbhr, $this->dbhm, $loc['groupid']);
×
5579
            $l = new Location($this->dbhr, $this->dbhm, $loc['locationid']);
×
5580
            $d['locations'][] = [
×
5581
                'group' => $g->getName(),
×
5582
                'location' => $l->getPrivate('name'),
×
5583
                'date' => Utils::ISODate($loc['date'])
×
5584
            ];
×
5585
        }
5586

5587
        error_log("...messages");
7✔
5588
        $msgs = $this->dbhr->preQuery("SELECT id FROM messages WHERE fromuser = ? ORDER BY arrival ASC;", [
7✔
5589
            $this->id
7✔
5590
        ]);
7✔
5591

5592
        $d['messages'] = [];
7✔
5593

5594
        foreach ($msgs as $msg) {
7✔
5595
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
×
5596

5597
            # Show all info here even moderator attributes.  This wouldn't normally be shown to users, but none
5598
            # of it is confidential really.
5599
            $thisone = $m->getPublic(FALSE, FALSE, TRUE);
×
5600

5601
            if (count($thisone['groups']) > 0) {
×
5602
                $g = Group::get($this->dbhr, $this->dbhm, $thisone['groups'][0]['groupid']);
×
5603
                $thisone['groups'][0]['namedisplay'] = $g->getName();
×
5604
            }
5605

5606
            $d['messages'][] = $thisone;
×
5607
        }
5608

5609
        # Chats.  Can't use listForUser as that filters on various things and has a ModTools vs FD distinction, and
5610
        # we're interested in information we have provided.  So we get the chats mentioned in the roster (we have
5611
        # provided information about being online) and where we have sent or reviewed a chat message.
5612
        error_log("...chats");
7✔
5613
        $chatids = $this->dbhr->preQuery("SELECT DISTINCT id, latestmessage FROM chat_rooms INNER JOIN (SELECT DISTINCT chatid FROM chat_roster WHERE userid = ? UNION SELECT DISTINCT chatid FROM chat_messages WHERE userid = ? OR reviewedby = ?) t ON t.chatid = chat_rooms.id ORDER BY latestmessage ASC;", [
7✔
5614
            $this->id,
7✔
5615
            $this->id,
7✔
5616
            $this->id
7✔
5617
        ]);
7✔
5618

5619
        $d['chatrooms'] = [];
7✔
5620
        $count = 0;
7✔
5621

5622
        foreach ($chatids as $chatid) {
7✔
5623
            # We don't return the chat name because it's too slow to produce.
5624
            $r = new ChatRoom($this->dbhr, $this->dbhm, $chatid['id']);
6✔
5625
            $thisone = [
6✔
5626
                'id' => $chatid['id'],
6✔
5627
                'name' => $r->getPublic($this)['name'],
6✔
5628
                'messages' => []
6✔
5629
            ];
6✔
5630

5631
            $sql = "SELECT date, lastip FROM chat_roster WHERE `chatid` = ? AND userid = ?;";
6✔
5632
            $roster = $this->dbhr->preQuery($sql, [$chatid['id'], $this->id]);
6✔
5633
            foreach ($roster as $rost) {
6✔
5634
                $thisone['lastip'] = $rost['lastip'];
6✔
5635
                $thisone['date'] = Utils::ISODate($rost['date']);
6✔
5636
            }
5637

5638
            # Get the messages we have sent in this chat.
5639
            $msgs = $this->dbhr->preQuery("SELECT id FROM chat_messages WHERE chatid = ? AND (userid = ? OR reviewedby = ?);", [
6✔
5640
                $chatid['id'],
6✔
5641
                $this->id,
6✔
5642
                $this->id
6✔
5643
            ]);
6✔
5644

5645
            $userlist = NULL;
6✔
5646

5647
            foreach ($msgs as $msg) {
6✔
5648
                $cm = new ChatMessage($this->dbhr, $this->dbhm, $msg['id']);
6✔
5649
                $thismsg = $cm->getPublic(FALSE, $userlist);
6✔
5650

5651
                # Strip out most of the refmsg detail - it's not ours and we need to save volume of data.
5652
                $refmsg = Utils::pres('refmsg', $thismsg);
6✔
5653

5654
                if ($refmsg) {
6✔
5655
                    $thismsg['refmsg'] = [
×
5656
                        'id' => $msg['id'],
×
5657
                        'subject' => Utils::presdef('subject', $refmsg, NULL)
×
5658
                    ];
×
5659
                }
5660

5661
                $thismsg['mine'] = Utils::presdef('userid', $thismsg, NULL) == $this->id;
6✔
5662
                $thismsg['date'] = Utils::ISODate($thismsg['date']);
6✔
5663
                $thisone['messages'][] = $thismsg;
6✔
5664

5665
                $count++;
6✔
5666
//
5667
//                if ($count > 200) {
5668
//                    break 2;
5669
//                }
5670
            }
5671

5672
            if (count($thisone['messages']) > 0) {
6✔
5673
                $d['chatrooms'][] = $thisone;
6✔
5674
            }
5675
        }
5676

5677
        error_log("...newsfeed");
7✔
5678
        $newsfeeds = $this->dbhr->preQuery("SELECT * FROM newsfeed WHERE userid = ?;", [
7✔
5679
            $this->id
7✔
5680
        ]);
7✔
5681

5682
        $d['newsfeed'] = [];
7✔
5683

5684
        foreach ($newsfeeds as $newsfeed) {
7✔
5685
            $n = new Newsfeed($this->dbhr, $this->dbhm, $newsfeed['id']);
6✔
5686
            $thisone = $n->getPublic(FALSE, FALSE, FALSE, FALSE);
6✔
5687
            $d['newsfeed'][] = $thisone;
6✔
5688
        }
5689

5690
        $d['newsfeed_unfollows'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_unfollow WHERE userid = ?;", [
7✔
5691
            $this->id
7✔
5692
        ]);
7✔
5693

5694
        foreach ($d['newsfeed_unfollows'] as &$dd) {
7✔
5695
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5696
        }
5697

5698
        $d['newsfeed_likes'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_likes WHERE userid = ?;", [
7✔
5699
            $this->id
7✔
5700
        ]);
7✔
5701

5702
        foreach ($d['newsfeed_likes'] as &$dd) {
7✔
5703
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5704
        }
5705

5706
        $d['newsfeed_reports'] = $this->dbhr->preQuery("SELECT * FROM newsfeed_reports WHERE userid = ?;", [
7✔
5707
            $this->id
7✔
5708
        ]);
7✔
5709

5710
        foreach ($d['newsfeed_reports'] as &$dd) {
7✔
5711
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5712
        }
5713

5714
        $d['aboutme'] = $this->dbhr->preQuery("SELECT timestamp, text FROM users_aboutme WHERE userid = ? AND LENGTH(text) > 5;", [
7✔
5715
            $this->id
7✔
5716
        ]);
7✔
5717

5718
        foreach ($d['aboutme'] as &$dd) {
7✔
5719
            $dd['timestamp'] = Utils::ISODate($dd['timestamp']);
×
5720
        }
5721

5722
        error_log("...stories");
7✔
5723
        $d['stories'] = $this->dbhr->preQuery("SELECT date, headline, story FROM users_stories WHERE userid = ?;", [
7✔
5724
            $this->id
7✔
5725
        ]);
7✔
5726

5727
        foreach ($d['stories'] as &$dd) {
7✔
5728
            $dd['date'] = Utils::ISODate($dd['date']);
×
5729
        }
5730

5731
        $d['stories_likes'] = $this->dbhr->preQuery("SELECT storyid FROM users_stories_likes WHERE userid = ?;", [
7✔
5732
            $this->id
7✔
5733
        ]);
7✔
5734

5735
        error_log("...exports");
7✔
5736
        $d['exports'] = $this->dbhr->preQuery("SELECT userid, started, completed FROM users_exports WHERE userid = ?;", [
7✔
5737
            $this->id
7✔
5738
        ]);
7✔
5739

5740
        foreach ($d['exports'] as &$dd) {
7✔
5741
            $dd['started'] = Utils::ISODate($dd['started']);
7✔
5742
            $dd['completed'] = Utils::ISODate($dd['completed']);
7✔
5743
        }
5744

5745
        error_log("...logs");
7✔
5746
        $l = new Log($this->dbhr, $this->dbhm);
7✔
5747
        $ctx = NULL;
7✔
5748
        $d['logs'] = $l->get(NULL, NULL, NULL, NULL, NULL, NULL, PHP_INT_MAX, $ctx, $this->id);
7✔
5749

5750
        error_log("...add group to logs");
7✔
5751
        $loggroups = [];
7✔
5752
        foreach ($d['logs'] as &$log) {
7✔
5753
            if (Utils::pres('groupid', $log)) {
7✔
5754
                # Don't put the whole group info in there, as it is slow to get.
5755
                if (!array_key_exists($log['groupid'], $loggroups)) {
7✔
5756
                    $g = Group::get($this->dbhr, $this->dbhm, $log['groupid']);
7✔
5757

5758
                    if ($g->getId() == $log['groupid']) {
7✔
5759
                        $loggroups[$log['groupid']] = [
7✔
5760
                            'id' => $log['groupid'],
7✔
5761
                            'nameshort' => $g->getPrivate('nameshort'),
7✔
5762
                            'namedisplay' => $g->getName()
7✔
5763
                        ];
7✔
5764
                    } else {
5765
                        $loggroups[$log['groupid']] = [
×
5766
                            'id' => $log['groupid'],
×
5767
                            'nameshort' => "DeletedGroup{$log['groupid']}",
×
5768
                            'namedisplay' => "Deleted group #{$log['groupid']}"
×
5769
                        ];
×
5770
                    }
5771
                }
5772

5773
                $log['group'] = $loggroups[$log['groupid']];
7✔
5774
            }
5775
        }
5776

5777
        # Gift aid
5778
        $don = new Donations($this->dbhr, $this->dbhm);
7✔
5779
        $d['giftaid'] = $don->getGiftAid($this->id);
7✔
5780

5781
        $ret = $d;
7✔
5782

5783
        # There are some other tables with information which we don't return.  Here's what and why:
5784
        # - Not part of the current UI so can't have any user data
5785
        #     polls_users
5786
        # - Covered by data that we do return from other tables
5787
        #     messages_drafts, messages_history, messages_groups, messages_outcomes,
5788
        #     messages_promises, users_modmails, modnotifs, users_dashboard,
5789
        #     users_nudges
5790
        # - Transient logging data
5791
        #     logs_emails, logs_sql, logs_api, logs_errors, logs_src
5792
        # - Not provided by the user themselves
5793
        #     user_comments, messages_reneged, spam_users, users_banned, users_stories_requested,
5794
        #     users_thanks
5795
        # - Inferred or derived data.  These are not considered to be provided by the user (see p10 of
5796
        #   http://ec.europa.eu/newsroom/document.cfm?doc_id=44099)
5797
        #     users_kudos, visualise
5798

5799
        # Compress the data in the DB because it can be huge.
5800
        #
5801
        error_log("...filter");
7✔
5802
        Utils::filterResult($ret);
7✔
5803
        error_log("...encode");
7✔
5804
        $data = json_encode($ret, JSON_PARTIAL_OUTPUT_ON_ERROR);
7✔
5805
        error_log("...encoded length " . strlen($data) . ", now compress");
7✔
5806
        $data = gzdeflate($data);
7✔
5807
        $this->dbhm->preExec("UPDATE users_exports SET completed = NOW(), data = ? WHERE id = ? AND tag = ?;", [
7✔
5808
            $data,
7✔
5809
            $exportid,
7✔
5810
            $tag
7✔
5811
        ]);
7✔
5812
        error_log("...completed, length " . strlen($data));
7✔
5813

5814
        return ($ret);
7✔
5815
    }
5816

5817
    function getExport($userid, $id, $tag)
5818
    {
5819
        $ret = NULL;
2✔
5820

5821
        $exports = $this->dbhr->preQuery("SELECT * FROM users_exports WHERE userid = ? AND id = ? AND tag = ?;", [
2✔
5822
            $userid,
2✔
5823
            $id,
2✔
5824
            $tag
2✔
5825
        ]);
2✔
5826

5827
        foreach ($exports as $export) {
2✔
5828
            $ret = $export;
2✔
5829
            $ret['requested'] = $ret['requested'] ? Utils::ISODate($ret['requested']) : NULL;
2✔
5830
            $ret['started'] = $ret['started'] ? Utils::ISODate($ret['started']) : NULL;
2✔
5831
            $ret['completed'] = $ret['completed'] ? Utils::ISODate($ret['completed']) : NULL;
2✔
5832

5833
            if ($ret['completed']) {
2✔
5834
                # This has completed.  Return the data.  Will be zapped in cron exports..
5835
                $ret['data'] = json_decode(gzinflate($export['data']), TRUE);
2✔
5836
                $ret['infront'] = 0;
2✔
5837
            } else {
5838
                # Find how many are in front of us.
5839
                $infront = $this->dbhr->preQuery("SELECT COUNT(*) AS count FROM users_exports WHERE id < ? AND completed IS NULL;", [
2✔
5840
                    $id
2✔
5841
                ]);
2✔
5842

5843
                $ret['infront'] = $infront[0]['count'];
2✔
5844
            }
5845
        }
5846

5847
        return ($ret);
2✔
5848
    }
5849

5850
    public function limbo() {
5851
        # We set the deleted attribute, which will cause the v2 Go API not to return any personal data.  The user
5852
        # can still log in, and potentially recover their account by calling session with PATCH of deleted = NULL.
5853
        # Otherwise a background script will purge their account after a couple of weeks.
5854
        #
5855
        # This allows us to handle the fairly common case of users deleting their accounts by mistake, or changing
5856
        # their minds.  This often happens because one-click unsubscribe in emails, which we need to have for
5857
        # delivery.
5858
        $this->dbhm->preExec("UPDATE users SET deleted = NOW() WHERE id = ?;", [
5✔
5859
            $this->id
5✔
5860
        ]);
5✔
5861

5862
        # Send email notification about account removal
5863
        $email = $this->getEmailPreferred();
5✔
5864
        if ($email) {
5✔
5865
            try {
5866
                $loader = new \Twig_Loader_Filesystem(IZNIK_BASE . '/mailtemplates/twig');
5✔
5867
                $twig = new \Twig_Environment($loader);
5✔
5868

5869
                list ($transport, $mailer) = Mail::getMailer();
5✔
5870

5871
                $html = $twig->render('limbo.html', [
5✔
5872
                    'site_url' => 'https://' . USER_SITE
5✔
5873
                ]);
5✔
5874

5875
                $message = \Swift_Message::newInstance()
5✔
5876
                    ->setSubject("Your Freegle account has been removed as requested")
5✔
5877
                    ->setFrom([NOREPLY_ADDR => SITE_NAME])
5✔
5878
                    ->setReplyTo(SUPPORT_ADDR)
5✔
5879
                    ->setTo($email)
5✔
5880
                    ->setBody("We've removed your Freegle account as requested. Your personal data has been marked for deletion and will be completely removed from our systems within 14 days.\n\nIf you changed your mind or removed your account by mistake, don't worry! You can still reactivate it by visiting " . USER_SITE . " and logging back in.\n\nOnce you visit the site and log back in, your account will be automatically restored with all your data intact.\n\nIf you meant to remove your account, you can safely ignore this email. After 14 days, your data will be permanently deleted and cannot be recovered.");
5✔
5881

5882
                # Add HTML in base-64 as default quoted-printable encoding leads to problems on
5883
                # Outlook.
5884
                $htmlPart = \Swift_MimePart::newInstance();
5✔
5885
                $htmlPart->setCharset('utf-8');
5✔
5886
                $htmlPart->setEncoder(new \Swift_Mime_ContentEncoder_Base64ContentEncoder);
5✔
5887
                $htmlPart->setContentType('text/html');
5✔
5888
                $htmlPart->setBody($html);
5✔
5889
                $message->attach($htmlPart);
5✔
5890

5891
                Mail::addHeaders($this->dbhr, $this->dbhm, $message, Mail::LIMBO, $this->id);
5✔
5892
                $this->sendIt($mailer, $message);
5✔
5893
            } catch (\Exception $e) {
×
5894
                error_log("Failed to send limbo email to user {$this->id}: " . $e->getMessage());
×
5895
            }
5896
        }
5897
    }
5898

5899
    public function processForgets($id = NULL) {
5900
        $count = 0;
1✔
5901

5902
        $idq = $id ? "AND id = $id" : "";
1✔
5903
        $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✔
5904

5905
        foreach ($users as $user) {
1✔
5906
            $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
5907
            $u->forget('Grace period');
1✔
5908
            $count++;
1✔
5909
        }
5910

5911
        return $count;
1✔
5912
    }
5913

5914
    public function forget($reason)
5915
    {
5916
        # Wipe a user of personal data, for the GDPR right to be forgotten.  We don't delete the user entirely
5917
        # otherwise it would mess up the stats.
5918

5919
        # Clear name etc.
5920
        $this->setPrivate('firstname', NULL);
11✔
5921
        $this->setPrivate('lastname', NULL);
11✔
5922
        $this->setPrivate('fullname', "Deleted User #" . $this->id);
11✔
5923
        $this->setPrivate('settings', NULL);
11✔
5924
        $this->setPrivate('yahooid', NULL);
11✔
5925

5926
        # Delete emails which aren't ours.
5927
        $emails = $this->getEmails();
11✔
5928

5929
        foreach ($emails as $email) {
11✔
5930
            if (!$email['ourdomain']) {
8✔
5931
                $this->removeEmail($email['email']);
8✔
5932
            }
5933
        }
5934

5935
        # Delete all logins.
5936
        $this->dbhm->preExec("DELETE FROM users_logins WHERE userid = ?;", [
11✔
5937
            $this->id
11✔
5938
        ]);
11✔
5939

5940
        # Delete the content (but not subject) of any messages, and any email header information such as their
5941
        # name and email address.
5942
        $msgs = $this->dbhm->preQuery("SELECT id FROM messages WHERE fromuser = ? AND messages.type IN (?, ?);", [
11✔
5943
            $this->id,
11✔
5944
            Message::TYPE_OFFER,
11✔
5945
            Message::TYPE_WANTED
11✔
5946
        ]);
11✔
5947

5948
        foreach ($msgs as $msg) {
11✔
5949
            $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✔
5950
                $msg['id']
1✔
5951
            ]);
1✔
5952

5953
            $this->dbhm->preExec("UPDATE messages_groups SET deleted = 1 WHERE msgid = ?;", [
1✔
5954
                $msg['id']
1✔
5955
            ]);
1✔
5956

5957
            # Delete outcome comments that they've added - just about might have personal data.
5958
            $this->dbhm->preExec("UPDATE messages_outcomes SET comments = NULL WHERE msgid = ?;", [
1✔
5959
                $msg['id']
1✔
5960
            ]);
1✔
5961

5962
            $m = new Message($this->dbhr, $this->dbhm, $msg['id']);
1✔
5963

5964
            if (!$m->hasOutcome()) {
1✔
5965
                $m->withdraw('Withdrawn on user unsubscribe', NULL);
1✔
5966
            }
5967
        }
5968

5969
        # Remove all the content of all chat messages which they have sent (but not received).
5970
        $msgs = $this->dbhm->preQuery("SELECT id FROM chat_messages WHERE userid = ?;", [
11✔
5971
            $this->id
11✔
5972
        ]);
11✔
5973

5974
        foreach ($msgs as $msg) {
11✔
5975
            $this->dbhm->preExec("UPDATE chat_messages SET message = NULL WHERE id = ?;", [
1✔
5976
                $msg['id']
1✔
5977
            ]);
1✔
5978
        }
5979

5980
        # Delete completely any community events, volunteering opportunities, newsfeed posts, searches and stories
5981
        # they have created (their personal details might be in there), and any ratings by or about them.
5982
        $this->dbhm->preExec("DELETE FROM communityevents WHERE userid = ?;", [
11✔
5983
            $this->id
11✔
5984
        ]);
11✔
5985
        $this->dbhm->preExec("DELETE FROM volunteering WHERE userid = ?;", [
11✔
5986
            $this->id
11✔
5987
        ]);
11✔
5988
        $this->dbhm->preExec("DELETE FROM newsfeed WHERE userid = ?;", [
11✔
5989
            $this->id
11✔
5990
        ]);
11✔
5991
        $this->dbhm->preExec("DELETE FROM users_stories WHERE userid = ?;", [
11✔
5992
            $this->id
11✔
5993
        ]);
11✔
5994
        $this->dbhm->preExec("DELETE FROM users_searches WHERE userid = ?;", [
11✔
5995
            $this->id
11✔
5996
        ]);
11✔
5997
        $this->dbhm->preExec("DELETE FROM users_aboutme WHERE userid = ?;", [
11✔
5998
            $this->id
11✔
5999
        ]);
11✔
6000
        $this->dbhm->preExec("DELETE FROM ratings WHERE rater = ?;", [
11✔
6001
            $this->id
11✔
6002
        ]);
11✔
6003
        $this->dbhm->preExec("DELETE FROM ratings WHERE ratee = ?;", [
11✔
6004
            $this->id
11✔
6005
        ]);
11✔
6006

6007
        # Remove them from all groups.
6008
        $membs = $this->getMemberships();
11✔
6009

6010
        foreach ($membs as $memb) {
11✔
6011
            $this->removeMembership($memb['id']);
7✔
6012
        }
6013

6014
        # Delete any postal addresses
6015
        $this->dbhm->preExec("DELETE FROM users_addresses WHERE userid = ?;", [
11✔
6016
            $this->id
11✔
6017
        ]);
11✔
6018

6019
        # Delete any profile images
6020
        $this->dbhm->preExec("DELETE FROM users_images WHERE userid = ?;", [
11✔
6021
            $this->id
11✔
6022
        ]);
11✔
6023

6024
        # Remove any promises.
6025
        $this->dbhm->preExec("DELETE FROM messages_promises WHERE userid = ?;", [
11✔
6026
            $this->id
11✔
6027
        ]);
11✔
6028

6029
        $this->dbhm->preExec("UPDATE users SET forgotten = NOW(), tnuserid = NULL WHERE id = ?;", [
11✔
6030
            $this->id
11✔
6031
        ]);
11✔
6032

6033
        $this->dbhm->preExec("DELETE FROM sessions WHERE userid = ?;", [
11✔
6034
            $this->id
11✔
6035
        ]);
11✔
6036

6037
        $l = new Log($this->dbhr, $this->dbhm);
11✔
6038
        $l->log([
11✔
6039
            'type' => Log::TYPE_USER,
11✔
6040
            'subtype' => Log::SUBTYPE_DELETED,
11✔
6041
            'user' => $this->id,
11✔
6042
            'text' => $reason
11✔
6043
        ]);
11✔
6044
    }
6045

6046
    public function userRetention($userid = NULL)
6047
    {
6048
        # Find users who:
6049
        # - were added six months ago
6050
        # - are not on any groups
6051
        # - have not logged in for six months
6052
        # - are not on the spammer list
6053
        # - do not have mod notes
6054
        # - have no logs for six months
6055
        #
6056
        # We have no good reason to keep any data about them, and should therefore purge them.
6057
        $count = 0;
1✔
6058
        $userq = $userid ? " users.id = $userid AND " : '';
1✔
6059
        $mysqltime = date("Y-m-d", strtotime("6 months ago"));
1✔
6060
        
6061
        # First, delete users with @yahoogroups.com emails (test/old emails)
6062
        $yahoosql = "SELECT DISTINCT users.id FROM users 
1✔
6063
                     INNER JOIN users_emails ON users.id = users_emails.userid 
6064
                     WHERE $userq users_emails.email LIKE '%@yahoogroups.com' AND users.deleted IS NULL;";
1✔
6065
        $yahooUsers = $this->dbhr->preQuery($yahoosql);
1✔
6066
        
6067
        foreach ($yahooUsers as $user) {
1✔
6068
            error_log("Deleting Yahoo Groups user #{$user['id']}");
×
6069
            $u = new User($this->dbhr, $this->dbhm, $user['id']);
×
6070
            $u->delete();
×
6071
            $count++;
×
6072
            
6073
            # Prod garbage collection
6074
            User::clearCache();
×
6075
            gc_collect_cycles();
×
6076
        }
6077
        
6078
        $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✔
6079
        $users = $this->dbhr->preQuery($sql, [
1✔
6080
            User::SYSTEMROLE_USER
1✔
6081
        ]);
1✔
6082

6083
        foreach ($users as $user) {
1✔
6084
            $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✔
6085
                $user['id'],
1✔
6086
                Log::TYPE_USER,
1✔
6087
                Log::SUBTYPE_CREATED,
1✔
6088
                Log::SUBTYPE_DELETED
1✔
6089
            ]);
1✔
6090

6091
            error_log("#{$user['id']} Found logs " . count($logs) . " age " . (count($logs) > 0 ? $logs['0']['logsago'] : ' none '));
1✔
6092

6093
            if (count($logs) == 0 || $logs[0]['logsago'] > 90) {
1✔
6094
                error_log("...forget user #{$user['id']} " . (count($logs) > 0 ? $logs[0]['logsago'] : ''));
1✔
6095
                $u = new User($this->dbhr, $this->dbhm, $user['id']);
1✔
6096
                $u->forget('Inactive');
1✔
6097
                $count++;
1✔
6098
            }
6099

6100
            # Prod garbage collection, as we've seen high memory usage by this.
6101
            User::clearCache();
1✔
6102
            gc_collect_cycles();
1✔
6103
        }
6104

6105
        # The only reason for preserving deleted users is as a placeholder user for messages they sent.  If they
6106
        # don't have any messages, they can go.
6107
        $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✔
6108
            $mysqltime
1✔
6109
        ]);
1✔
6110

6111
        $total = count($ids);
1✔
6112
        $count = 0;
1✔
6113

6114
        foreach ($ids as $id) {
1✔
6115
            $u = new User($this->dbhr, $this->dbhm, $id['id']);
1✔
6116
            #error_log("...delete user #{$id['id']}");
6117
            $u->delete();
1✔
6118

6119
            $count++;
1✔
6120

6121
            if ($count % 1000 == 0) {
1✔
6122
                error_log("...delete $count / $total");
×
6123
            }
6124

6125

6126
            # Prod garbage collection, as we've seen high memory usage by this.
6127
            User::clearCache();
1✔
6128
            gc_collect_cycles();
1✔
6129
        }
6130

6131
        return ($count);
1✔
6132
    }
6133

6134
    public function recordActive()
6135
    {
6136
        # We record this on an hourly basis.  Avoid pointless mod ops for cluster health.
6137
        $now = date("Y-m-d H:00:00", time());
2✔
6138
        $already = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ? AND timestamp = ?;", [
2✔
6139
            $this->id,
2✔
6140
            $now
2✔
6141
        ]);
2✔
6142

6143
        if (count($already) == 0) {
2✔
6144
            $this->dbhm->background("INSERT IGNORE INTO users_active (userid, timestamp) VALUES ({$this->id}, '$now');");
2✔
6145
        }
6146
    }
6147

6148
    public function getActive()
6149
    {
6150
        $active = $this->dbhr->preQuery("SELECT * FROM users_active WHERE userid = ?;", [$this->id]);
1✔
6151
        return ($active);
1✔
6152
    }
6153

6154
    public function mostActive($gid, $limit = 20)
6155
    {
6156
        $limit = intval($limit);
1✔
6157
        $earliest = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6158

6159
        $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✔
6160
            $gid,
1✔
6161
            User::SYSTEMROLE_USER,
1✔
6162
            $earliest
1✔
6163
        ]);
1✔
6164

6165
        $ret = [];
1✔
6166

6167
        foreach ($users as $user) {
1✔
6168
            $u = User::get($this->dbhr, $this->dbhm, $user['userid']);
1✔
6169
            $thisone = $u->getPublic();
1✔
6170
            $thisone['groupid'] = $gid;
1✔
6171
            $thisone['email'] = $u->getEmailPreferred();
1✔
6172

6173
            if (Utils::pres('memberof', $thisone)) {
1✔
6174
                foreach ($thisone['memberof'] as $group) {
1✔
6175
                    if ($group['id'] == $gid) {
1✔
6176
                        $thisone['joined'] = $group['added'];
1✔
6177
                    }
6178
                }
6179
            }
6180

6181
            $ret[] = $thisone;
1✔
6182
        }
6183

6184
        return ($ret);
1✔
6185
    }
6186

6187
    public function setAboutMe($text) {
6188
        $this->dbhm->preExec("INSERT INTO users_aboutme (userid, text) VALUES (?, ?);", [
3✔
6189
            $this->id,
3✔
6190
            $text
3✔
6191
        ]);
3✔
6192

6193
        $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id = {$this->id};");
3✔
6194

6195
        return($this->dbhm->lastInsertId());
3✔
6196
    }
6197

6198
    public function rate($rater, $ratee, $rating, $reason = NULL, $text = NULL) {
6199
        $ret = NULL;
2✔
6200

6201
        if ($rater != $ratee) {
2✔
6202
            # Can't rate yourself.
6203
            $review = $rating == User::RATING_DOWN && $reason && $text;
2✔
6204
            $this->dbhm->preExec("REPLACE INTO ratings (rater, ratee, rating, reason, text, timestamp, reviewrequired) VALUES (?, ?, ?, ?, ?, NOW(), ?);", [
2✔
6205
                $rater,
2✔
6206
                $ratee,
2✔
6207
                $rating,
2✔
6208
                $reason,
2✔
6209
                $text,
2✔
6210
                $review
2✔
6211
            ]);
2✔
6212

6213
            $ret = $this->dbhm->lastInsertId();
2✔
6214

6215
            $this->dbhm->background("UPDATE users SET lastupdated = NOW() WHERE id IN ($rater, $ratee);");
2✔
6216
        }
6217

6218
        return($ret);
2✔
6219
    }
6220

6221
    public function getRatings($uids) {
6222
        $mysqltime = date("Y-m-d", strtotime("Midnight 182 days ago"));
131✔
6223
        $ret = [];
131✔
6224
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
131✔
6225
        $myid = $me ? $me->getId() : NULL;
131✔
6226

6227
        # We show visible ratings, ones we have made ourselves, or those from TN.
6228
        $sql = "SELECT ratee, COUNT(*) AS count, rating FROM ratings WHERE ratee IN (" . implode(',', $uids) . ") AND (timestamp >= '$mysqltime' OR rater = ?) AND (tn_rating_id IS NOT NULL OR rater = ? OR visible = 1) GROUP BY rating, ratee;";
131✔
6229
        $ratings = $this->dbhr->preQuery($sql, [ $myid, $myid ]);
131✔
6230

6231
        foreach ($uids as $uid) {
131✔
6232
            $ret[$uid] = [
131✔
6233
                User::RATING_UP => 0,
131✔
6234
                User::RATING_DOWN => 0,
131✔
6235
                User::RATING_MINE => NULL
131✔
6236
            ];
131✔
6237

6238
            foreach ($ratings as $rate) {
131✔
6239
                if ($rate['ratee'] == $uid) {
1✔
6240
                    $ret[$uid][$rate['rating']] = $rate['count'];
1✔
6241
                }
6242
            }
6243
        }
6244

6245
        $ratings = $this->dbhr->preQuery("SELECT rating, ratee FROM ratings WHERE ratee IN (" . implode(',', $uids) . ") AND rater = ? AND timestamp >= '$mysqltime';", [
131✔
6246
            $myid
131✔
6247
        ]);
131✔
6248

6249
        foreach ($uids as $uid) {
131✔
6250
            if ($myid != $this->id) {
131✔
6251
                # We can't rate ourselves, so don't bother checking.
6252

6253
                foreach ($ratings as $rating) {
80✔
6254
                    if ($rating['ratee'] == $uid) {
1✔
6255
                        $ret[$uid][User::RATING_MINE] = $rating['rating'];
1✔
6256
                    }
6257
                }
6258
            }
6259
        }
6260

6261
        return($ret);
131✔
6262
    }
6263

6264
    public function getAllRatings($since) {
6265
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6266

6267
        $sql = "SELECT * FROM ratings WHERE timestamp >= ? AND visible = 1;";
1✔
6268
        $ratings = $this->dbhr->preQuery($sql, [
1✔
6269
            $mysqltime
1✔
6270
        ]);
1✔
6271

6272
        foreach ($ratings as &$rating) {
1✔
6273
            $rating['timestamp'] = Utils::ISODate($rating['timestamp']);
1✔
6274
        }
6275

6276
        return $ratings;
1✔
6277
    }
6278

6279
    public function getVisibleRatings($unreviewedonly, $since = '7 days ago') {
6280
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
3✔
6281
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
3✔
6282

6283
        $modships = $me->getModeratorships(NULL, TRUE);
3✔
6284

6285
        $ret = [];
3✔
6286
        $revq = $unreviewedonly ? " AND reviewrequired = 1" : '';
3✔
6287

6288
        if (count($modships)) {
3✔
6289
            $sql = "SELECT ratings.*, m1.groupid,
3✔
6290
       CASE WHEN u1.fullname IS NOT NULL THEN u1.fullname ELSE CONCAT(u1.firstname, ' ', u1.lastname) END AS raterdisplayname,
6291
       CASE WHEN u2.fullname IS NOT NULL THEN u2.fullname ELSE CONCAT(u2.firstname, ' ', u2.lastname) END AS rateedisplayname
6292
    FROM ratings 
6293
    INNER JOIN memberships m1 ON m1.userid = ratings.rater
6294
    INNER JOIN memberships m2 ON m2.userid = ratings.ratee
6295
    INNER JOIN users u1 ON ratings.rater = u1.id
6296
    INNER JOIN users u2 ON ratings.ratee = u2.id
6297
    WHERE ratings.timestamp >= ? AND 
6298
        m1.groupid IN (" . implode(',', $modships) . ") AND
3✔
6299
        m2.groupid IN (" . implode(',', $modships) . ") AND
3✔
6300
        m1.groupid = m2.groupid AND
6301
        ratings.rating IS NOT NULL 
6302
        $revq    
3✔
6303
        GROUP BY ratings.rater ORDER BY ratings.timestamp DESC;";
3✔
6304

6305
            $ret = $this->dbhr->preQuery($sql, [
3✔
6306
                $mysqltime
3✔
6307
            ]);
3✔
6308

6309
            foreach ($ret as &$r) {
3✔
6310
                $r['timestamp'] = Utils::ISODate($r['timestamp']);
1✔
6311
            }
6312
        }
6313

6314
        return $ret;
3✔
6315
    }
6316

6317
    public function ratingReviewed($ratingid) {
6318
        $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6319

6320
        $unreviewed = $me->getVisibleRatings(TRUE);
1✔
6321

6322
        foreach ($unreviewed as $r) {
1✔
6323
            if ($r['id'] == $ratingid) {
1✔
6324
                $this->dbhm->preExec("UPDATE ratings SET reviewrequired = 0 WHERE id = ?;", [
1✔
6325
                    $ratingid
1✔
6326
                ]);
1✔
6327
            }
6328
        }
6329
    }
6330

6331
    public function getChanges($since) {
6332
        $mysqltime = date("Y-m-d H:i:s", strtotime($since));
1✔
6333

6334
        $users = $this->dbhr->preQuery("SELECT id, lastupdated FROM users WHERE lastupdated >= ?;", [
1✔
6335
            $mysqltime
1✔
6336
        ]);
1✔
6337

6338
        foreach ($users as &$user) {
1✔
6339
            $user['lastupdated'] = Utils::ISODate($user['lastupdated']);
1✔
6340
        }
6341

6342
        return $users;
1✔
6343
    }
6344

6345
    public function getRated() {
6346
        $rateds = $this->dbhr->preQuery("SELECT * FROM ratings WHERE rater = ?;", [
8✔
6347
            $this->id
8✔
6348
        ]);
8✔
6349

6350
        foreach ($rateds as &$rate) {
8✔
6351
            $rate['timestamp'] = Utils::ISODate($rate['timestamp']);
1✔
6352
        }
6353

6354
        return($rateds);
8✔
6355
    }
6356

6357
    public function getActiveSince($since, $createdbefore, $uid = NULL) {
6358
        $sincetime = date("Y-m-d H:i:s", strtotime($since));
1✔
6359
        $beforetime = date("Y-m-d H:i:s", strtotime($createdbefore));
1✔
6360
        $ids = $uid ? [
1✔
6361
            [
1✔
6362
                'id' => $uid
1✔
6363
            ]
1✔
6364
        ] : $this->dbhr->preQuery("SELECT id FROM users WHERE lastaccess >= ? AND added <= ?;", [
1✔
6365
            $sincetime,
1✔
6366
            $beforetime
1✔
6367
        ]);
1✔
6368

6369
        return(count($ids) ? array_filter(array_column($ids, 'id')) : []);
1✔
6370
    }
6371

6372
    public static function encodeId($id) {
6373
        # We're told that this is affecting our spam rating.  Let's see.
6374
        return '';
9✔
6375
//        $bin = base_convert($id, 10, 2);
6376
//        $bin = str_replace('0', '-', $bin);
6377
//        $bin = str_replace('1', '~', $bin);
6378
//        return($bin);
6379
    }
6380

6381
    public static function decodeId($enc) {
6382
        $enc = trim($enc);
×
6383
        $enc = str_replace('-', '0', $enc);
×
6384
        $enc = str_replace('~', '1', $enc);
×
6385
        $id  = base_convert($enc, 2, 10);
×
6386
        return($id);
×
6387
    }
6388

6389
    public function getCity()
6390
    {
6391
        $city = NULL;
23✔
6392

6393
        # Find the closest town
6394
        list ($lat, $lng, $loc) = $this->getLatLng(FALSE, TRUE);
23✔
6395

6396
        if ($lat || $lng) {
23✔
6397
            $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✔
6398
            #error_log("Get $sql, $lng, $lat");
6399
            $towns = $this->dbhr->preQuery($sql);
1✔
6400

6401
            foreach ($towns as $town) {
1✔
6402
                $city = $town['name'];
1✔
6403
            }
6404
        }
6405

6406
        return([ $city, $lat, $lng ]);
23✔
6407
    }
6408

6409
    public function microVolunteering() {
6410
        // Are we on a group where microvolunteering is enabled.
6411
        $groups = $this->dbhr->preQuery("SELECT memberships.id FROM memberships INNER JOIN `groups` ON groups.id = memberships.groupid WHERE userid = ? AND microvolunteering = 1 LIMIT 1;", [
22✔
6412
            $this->id
22✔
6413
        ]);
22✔
6414

6415
        return count($groups);
22✔
6416
    }
6417

6418
    public function getJobAds() {
6419
        # We want to show a few job ads from nearby.
6420
        $search = NULL;
35✔
6421
        $ret = '<span class="jobads">';
35✔
6422

6423
        list ($lat, $lng) = $this->getLatLng();
35✔
6424

6425
        if ($lat || $lng) {
35✔
6426
            $j = new Jobs($this->dbhr, $this->dbhm);
5✔
6427
            $jobs = $j->query($lat, $lng, 4);
5✔
6428

6429
            foreach ($jobs as $job) {
5✔
6430
                $loc = Utils::presdef('location', $job, '');
3✔
6431
                $title = "{$job['title']}" . ($loc !== ' ' ? " ($loc)" : '');
3✔
6432

6433
                # Link via our site to avoid spam trap warnings.
6434
                $url = "https://" . USER_SITE . "/job/{$job['id']}";
3✔
6435
                $ret .= '<a href="' . $url . '" target="_blank" style="color:black; font-weight:bold;">' . htmlentities($title) . '</a><br />';
3✔
6436
            }
6437
        }
6438

6439
        $ret .= '</span>';
35✔
6440

6441
        return([
35✔
6442
            'location' => $search,
35✔
6443
            'jobs' => $ret
35✔
6444
        ]);
35✔
6445
    }
6446

6447
    public function updateModMails($uid = NULL) {
6448
        # We maintain a count of recent modmails by scanning logs regularly, and pruning old ones.  This means we can
6449
        # find the value in a well-indexed way without the disk overhead of having a two-column index on logs.
6450
        #
6451
        # Ignore logs where the user is the same as the byuser - for example a user can delete their own posts, and we are
6452
        # only interested in things where a mod has done something to another user.
6453
        $mysqltime = date("Y-m-d H:i:s", strtotime("10 minutes ago"));
1✔
6454
        $uidq = $uid ? " AND user = $uid " : '';
1✔
6455

6456
        $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✔
6457
            $mysqltime
1✔
6458
        ]);
1✔
6459

6460
        foreach ($logs as $log) {
1✔
6461
            $this->dbhm->preExec("INSERT IGNORE INTO users_modmails (userid, logid, timestamp, groupid) VALUES (?,?,?,?);", [
1✔
6462
                $log['user'],
1✔
6463
                $log['id'],
1✔
6464
                $log['timestamp'],
1✔
6465
                $log['groupid']
1✔
6466
            ]);
1✔
6467
        }
6468

6469
        # Prune old ones.
6470
        $mysqltime = date("Y-m-d", strtotime("Midnight 30 days ago"));
1✔
6471
        $uidq2 = $uid ? " AND userid = $uid " : '';
1✔
6472

6473
        $logs = $this->dbhr->preQuery("SELECT id FROM users_modmails WHERE timestamp < ? $uidq2;", [
1✔
6474
            $mysqltime
1✔
6475
        ]);
1✔
6476

6477
        foreach ($logs as $log) {
1✔
6478
            $this->dbhm->preExec("DELETE FROM users_modmails WHERE id = ?;", [ $log['id'] ], FALSE);
×
6479
        }
6480
    }
6481

6482
    public function getModGroupsByActivity() {
6483
        $start = date('Y-m-d', strtotime("60 days ago"));
1✔
6484
        $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✔
6485
        return $this->dbhr->preQuery($sql, [
1✔
6486
            $this->id
1✔
6487
        ]);
1✔
6488
    }
6489

6490
    public function related($userlist) {
6491
        $userlist = array_unique($userlist);
2✔
6492

6493
        foreach ($userlist as $user1) {
2✔
6494
            foreach ($userlist as $user2) {
2✔
6495
                if ($user1 && $user2 && $user1 !== $user2) {
2✔
6496
                    # We may be passed user ids which no longer exist.
6497
                    $u1 = User::get($this->dbhr, $this->dbhm, $user1);
2✔
6498
                    $u2 = User::get($this->dbhr, $this->dbhm, $user2);
2✔
6499

6500
                    if ($u1->getId() && $u2->getId() && !$u1->isAdminOrSupport() && !$u2->isAdminOrSupport()) {
2✔
6501
                        $this->dbhm->background("INSERT INTO users_related (user1, user2) VALUES ($user1, $user2) ON DUPLICATE KEY UPDATE timestamp = NOW();");
2✔
6502
                    }
6503
                }
6504
            }
6505
        }
6506
    }
6507

6508
    public function getRelated($userid, $since = "30 days ago") {
6509
        $starttime = date("Y-m-d H:i:s", strtotime($since));
1✔
6510
        $users = $this->dbhr->preQuery("SELECT * FROM users_related WHERE user1 = ? AND timestamp >= '$starttime';", [
1✔
6511
            $userid
1✔
6512
        ]);
1✔
6513

6514
        return ($users);
1✔
6515
    }
6516

6517
    public function listRelated($groupids, &$ctx, $limit = 10) {
6518
        # The < condition ensures we don't duplicate during a single run.
6519
        $limit = intval($limit);
1✔
6520
        $ret = [];
1✔
6521
        $backstop = 100;
1✔
6522

6523
        do {
6524
            $ctx = $ctx ? $ctx : [ 'id'  => NULL ];
1✔
6525

6526
            if ($groupids && count($groupids)) {
1✔
6527
                $ctxq = ($ctx && intval($ctx['id'])) ? (" WHERE id < " . intval($ctx['id'])) : '';
1✔
6528
                $groupq = "(" . implode(',', $groupids) . ")";
1✔
6529
                $sql = "SELECT DISTINCT id, user1, user2 FROM (
1✔
6530
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6531
INNER JOIN memberships ON users_related.user1 = memberships.userid 
6532
INNER JOIN users u1 ON users_related.user1 = u1.id AND u1.deleted IS NULL AND u1.systemrole = 'User'
6533
WHERE 
6534
user1 < user2 AND
6535
notified = 0 AND
6536
memberships.groupid IN $groupq UNION
1✔
6537
SELECT users_related.id, user1, user2, memberships.groupid FROM users_related 
6538
INNER JOIN memberships ON users_related.user2 = memberships.userid 
6539
INNER JOIN users u2 ON users_related.user2 = u2.id AND u2.deleted IS NULL AND u2.systemrole = 'User'
6540
WHERE 
6541
user1 < user2 AND
6542
notified = 0 AND
6543
memberships.groupid IN $groupq 
1✔
6544
) t $ctxq ORDER BY id DESC LIMIT $limit;";
1✔
6545
                $members = $this->dbhr->preQuery($sql);
1✔
6546
            } else {
6547
                $ctxq = ($ctx && intval($ctx['id'])) ? (" AND users_related.id < " . intval($ctx['id'])) : '';
1✔
6548
                $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✔
6549
                $members = $this->dbhr->preQuery($sql, NULL, FALSE, FALSE);
1✔
6550
            }
6551

6552
            $uids1 = array_column($members, 'user1');
1✔
6553
            $uids2 = array_column($members, 'user2');
1✔
6554

6555
            $related = [];
1✔
6556
            foreach ($members as $member) {
1✔
6557
                $related[$member['user1']] = $member['user2'];
1✔
6558
                $ctx['id'] = $member['id'];
1✔
6559
            }
6560

6561
            $users = $this->getPublicsById(array_merge($uids1, $uids2));
1✔
6562

6563
            foreach ($users as &$user1) {
1✔
6564
                if (Utils::pres($user1['id'], $related)) {
1✔
6565
                    $thisone = $user1;
1✔
6566

6567
                    foreach ($users as $user2) {
1✔
6568
                        if ($user2['id'] == $related[$user1['id']]) {
1✔
6569
                            $user2['userid'] = $user2['id'];
1✔
6570
                            $thisone['relatedto'] = $user2;
1✔
6571
                            break;
1✔
6572
                        }
6573
                    }
6574

6575
                    $logins = $this->getLogins(FALSE, $thisone['id'], TRUE);
1✔
6576
                    $rellogins = $this->getLogins(FALSE, $thisone['relatedto']['id'], TRUE);
1✔
6577

6578
                    if ($thisone['deleted'] ||
1✔
6579
                        $thisone['relatedto']['deleted'] ||
1✔
6580
                        $thisone['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6581
                        $thisone['relatedto']['systemrole'] != User::SYSTEMROLE_USER ||
1✔
6582
                        !count($logins) ||
1✔
6583
                        !count($rellogins)) {
1✔
6584
                        # No sense in telling people about these.
6585
                        #
6586
                        # If there are n valid login types for one of the users - no way they can log in again so no point notifying.
6587
                        $this->dbhm->preExec("UPDATE users_related SET notified = 1 WHERE (user1 = ? AND user2 = ?) OR (user1 = ? AND user2 = ?);", [
1✔
6588
                            $thisone['id'],
1✔
6589
                            $thisone['relatedto']['id'],
1✔
6590
                            $thisone['relatedto']['id'],
1✔
6591
                            $thisone['id']
1✔
6592
                        ]);
1✔
6593
                    } else {
6594
                        $thisone['userid'] = $thisone['id'];
1✔
6595
                        $thisone['logins'] = $logins;
1✔
6596
                        $thisone['relatedto']['logins'] = $rellogins;
1✔
6597

6598
                        $ret[] = $thisone;
1✔
6599
                    }
6600
                }
6601
            }
6602

6603
            $backstop--;
1✔
6604
        } while ($backstop > 0 && count($ret) < $limit && count($members));
1✔
6605

6606
        return $ret;
1✔
6607
    }
6608

6609
    public function getExpectedReplies($uids, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6610
        # We count replies where the user has been active since the reply was requested, which means they've had
6611
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6612
        #
6613
        # $since here has to match the value in ChatRoom::
6614
        $starttime = date("Y-m-d H:i:s", strtotime($since));
131✔
6615
        $replies = $this->dbhr->preQuery("SELECT COUNT(*) AS count, expectee FROM users_expected INNER JOIN users ON users.id = users_expected.expectee INNER JOIN chat_messages ON chat_messages.id = users_expected.chatmsgid WHERE expectee IN (" . implode(',', $uids) . ") AND chat_messages.date >= '$starttime' AND replyexpected = 1 AND replyreceived = 0 AND TIMESTAMPDIFF(MINUTE, chat_messages.date, users.lastaccess) >= ?", [
131✔
6616
            $grace
131✔
6617
        ]);
131✔
6618

6619
        return($replies);
131✔
6620
    }
6621

6622
    public function listExpectedReplies($uid, $since = ChatRoom::ACTIVELIM, $grace = ChatRoom::REPLY_GRACE) {
6623
        # We count replies where the user has been active since the reply was requested, which means they've had
6624
        # a chance to reply, plus a grace period in minutes, so that if they're active right now we don't penalise them.
6625
        #
6626
        # $since here has to match the value in ChatRoom::
6627
        $starttime = date("Y-m-d H:i:s", strtotime($since));
21✔
6628
        $replies = $this->dbhr->preQuery("SELECT chatid FROM users_expected INNER JOIN users ON users.id = users_expected.expectee INNER JOIN chat_messages ON chat_messages.id = users_expected.chatmsgid WHERE expectee = ? AND chat_messages.date >= '$starttime' AND replyexpected = 1 AND replyreceived = 0 AND TIMESTAMPDIFF(MINUTE, chat_messages.date, users.lastaccess) > ?", [
21✔
6629
            $uid,
21✔
6630
            $grace
21✔
6631
        ]);
21✔
6632

6633
        $ret = [];
21✔
6634

6635
        if (count($replies)) {
21✔
6636
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
1✔
6637
            $myid = $me ? $me->getId() : NULL;
1✔
6638

6639
            $r = new ChatRoom($this->dbhr, $this->dbhm);
1✔
6640
            $rooms = $r->fetchRooms(array_column($replies, 'chatid'), $myid, TRUE);
1✔
6641

6642
            foreach ($rooms as $room) {
1✔
6643
                $ret[] = [
1✔
6644
                    'id' => $room['id'],
1✔
6645
                    'name' => $room['name']
1✔
6646
                ];
1✔
6647
            }
6648
        }
6649

6650
        return $ret;
21✔
6651
    }
6652
    
6653
    public function getWorkCounts($groups = NULL) {
6654
        # Tell them what mod work there is.  Similar code in Notifications.
6655
        $ret = [];
25✔
6656
        $total = 0;
25✔
6657

6658
        $national = $this->hasPermission(User::PERM_NATIONAL_VOLUNTEERS);
25✔
6659

6660
        if ($national) {
25✔
6661
            $v = new Volunteering($this->dbhr, $this->dbhm);
1✔
6662
            $ret['pendingvolunteering'] = $v->systemWideCount();
1✔
6663
        }
6664

6665
        $s = new Spam($this->dbhr, $this->dbhm);
25✔
6666
        $spamcounts = $s->collectionCounts();
25✔
6667
        $ret['spammerpendingadd'] = $spamcounts[Spam::TYPE_PENDING_ADD];
25✔
6668
        $ret['spammerpendingremove'] = $spamcounts[Spam::TYPE_PENDING_REMOVE];
25✔
6669

6670
        # Show social actions from last 4 days.
6671
        $ctx = NULL;
25✔
6672
        $f = new GroupFacebook($this->dbhr, $this->dbhm);
25✔
6673
        $ret['socialactions'] = 0; // FB DISABLED = count($f->listSocialActions($ctx,$this));
25✔
6674

6675
        $g = new Group($this->dbhr, $this->dbhm);
25✔
6676
        $ret['popularposts'] = 0; // FB DISABLED count($g->getPopularMessages());
25✔
6677

6678
        if ($this->hasPermission(User::PERM_GIFTAID)) {
25✔
6679
            $d = new Donations($this->dbhr, $this->dbhm);
1✔
6680
            $ret['giftaid'] = $d->countGiftAidReview();
1✔
6681
        }
6682

6683
        if (!$groups) {
25✔
6684
            $groups = $this->getMemberships(FALSE, NULL, TRUE, TRUE, $this->id, FALSE);
10✔
6685
        }
6686

6687
        foreach ($groups as &$group) {
25✔
6688
            if (Utils::pres('work', $group)) {
19✔
6689
                foreach ($group['work'] as $key => $work) {
17✔
6690
                    if (Utils::pres($key, $ret)) {
17✔
6691
                        $ret[$key] += $work;
2✔
6692
                    } else {
6693
                        $ret[$key] = $work;
17✔
6694
                    }
6695
                }
6696
            }
6697
        }
6698

6699
        $s = new Story($this->dbhr, $this->dbhm);
25✔
6700
        $ret['stories'] = $s->getReviewCount(FALSE, $this, $groups);
25✔
6701
        $ret['newsletterstories'] = $this->hasPermission(User::PERM_NEWSLETTER) ? $s->getReviewCount(TRUE) : 0;
25✔
6702

6703
        // All the types of work which are worth nagging about.
6704
        $worktypes = [
25✔
6705
            'pendingvolunteering',
25✔
6706
            'socialactions',
25✔
6707
            'popularposts',
25✔
6708
            'chatreview',
25✔
6709
            'relatedmembers',
25✔
6710
            'stories',
25✔
6711
            'newsletterstories',
25✔
6712
            'pending',
25✔
6713
            'spam',
25✔
6714
            'pendingmembers',
25✔
6715
            'pendingevents',
25✔
6716
            'spammembers',
25✔
6717
            'editreview',
25✔
6718
            'pendingadmins'
25✔
6719
        ];
25✔
6720

6721
        if ($this->isAdminOrSupport()) {
25✔
6722
            $worktypes[] = 'spammerpendingadd';
1✔
6723
            $worktypes[] = 'spammerpendingremove';
1✔
6724
        }
6725

6726
        foreach ($worktypes as $key) {
25✔
6727
            $total += Utils::presdef($key, $ret, 0);
25✔
6728
        }
6729

6730
        $ret['total'] = $total;
25✔
6731

6732
        return $ret;
25✔
6733
    }
6734

6735
    public function ratingVisibility($since = "1 hour ago") {
6736
        $mysqltime = date("Y-m-d", strtotime($since));
1✔
6737

6738
        $ratings = $this->dbhr->preQuery("SELECT * FROM ratings WHERE timestamp >= ?;", [
1✔
6739
            $mysqltime
1✔
6740
        ]);
1✔
6741

6742
        foreach ($ratings as $rating) {
1✔
6743
            # A rating is visible to others if there is a chat between the two members, and
6744
            # - the ratee replied to a post, or
6745
            # - there is at least one message from each of them.
6746
            # This means that has been an exchange substantial enough for the rating not to just be frivolous.  It
6747
            # deliberately excludes interactions on ChitChat, where we have seen some people go a bit overboard on
6748
            # rating people.
6749
            $visible = FALSE;
1✔
6750
            #error_log("Check {$rating['rater']} rating of {$rating['ratee']}");
6751

6752
            $chats = $this->dbhr->preQuery("SELECT id FROM chat_rooms WHERE (user1 = ? AND user2 = ?) OR (user2 = ? AND user1 = ?)", [
1✔
6753
                $rating['rater'],
1✔
6754
                $rating['ratee'],
1✔
6755
                $rating['rater'],
1✔
6756
                $rating['ratee'],
1✔
6757
            ]);
1✔
6758

6759
            foreach ($chats as $chat) {
1✔
6760
                $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✔
6761
                    $chat['id']
1✔
6762
                ]);
1✔
6763

6764
                if ($distincts[0]['count'] >= 2) {
1✔
6765
                    #error_log("At least one real message from each of them in {$chat['id']}");
6766
                    $visible = TRUE;
1✔
6767
                } else {
6768
                    $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✔
6769
                        $chat['id'],
1✔
6770
                        $rating['ratee']
1✔
6771
                    ]);
1✔
6772

6773
                    if ($replies[0]['count']) {
1✔
6774
                        #error_log("Significant reply from {$rating['ratee']} in {$chat['id']}");
6775
                        $visible = TRUE;
1✔
6776
                    }
6777
                }
6778
            }
6779

6780
            #error_log("Use {$rating['rating']} from {$rating['rater']} ? " . ($visible ? 'yes': 'no'));
6781
            $oldvisible = intval($rating['visible']) ? TRUE : FALSE;
1✔
6782

6783
            if ($visible != $oldvisible) {
1✔
6784
                $this->dbhm->preExec("UPDATE ratings SET visible = ?, timestamp = NOW() WHERE id = ?;", [
1✔
6785
                    $visible,
1✔
6786
                    $rating['id']
1✔
6787
                ]);
1✔
6788
            }
6789
        }
6790
    }
6791

6792
    public function unban($groupid) {
6793
        $this->dbhm->preExec("DELETE FROM users_banned WHERE userid = ? AND groupid = ?;", [
4✔
6794
            $this->id,
4✔
6795
            $groupid
4✔
6796
        ]);
4✔
6797
    }
6798

6799
    public function hasFacebookLogin() {
6800
        $logins = $this->getLogins();
4✔
6801
        $ret = FALSE;
4✔
6802

6803
        foreach ($logins as $login) {
4✔
6804
            if ($login['type'] == User::LOGIN_FACEBOOK) {
4✔
6805
                $ret = TRUE;
1✔
6806
            }
6807
        }
6808

6809
        return $ret;
4✔
6810
    }
6811

6812
    public function memberReview($groupid, $request, $reason) {
6813
        $mysqltime = date('Y-m-d H:i');
6✔
6814

6815
        if ($request) {
6✔
6816
            # Requesting review.  Leave reviewedat unchanged, so that we can use it to avoid asking too
6817
            # frequently.
6818
            $this->setMembershipAtt($groupid, 'reviewreason', $reason);
4✔
6819
            $this->setMembershipAtt($groupid, 'reviewrequestedat', $mysqltime);
4✔
6820
        } else {
6821
            # We have reviewed.  Note that they might have been removed, in which case the set will do nothing.
6822
            $this->setMembershipAtt($groupid, 'reviewrequestedat', NULL);
3✔
6823
            $this->setMembershipAtt($groupid, 'reviewedat', $mysqltime);
3✔
6824
        }
6825
    }
6826

6827
    private function checkSupporterSettings($settings) {
6828
        $ret = TRUE;
78✔
6829

6830
        if ($settings) {
78✔
6831
            $s = json_decode($settings, TRUE);
15✔
6832

6833
            if ($s && array_key_exists('hidesupporter', $s)) {
15✔
6834
                $ret = !$s['hidesupporter'];
1✔
6835
            }
6836
        }
6837

6838
        return $ret;
78✔
6839
    }
6840

6841
    public function getSupporters(&$rets, $users) {
6842
        $idsleft = [];
302✔
6843

6844
        foreach ($rets as $userid => $ret) {
302✔
6845
            if (Utils::pres($userid, $users)) {
263✔
6846
                if (array_key_exists('supporter', $users[$userid])) {
11✔
6847
                    $rets[$userid]['supporter'] = $users[$userid]['supporter'];
3✔
6848
                    $rets[$userid]['donor'] = Utils::presdef('donor', $users[$userid], FALSE);
11✔
6849
                }
6850
            } else {
6851
                $idsleft[] = $userid;
258✔
6852
            }
6853
        }
6854

6855
        if (count($idsleft)) {
302✔
6856
            $me = Session::whoAmI($this->dbhr, $this->dbhm);
258✔
6857
            $myid = $me ? $me->getId() : null;
258✔
6858

6859
            # A supporter is a mod, someone who has donated recently, or done microvolunteering recently.
6860
            if (count($idsleft)) {
258✔
6861
                $start = date('Y-m-d', strtotime("360 days ago"));
258✔
6862
                $info = $this->dbhr->preQuery(
258✔
6863
                    "SELECT DISTINCT users.id AS userid, settings, systemrole FROM users 
258✔
6864
    LEFT JOIN microactions ON users.id = microactions.userid
6865
    LEFT JOIN users_donations ON users_donations.userid = users.id 
6866
    WHERE users.id IN (" . implode(
258✔
6867
                        ',',
258✔
6868
                        $idsleft
258✔
6869
                    ) . ") AND 
258✔
6870
                    (systemrole IN (?, ?, ?) OR microactions.timestamp >= ? OR users_donations.timestamp >= ?);",
258✔
6871
                    [
258✔
6872
                        User::SYSTEMROLE_ADMIN,
258✔
6873
                        User::SYSTEMROLE_SUPPORT,
258✔
6874
                        User::SYSTEMROLE_MODERATOR,
258✔
6875
                        $start,
258✔
6876
                        $start
258✔
6877
                    ]
258✔
6878
                );
258✔
6879

6880
                $found = [];
258✔
6881

6882
                foreach ($info as $i) {
258✔
6883
                    $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
78✔
6884
                    $found[] = $i['userid'];
78✔
6885
                }
6886

6887
                $left = array_diff($idsleft, $found);
258✔
6888

6889
                # If we are one of the users, then we want to return whether we are a donor.
6890
                if (in_array($myid, $idsleft)) {
258✔
6891
                    $left[] = $myid;
143✔
6892
                    $left = array_filter(array_unique($left));
143✔
6893
                }
6894

6895
                if (count($left)) {
258✔
6896
                    $info = $this->dbhr->preQuery(
256✔
6897
                        "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(
256✔
6898
                            ',',
256✔
6899
                            $left
256✔
6900
                        ) . ") GROUP BY TransactionType;",
256✔
6901
                        [
256✔
6902
                            $start
256✔
6903
                        ]
256✔
6904
                    );
256✔
6905

6906
                    foreach ($info as $i) {
256✔
6907
                        $rets[$i['userid']]['supporter'] = $this->checkSupporterSettings($i['settings']);
3✔
6908

6909
                        if ($i['userid'] == $myid) {
3✔
6910
                            # Only return this info for ourselves, otherwise it's a privacy leak.
6911
                            $rets[$i['userid']]['donor'] = TRUE;
3✔
6912

6913
                            if ($i['TransactionType'] == 'subscr_payment' || $i['TransactionType'] == 'recurring_payment') {
3✔
6914
                                $rets[$i['userid']]['donorrecurring'] = TRUE;
1✔
6915
                            }
6916
                        }
6917
                    }
6918
                }
6919
            }
6920
        }
6921
    }
6922

6923
    public function obfuscateEmail($email) {
6924
        $p = strpos($email, '@');
2✔
6925
        $q = strpos($email, 'privaterelay.appleid.com');
2✔
6926

6927
        if ($q) {
2✔
6928
            $email = 'Your Apple ID';
1✔
6929
        } else {
6930
            # For very short emails, we just show the first character.
6931
            if ($p <= 3) {
2✔
6932
                $email = substr($email, 0, 1) . "***" . substr($email, $p);
1✔
6933
            } else if ($p < 10) {
2✔
6934
                $email = substr($email, 0, 1) . "***" . substr($email, $p - 1);
2✔
6935
            } else {
6936
                $email = substr($email, 0, 3) . "***" . substr($email, $p - 3);
1✔
6937
            }
6938
        }
6939

6940
        return $email;
2✔
6941
    }
6942

6943
    public function setSimpleMail($simplemail) {
6944
        $s = $this->getPrivate('settings');
2✔
6945

6946
        if ($s) {
2✔
6947
            $settings = json_decode($s, TRUE);
1✔
6948
        } else {
6949
            $settings = [];
2✔
6950
        }
6951

6952
        $this->dbhm->beginTransaction();
2✔
6953

6954
        switch ($simplemail) {
6955
            case User::SIMPLE_MAIL_NONE: {
6956
                # No digests, no events/volunteering.
6957
                # No relevant or newsletters.
6958
                # No email notifications.
6959
                # No enagement.
6960
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 0, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
2✔
6961
                    $this->id
2✔
6962
                ]);
2✔
6963

6964
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
2✔
6965
                    $this->id
2✔
6966
                ]);
2✔
6967

6968
                $settings['notifications']['email'] = FALSE;
2✔
6969
                $settings['notifications']['emailmine'] = FALSE;
2✔
6970
                $settings['notificationmails']= FALSE;
2✔
6971
                $settings['engagement'] = FALSE;
2✔
6972
                break;
2✔
6973
            }
6974
            case User::SIMPLE_MAIL_BASIC: {
6975
                # Daily digests, no events/volunteering.
6976
                # No relevant or newsletters.
6977
                # Chat email notifications.
6978
                # No enagement.
6979
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = 24, eventsallowed = 0, volunteeringallowed = 0 WHERE userid = ?;", [
1✔
6980
                    $this->id
1✔
6981
                ]);
1✔
6982

6983
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 0,  newslettersallowed = 0 WHERE id = ?;", [
1✔
6984
                    $this->id
1✔
6985
                ]);
1✔
6986

6987
                $settings['notifications']['email'] = TRUE;
1✔
6988
                $settings['notifications']['emailmine'] = FALSE;
1✔
6989
                $settings['notificationmails']= FALSE;
1✔
6990
                $settings['engagement']= FALSE;
1✔
6991
                break;
1✔
6992
            }
6993
            case User::SIMPLE_MAIL_FULL: {
6994
                # Immediate mails, events/volunteering.
6995
                # Relevant and newsletters.
6996
                # Email notifications.
6997
                # Enagement.
6998
                $this->dbhm->preExec("UPDATE memberships SET emailfrequency = -1, eventsallowed = 1, volunteeringallowed = 1 WHERE userid = ?;", [
1✔
6999
                    $this->id
1✔
7000
                ]);
1✔
7001

7002
                $this->dbhm->preExec("UPDATE users SET relevantallowed = 1,  newslettersallowed = 1 WHERE id = ?;", [
1✔
7003
                    $this->id
1✔
7004
                ]);
1✔
7005

7006
                $settings['notifications']['email'] = TRUE;
1✔
7007
                $settings['notifications']['emailmine'] = FALSE;
1✔
7008
                $settings['notificationmails']= TRUE;
1✔
7009
                $settings['engagement']= TRUE;
1✔
7010
                break;
1✔
7011
            }
7012
        }
7013

7014
        $settings['simplemail'] = $simplemail;
2✔
7015

7016
        $this->setPrivate('settings', json_encode($settings));
2✔
7017

7018
        # Holiday no longer exposed so turn off.
7019
        $this->dbhm->preExec("UPDATE users SET onholidaytill = NULL, settings = ? WHERE id = ?;", [
2✔
7020
            json_encode($settings),
2✔
7021
            $this->id
2✔
7022
        ]);
2✔
7023

7024
        $this->dbhm->commit();
2✔
7025
    }
7026

7027
    public function processMemberships($g = NULL) {
7028
        $memberships = $this->dbhr->preQuery("SELECT id FROM `memberships_history` WHERE processingrequired = 1 ORDER BY id ASC;");
6✔
7029

7030
        foreach ($memberships as $membership) {
6✔
7031
            $this->processMembership($membership['id'], $g);
6✔
7032
        }
7033
    }
7034

7035
    public function processMembership($id, $g) {
7036
        $memberships = $this->dbhr->preQuery("SELECT * FROM memberships_history WHERE id = ?;",[
6✔
7037
            $id
6✔
7038
        ]);
6✔
7039

7040
        foreach ($memberships as $membership) {
6✔
7041
            $groupid = $membership['groupid'];
6✔
7042
            $userid = $membership['userid'];
6✔
7043
            $collection = $membership['collection'];
6✔
7044

7045
            $g = $g ? $g : Group::get($this->dbhr, $this->dbhm, $groupid);
6✔
7046

7047
            # The membership didn't already exist.  We might want to send a welcome mail.
7048
            if ($g->getPrivate('welcomemail') && $collection == MembershipCollection::APPROVED && $g->getPrivate('onhere')) {
6✔
7049
                # They are now approved.  We need to send a per-group welcome mail.
7050
                try {
7051
                    $g->sendWelcome($userid, FALSE);
2✔
7052
                } catch (Exception $e) {
×
7053
                    error_log("Welcome failed: " . $e->getMessage());
×
7054
                    \Sentry\captureException($e);
×
7055
                }
7056
            }
7057

7058
            # Check whether this user now counts as a possible spammer.
7059
            $s = new Spam($this->dbhr, $this->dbhm);
6✔
7060
            $s->checkUser($userid, $groupid);
6✔
7061

7062
            # We might have mod notes which require this member to be flagged up.
7063
            $comments = $this->dbhr->preQuery(
6✔
7064
                "SELECT COUNT(*) AS count FROM users_comments WHERE userid = ? AND flag = 1;", [
6✔
7065
                    $userid,
6✔
7066
                ]
6✔
7067
            );
6✔
7068

7069
            if ($comments[0]['count'] > 0) {
6✔
7070
                $this->memberReview($groupid, TRUE, 'Note flagged to other groups');
1✔
7071
            }
7072

7073
            $this->dbhm->preExec("UPDATE memberships_history SET processingrequired = 0 WHERE id = ?", [
6✔
7074
                $id
6✔
7075
            ]);
6✔
7076
        }
7077
    }
7078

7079
    public function getUserKey($id) {
7080
        $key = null;
105✔
7081
        $sql = "SELECT * FROM users_logins WHERE userid = ? AND type = ?;";
105✔
7082
        $logins = $this->dbhr->preQuery($sql, [$id, User::LOGIN_LINK]);
105✔
7083
        foreach ($logins as $login) {
105✔
7084
            $key = $login['credentials'];
31✔
7085
        }
7086

7087
        if (!$key) {
105✔
7088
            $key = Utils::randstr(32);
104✔
7089
            $rc = $this->dbhm->preExec("INSERT INTO users_logins (userid, type, credentials) VALUES (?,?,?);", [
104✔
7090
                $id,
104✔
7091
                User::LOGIN_LINK,
104✔
7092
                $key
104✔
7093
            ]);
104✔
7094
        }
7095

7096
        return $key;
104✔
7097
    }
7098

7099
    public function assignUserToToDonation($email, $userid) {
7100
        $email = trim($email);
539✔
7101

7102
        if (strlen($email)) {
539✔
7103
            # We might have donations made via PayPal using this email address which we can now link to this user.  Do
7104
            # SELECT first to avoid this having to replicate in the cluster.
7105
            $donations = $this->dbhr->preQuery("SELECT id FROM users_donations WHERE Payer = ? AND userid IS NULL;", [
539✔
7106
                $email
539✔
7107
            ]);
539✔
7108

7109
            foreach ($donations as $donation) {
539✔
7110
                // Check if user exists before updating to avoid foreign key constraint violations
7111
                $userExists = $this->dbhr->preQuery("SELECT id FROM users WHERE id = ?;", [$userid]);
156✔
7112
                if (count($userExists) > 0) {
156✔
7113
                    $this->dbhm->preExec("UPDATE users_donations SET userid = ? WHERE id = ?;", [
156✔
7114
                        $userid,
156✔
7115
                        $donation['id']
156✔
7116
                    ]);
156✔
7117
                }
7118
            }
7119
        }
7120
    }
7121
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc