Completed
Push — master ( 0f6e4e...006bd3 )
by Vladimir
02:52
created

Player::setColorBlindAssist()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 1
cp 0
cc 1
eloc 2
nc 1
nop 1
crap 2
1
<?php
2
/**
3
 * This file contains functionality relating to a league player
4
 *
5
 * @package    BZiON\Models
6
 * @license    https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3
7
 */
8
9
use Carbon\Carbon;
10
use Symfony\Component\Security\Core\Util\SecureRandom;
11
use Symfony\Component\Security\Core\Util\StringUtils;
12
13
/**
14
 * A league player
15
 * @package    BZiON\Models
16
 */
17
class Player extends AvatarModel implements NamedModel, DuplexUrlInterface, EloInterface
18
{
19
    /**
20
     * These are built-in roles that cannot be deleted via the web interface so we will be storing these values as
21
     * constant variables. Hopefully, a user won't be silly enough to delete them manually from the database.
22
     *
23
     * @TODO Deprecate these and use the Role constants
24
     */
25
    const DEVELOPER    = Role::DEVELOPER;
26
    const ADMIN        = Role::ADMINISTRATOR;
27
    const COP          = Role::COP;
28
    const REFEREE      = Role::REFEREE;
29
    const S_ADMIN      = Role::SYSADMIN;
30
    const PLAYER       = Role::PLAYER;
31
    const PLAYER_NO_PM = Role::PLAYER_NO_PM;
32
33
    /**
34
     * The bzid of the player
35
     * @var int
36
     */
37
    protected $bzid;
38
39
    /**
40
     * The id of the player's team
41
     * @var int
42
     */
43
    protected $team;
44
45
    /**
46
     * The player's e-mail address
47
     * @var string
48
     */
49
    protected $email;
50
51
    /**
52
     * Whether the player has verified their e-mail address
53
     * @var bool
54
     */
55
    protected $verified;
56
57
    /**
58
     * What kind of events the player should be e-mailed about
59
     * @var string
60
     */
61
    protected $receives;
62
63
    /**
64
     * A confirmation code for the player's e-mail address verification
65
     * @var string
66
     */
67
    protected $confirmCode;
68
69
    /**
70
     * Whether the callsign of the player is outdated
71
     * @var bool
72
     */
73
    protected $outdated;
74
75
    /**
76
     * The player's profile description
77
     * @var string
78
     */
79
    protected $description;
80
81
    /**
82
     * The id of the player's country
83
     * @var int
84
     */
85
    protected $country;
86
87
    /**
88
     * The site theme this player has chosen
89
     * @var string
90
     */
91
    protected $theme;
92
93
    /**
94
     * Whether or not this player has opted-in for color blindness assistance.
95
     * @var bool
96
     */
97
    protected $color_blind_enabled;
98
99
    /**
100
     * The player's timezone PHP identifier, e.g. "Europe/Paris"
101
     * @var string
102
     */
103
    protected $timezone;
104
105
    /**
106
     * The date the player joined the site
107
     * @var TimeDate
108
     */
109
    protected $joined;
110
111
    /**
112
     * The date of the player's last login
113
     * @var TimeDate
114
     */
115
    protected $last_login;
116
117
    /**
118
     * The date of the player's last match
119
     * @var Match
120
     */
121
    protected $last_match;
122
123
    /**
124
     * The roles a player belongs to
125
     * @var Role[]
126
     */
127
    protected $roles;
128
129
    /**
130
     * The permissions a player has
131
     * @var Permission[]
132
     */
133
    protected $permissions;
134
135
    /**
136
     * A section for admins to write notes about players
137
     * @var string
138
     */
139
    protected $admin_notes;
140
141
    /**
142
     * The ban of the player, or null if the player is not banned
143
     * @var Ban|null
144
     */
145
    protected $ban;
146
147
    /**
148
     * Cached results for match summaries
149
     *
150
     * @var array
151
     */
152
    private $cachedMatchSummary;
153
154
    /**
155
     * The cached match count for a player
156
     *
157
     * @var int
158
     */
159
    private $cachedMatchCount = null;
160
161
    /**
162
     * The Elo for this player that has been explicitly set for this player from a database query. This value will take
163
     * precedence over having to build to an Elo season history.
164
     *
165
     * @var int
166
     */
167
    private $elo;
168
    private $eloSeason;
169
    private $eloSeasonHistory;
170
171
    private $matchActivity;
172
173
    /**
174
     * The name of the database table used for queries
175
     */
176
    const TABLE = "players";
177 76
178
    /**
179 76
     * The location where avatars will be stored
180 76
     */
181 76
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
182 76
183 76
    const EDIT_PERMISSION = Permission::EDIT_USER;
184 76
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
185 76
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
186
187 76
    /**
188
     * {@inheritdoc}
189
     */
190 76
    protected function assignResult($player)
191
    {
192
        $this->bzid = $player['bzid'];
193
        $this->name = $player['username'];
194
        $this->alias = $player['alias'];
195 76
        $this->team = $player['team'];
196
        $this->status = $player['status'];
197 76
        $this->avatar = $player['avatar'];
198 76
        $this->country = $player['country'];
199 76
200 76
        if (array_key_exists('activity', $player)) {
201 76
            $this->matchActivity = ($player['activity'] != null) ? $player['activity'] : 0.0;
202 76
        }
203 76
204 76
        if (array_key_exists('elo', $player)) {
205 76
            $this->elo = $player['elo'];
206 76
        }
207 76
    }
208 76
209
    /**
210 76
     * {@inheritdoc}
211
     */
212 76
    protected function assignLazyResult($player)
213 76
    {
214
        $this->email = $player['email'];
215
        $this->verified = $player['verified'];
216
        $this->receives = $player['receives'];
217
        $this->confirmCode = $player['confirm_code'];
218
        $this->outdated = $player['outdated'];
219
        $this->description = $player['description'];
220
        $this->timezone = $player['timezone'];
221
        $this->joined = TimeDate::fromMysql($player['joined']);
222 76
        $this->last_login = TimeDate::fromMysql($player['last_login']);
223
        $this->last_match = Match::get($player['last_match']);
224 76
        $this->admin_notes = $player['admin_notes'];
225 1
        $this->ban = Ban::getBan($this->id);
226
        $this->color_blind_enabled = $player['color_blind_enabled'];
227
228 76
        $this->cachedMatchSummary = [];
229
230
        // Theme user options
231 76
        if (isset($player['theme'])) {
232 14
            $this->theme = $player['theme'];
233 14
        } else {
234
            $themes = Service::getSiteThemes();
235
            $this->theme = $themes[0]['slug'];
236
        }
237 76
238 76
        $this->updateUserPermissions();
239
    }
240 76
241
    /**
242
     * Add a player a new role
243
     *
244
     * @param Role|int $role_id The role ID to add a player to
245
     *
246
     * @return bool Whether the operation was successful or not
247
     */
248
    public function addRole($role_id)
249
    {
250
        if ($role_id instanceof Role) {
251
            $role_id = $role_id->getId();
252
        }
253
254
        $this->lazyLoad();
255
256
        // Make sure the player doesn't already have the role
257
        foreach ($this->roles as $playerRole) {
258 22
            if ($playerRole->getId() == $role_id) {
259
                return false;
260 22
            }
261
        }
262
263
        $status = $this->modifyRole($role_id, "add");
264
        $this->refresh();
265
266
        return $status;
267
    }
268 1
269
    /**
270 1
     * Get the notes admins have left about a player
271
     * @return string The notes
272
     */
273
    public function getAdminNotes()
274
    {
275
        $this->lazyLoad();
276
277
        return $this->admin_notes;
278
    }
279
280
    /**
281
     * Get the player's BZID
282
     * @return int The BZID
283
     */
284
    public function getBZID()
285
    {
286
        return $this->bzid;
287
    }
288
289
    /**
290
     * Get the country a player belongs to
291
     *
292
     * @return Country The country belongs to
293 30
     */
294
    public function getCountry()
295 30
    {
296 29
        return Country::get($this->country);
297
    }
298
299 30
    /**
300 29
     * Get the e-mail address of the player
301
     *
302
     * @return string The address
303 30
     */
304
    public function getEmailAddress()
305
    {
306
        $this->lazyLoad();
307
308
        return $this->email;
309
    }
310
311
    /**
312
     * Build a key that we'll use for caching season Elo data in this model
313 3
     *
314
     * @param  string|null $season The season to get
315 3
     * @param  int|null    $year   The year of the season to get
316
     *
317 3
     * @return string
318
     */
319
    private function buildSeasonKey(&$season, &$year)
320
    {
321
        if ($season === null) {
322
            $season = Season::getCurrentSeason();
323
        }
324
325
        if ($year === null) {
326
            $year = Carbon::now()->year;
327
        }
328
329
        return sprintf('%s-%s', $year, $season);
330
    }
331
332 3
    /**
333
     * Build a key to use for caching season Elo data in this model from a timestamp
334 3
     *
335 3
     * @param DateTime $timestamp
336
     *
337
     * @return string
338
     */
339 3
    private function buildSeasonKeyFromTimestamp(\DateTime $timestamp)
340 2
    {
341
        $seasonInfo = Season::getSeason($timestamp);
342
343
        return sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
344
    }
345 3
346
    /**
347 3
     * Remove all Elo data for this model for matches occurring after the given match (inclusive)
348 1
     *
349
     * This function will not remove the Elo data for this match from the database. Ideally, this function should only
350
     * be called during Elo recalculation for this match.
351
     *
352
     * @internal
353 2
     *
354 2
     * @param Match $match
355
     *
356 2
     * @see Match::recalculateElo()
357
     */
358
    public function invalidateMatchFromCache(Match $match)
359 2
    {
360
        $seasonKey = $this->buildSeasonKeyFromTimestamp($match->getTimestamp());
361
        $seasonElo = null;
362
363
        // If we have an existing season history cached, save a reference to it for easy access. Don't create one if
364
        // nothing is cached or else it'll cause for an empty cache to be created
365
        if (isset($this->eloSeasonHistory[$seasonKey])) {
366
            $seasonElo = &$this->eloSeasonHistory[$seasonKey];
367
        }
368
369 30
        // Unset the currently cached Elo for a player so next time Player::getElo() is called, it'll pull the latest
370
        // available Elo
371 30
        unset($this->eloSeason[$seasonKey]);
372
373
        if ($seasonElo === null) {
374 30
            return;
375 29
        }
376
377
        // This function is called when we recalculate, so assume that the match will be recent, therefore towards the
378 30
        // end of the Elo history array. We splice the array to have all Elo data after this match to be removed.
379
        foreach (array_reverse($seasonElo) as $key => $match) {
380
            if ($match['match'] === $match) {
381
                $seasonElo = array_splice($seasonElo, $key);
382
                break;
383
            }
384
        }
385
    }
386
387
    /**
388
     * Get the Elo changes for a player for a given season
389
     *
390
     * @param  string|null $season The season to get
391
     * @param  int|null    $year   The year of the season to get
392 30
     *
393
     * @return array
394 30
     */
395 30
    public function getEloSeasonHistory($season = null, $year = null)
396
    {
397 30
        $seasonKey = $this->buildSeasonKey($season, $year);
398 30
399 30
        // This season's already been cached
400
        if (isset($this->eloSeasonHistory[$seasonKey])) {
401
            return $this->eloSeasonHistory[$seasonKey];
402 30
        }
403
404
        $this->eloSeasonHistory[$seasonKey] = $this->db->query('
405
          SELECT
406
            elo_new AS elo,
407
            match_id AS `match`,
408
            MONTH(matches.timestamp) AS `month`,
409
            YEAR(matches.timestamp) AS `year`,
410
            DAY(matches.timestamp) AS `day`
411
          FROM
412
            player_elo
413
            LEFT JOIN matches ON player_elo.match_id = matches.id
414
          WHERE
415 30
            user_id = ? AND season_period = ? AND season_year = ?
416
          ORDER BY
417 30
            match_id ASC
418 30
        ', [ $this->getId(), $season, $year ]);
419
420 30
        array_unshift($this->eloSeasonHistory[$seasonKey], [
421 29
            'elo' => 1200,
422
            'match' => null,
423
            'month' => Season::getCurrentSeasonRange($season)->getStartOfRange()->month,
424 30
            'year' => $year,
425
            'day' => 1
426 30
        ]);
427 30
428 30
        return $this->eloSeasonHistory[$seasonKey];
429
    }
430
431
    /**
432
     * Get the player's Elo for a season.
433 30
     *
434
     * With the default arguments, it will fetch the Elo for the current season.
435
     *
436
     * @param string|null $season The season we're looking for: winter, spring, summer, or fall
437
     * @param int|null    $year   The year of the season we're looking for
438
     *
439
     * @return int The player's Elo
440
     */
441
    public function getElo($season = null, $year = null)
442
    {
443
        // The Elo for this player has been forcefully set from a trusted database query, so just return that.
444
        if ($this->elo !== null) {
445 30
            return $this->elo;
446
        }
447 30
448 30
        $this->getEloSeasonHistory($season, $year);
449
        $seasonKey = $this->buildSeasonKey($season, $year);
450
451 30
        if (isset($this->eloSeason[$seasonKey])) {
452 30
            return $this->eloSeason[$seasonKey];
453
        }
454 30
455
        $season = &$this->eloSeasonHistory[$seasonKey];
456 30
457 29
        if (!empty($season)) {
458
            $elo = end($season);
459 29
            $this->eloSeason[$seasonKey] = ($elo !== false) ? $elo['elo'] : 1200;
460
        } else {
461 30
            $this->eloSeason[$seasonKey] = 1200;
462
        }
463
464
        return $this->eloSeason[$seasonKey];
465
    }
466
467
    /**
468
     * Adjust the Elo of a player for the current season based on a Match
469
     *
470
     * **Warning:** If $match is null, the Elo for the player will be modified but the value will not be persisted to
471
     * the database.
472
     *
473
     * @param int        $adjust The value to be added to the current ELO (negative to subtract)
474
     * @param Match|null $match  The match where this Elo change took place
475
     */
476
    public function adjustElo($adjust, Match $match = null)
477
    {
478
        $timestamp = ($match !== null) ? $match->getTimestamp() : (Carbon::now());
479
        $seasonInfo = Season::getSeason($timestamp);
480
481
        // Get the current Elo for the player, even if it's the default 1200. We need the value for adjusting
482
        $elo = $this->getElo($seasonInfo['season'], $seasonInfo['year']);
483
        $seasonKey = sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
484
485
        $this->eloSeason[$seasonKey] += $adjust;
486
487
        if ($match !== null && $this->isValid()) {
488
            $this->db->execute('
489
              INSERT INTO player_elo VALUES (?, ?, ?, ?, ?, ?)
490
            ', [ $this->getId(), $match->getId(), $seasonInfo['season'], $seasonInfo['year'], $elo, $this->eloSeason[$seasonKey] ]);
491
        }
492
    }
493
494
    /**
495
     * Returns whether the player has verified their e-mail address
496
     *
497
     * @return bool `true` for verified players
498
     */
499
    public function isVerified()
500
    {
501
        $this->lazyLoad();
502
503
        return $this->verified;
504
    }
505
506 1
    /**
507
     * Returns the confirmation code for the player's e-mail address verification
508 1
     *
509
     * @return string The player's confirmation code
510 1
     */
511
    public function getConfirmCode()
512 1
    {
513
        $this->lazyLoad();
514
515
        return $this->confirmCode;
516
    }
517
518
    /**
519
     * Returns what kind of events the player should be e-mailed about
520
     *
521
     * @return string The type of notifications
522
     */
523
    public function getReceives()
524
    {
525
        $this->lazyLoad();
526
527
        return $this->receives;
528
    }
529
530
    /**
531
     * Finds out whether the specified player wants and can receive an e-mail
532
     * message
533
     *
534
     * @param  string  $type
535
     * @return bool `true` if the player should be sent an e-mail
536
     */
537
    public function canReceive($type)
538
    {
539
        $this->lazyLoad();
540
541
        if (!$this->email || !$this->isVerified()) {
542
            // Unverified e-mail means the user will receive nothing
543
            return false;
544
        }
545
546
        if ($this->receives == 'everything') {
547
            return true;
548
        }
549
550
        return $this->receives == $type;
551
    }
552
553
    /**
554
     * Find out whether the specified confirmation code is correct
555
     *
556
     * This method protects against timing attacks
557
     *
558
     * @param  string $code The confirmation code to check
559
     * @return bool `true` for a correct e-mail verification code
560
     */
561
    public function isCorrectConfirmCode($code)
562
    {
563
        $this->lazyLoad();
564
565
        if ($this->confirmCode === null) {
566
            return false;
567
        }
568
569
        return StringUtils::equals($code, $this->confirmCode);
570
    }
571
572
    /**
573
     * Get the player's sanitized description
574
     * @return string The description
575
     */
576
    public function getDescription()
577
    {
578
        $this->lazyLoad();
579
580
        return $this->description;
581
    }
582
583
    /**
584
     * Get the joined date of the player
585
     * @return TimeDate The joined date of the player
586
     */
587
    public function getJoinedDate()
588
    {
589
        $this->lazyLoad();
590
591
        return $this->joined->copy();
592
    }
593
594
    /**
595
     * Get all of the known IPs used by the player
596
     *
597
     * @return string[][] An array containing IPs and hosts
598
     */
599
    public function getKnownIPs()
600
    {
601
        return $this->db->query(
602
            'SELECT DISTINCT ip, host FROM visits WHERE player = ? GROUP BY ip, host ORDER BY MAX(timestamp) DESC LIMIT 10',
603
            array($this->getId())
604
        );
605
    }
606
607
    /**
608
     * Get the last login for a player
609
     * @return TimeDate The date of the last login
610
     */
611 23
    public function getLastLogin()
612
    {
613 23
        $this->lazyLoad();
614
615
        return $this->last_login->copy();
616
    }
617
618
    /**
619
     * Get the last match
620 1
     * @return Match
621
     */
622 1
    public function getLastMatch()
623
    {
624 1
        $this->lazyLoad();
625
626
        return $this->last_match;
627
    }
628
629
    /**
630
     * Get all of the callsigns a player has used to log in to the website
631
     * @return string[] An array containing all of the past callsigns recorded for a player
632
     */
633
    public function getPastCallsigns()
634
    {
635
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
636
    }
637
638
    /**
639
     * Get the player's team
640
     * @return Team The object representing the team
641 76
     */
642
    public function getTeam()
643 76
    {
644 76
        return Team::get($this->team);
645
    }
646 76
647 76
    /**
648
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
649 76
     * @return string The timezone
650
     */
651
    public function getTimezone()
652
    {
653
        $this->lazyLoad();
654
655
        return ($this->timezone) ?: date_default_timezone_get();
656
    }
657
658 2
    /**
659
     * Get the roles of the player
660 2
     * @return Role[]
661 1
     */
662
    public function getRoles()
663
    {
664 2
        $this->lazyLoad();
665
666 2
        return $this->roles;
667
    }
668
669
    /**
670
     * Rebuild the list of permissions a user has been granted
671
     */
672
    private function updateUserPermissions()
673
    {
674
        $this->roles = Role::getRoles($this->id);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Role::getRoles($this->id) of type array<integer,object<Model>> is incompatible with the declared type array<integer,object<Role>> of property $roles.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
675
        $this->permissions = array();
676
677
        foreach ($this->roles as $role) {
678
            $this->permissions = array_merge($this->permissions, $role->getPerms());
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Model as the method getPerms() does only exist in the following sub-classes of Model: Permission, Role. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
Documentation Bug introduced by
It seems like array_merge($this->permi...ons, $role->getPerms()) of type array is incompatible with the declared type array<integer,object<Permission>> of property $permissions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
679
        }
680
    }
681
682
    /**
683
     * Check if a player has a specific permission
684
     *
685
     * @param string|null $permission The permission to check for
686
     *
687
     * @return bool Whether or not the player has the permission
688
     */
689
    public function hasPermission($permission)
690
    {
691
        if ($permission === null) {
692
            return false;
693
        }
694
695
        $this->lazyLoad();
696
697
        return isset($this->permissions[$permission]);
698
    }
699
700
    /**
701
     * Check whether or not a player been in a match or has logged on in the specified amount of time to be considered
702
     * active
703
     *
704
     * @return bool True if the player has been active
705
     */
706
    public function hasBeenActive()
707
    {
708
        $this->lazyLoad();
709
710
        $interval  = Service::getParameter('bzion.miscellaneous.active_interval');
711
        $lastLogin = $this->last_login->copy()->modify($interval);
712
713
        $hasBeenActive = (TimeDate::now() <= $lastLogin);
714
715
        if ($this->last_match->isValid()) {
716
            $lastMatch = $this->last_match->getTimestamp()->copy()->modify($interval);
717
            $hasBeenActive = ($hasBeenActive || TimeDate::now() <= $lastMatch);
718
        }
719
720
        return $hasBeenActive;
721
    }
722 1
723
    /**
724 1
     * Check whether the callsign of the player is outdated
725
     *
726
     * Returns true if this player has probably changed their callsign, making
727
     * the current username stored in the database obsolete
728
     *
729
     * @return bool Whether or not the player is disabled
730
     */
731
    public function isOutdated()
732 55
    {
733
        $this->lazyLoad();
734 55
735
        return $this->outdated;
736
    }
737
738
    /**
739
     * Check if a player's account has been disabled
740 1
     *
741
     * @return bool Whether or not the player is disabled
742 1
     */
743
    public function isDisabled()
744
    {
745
        return $this->status == "disabled";
746 1
    }
747
748
    /**
749
     * Check if everyone can log in as this user on a test environment
750
     *
751
     * @return bool
752
     */
753
    public function isTestUser()
754
    {
755
        return $this->status == "test";
756
    }
757
758
    /**
759
     * Check if a player is teamless
760
     *
761
     * @return bool True if the player is teamless
762
     */
763
    public function isTeamless()
764
    {
765
        return empty($this->team);
766 2
    }
767
768 2
    /**
769
     * Mark a player's account as banned
770
     *
771
     * @deprecated The players table shouldn't have any indicators of banned status, the Bans table is the authoritative source
772
     */
773
    public function markAsBanned()
774
    {
775
        return;
0 ignored issues
show
Coding Style introduced by
Empty return statement not required here
Loading history...
776
    }
777
778
    /**
779
     * Mark a player's account as unbanned
780
     *
781
     * @deprecated The players table shouldn't have any indicators of banned status, the Bans table is the authoritative source
782
     */
783
    public function markAsUnbanned()
784
    {
785
        return;
0 ignored issues
show
Coding Style introduced by
Empty return statement not required here
Loading history...
786
    }
787
788
    /**
789
     * Find out if a player is hard banned
790
     *
791
     * @return bool
792
     */
793
    public function isBanned()
794
    {
795
        $ban = Ban::getBan($this->id);
796
797
        return ($ban !== null && !$ban->isSoftBan());
798
    }
799
800
    /**
801
     * Get the ban of the player
802
     *
803
     * This method performs a load of all the lazy parameters of the Player
804
     *
805
     * @return Ban|null The current ban of the player, or null if the player is
806
     *                  is not banned
807
     */
808
    public function getBan()
809
    {
810
        $this->lazyLoad();
811
812
        return $this->ban;
813
    }
814
815
    /**
816
     * Remove a player from a role
817
     *
818
     * @param int $role_id The role ID to add or remove
819
     *
820
     * @return bool Whether the operation was successful or not
821
     */
822
    public function removeRole($role_id)
823
    {
824
        $status = $this->modifyRole($role_id, "remove");
825
        $this->refresh();
826
827
        return $status;
828
    }
829
830
    /**
831
     * Set the player's email address and reset their verification status
832
     * @param string $email The address
833
     */
834
    public function setEmailAddress($email)
835
    {
836
        $this->lazyLoad();
837
838
        if ($this->email == $email) {
839
            // The e-mail hasn't changed, don't do anything
840
            return;
841
        }
842
843
        $this->setVerified(false);
844
        $this->generateNewConfirmCode();
845
846
        $this->updateProperty($this->email, 'email', $email);
847
    }
848
849
    /**
850
     * Set whether the player has verified their e-mail address
851
     *
852
     * @param  bool $verified Whether the player is verified or not
853
     * @return self
854
     */
855
    public function setVerified($verified)
856
    {
857
        $this->lazyLoad();
858
859
        if ($verified) {
860
            $this->setConfirmCode(null);
861
        }
862
863
        return $this->updateProperty($this->verified, 'verified', $verified);
864
    }
865
866
    /**
867
     * Generate a new random confirmation token for e-mail address verification
868
     *
869
     * @return self
870
     */
871
    public function generateNewConfirmCode()
872
    {
873
        $generator = new SecureRandom();
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Component\Security\Core\Util\SecureRandom has been deprecated with message: since version 2.8, to be removed in 3.0. Use the random_bytes function instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
874
        $random = $generator->nextBytes(16);
875
876
        return $this->setConfirmCode(bin2hex($random));
877
    }
878
879
    /**
880
     * Set the confirmation token for e-mail address verification
881
     *
882 76
     * @param  string $code The confirmation code
883
     * @return self
884 76
     */
885
    private function setConfirmCode($code)
886 76
    {
887
        $this->lazyLoad();
888
889
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
890
    }
891
892
    /**
893
     * Set what kind of events the player should be e-mailed about
894
     *
895
     * @param  string $receives The type of notification
896
     * @return self
897
     */
898
    public function setReceives($receives)
899
    {
900
        $this->lazyLoad();
901
902
        return $this->updateProperty($this->receives, 'receives', $receives);
903
    }
904
905
    /**
906
     * Set whether the callsign of the player is outdated
907
     *
908
     * @param  bool $outdated Whether the callsign is outdated
909
     * @return self
910
     */
911 55
    public function setOutdated($outdated)
912
    {
913 55
        $this->lazyLoad();
914 55
915
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
916
    }
917
918
    /**
919
     * Set the player's description
920
     * @param string $description The description
921 29
     */
922
    public function setDescription($description)
923 29
    {
924 29
        $this->updateProperty($this->description, "description", $description);
925
    }
926
927
    /**
928
     * Set the player's timezone
929
     * @param string $timezone The timezone
930
     */
931
    public function setTimezone($timezone)
932
    {
933
        $this->updateProperty($this->timezone, "timezone", $timezone);
934
    }
935
936
    /**
937
     * Set the player's team
938
     * @param int $team The team's ID
939
     */
940
    public function setTeam($team)
941
    {
942
        $this->updateProperty($this->team, "team", $team);
943
    }
944
945
    /**
946
     * Set the match the player last participated in
947
     *
948
     * @param int $match The match's ID
949
     */
950
    public function setLastMatch($match)
951
    {
952
        $this->updateProperty($this->last_match, 'last_match', $match);
953
    }
954
955
    /**
956
     * Set the player's status
957
     * @param string $status The new status
958
     */
959
    public function setStatus($status)
960
    {
961
        $this->updateProperty($this->status, 'status', $status);
962
    }
963
964
    /**
965
     * Set the player's admin notes
966
     * @param  string $admin_notes The new admin notes
967 1
     * @return self
968
     */
969 1
    public function setAdminNotes($admin_notes)
970
    {
971
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
972
    }
973
974
    /**
975
     * Set the player's country
976 1
     * @param  int   $country The ID of the new country
977
     * @return self
978 1
     */
979
    public function setCountry($country)
980
    {
981
        return $this->updateProperty($this->country, 'country', $country);
982
    }
983
984
    /**
985
     * Get the player's chosen theme preference
986
     *
987
     * @return string
988
     */
989
    public function getTheme()
990
    {
991
        $this->lazyLoad();
992
993
        return $this->theme;
994
    }
995
996
    /**
997
     * Set the site theme for the player
998
     *
999
     * If the chosen site theme is invalid, it'll be defaulted to the site default (the first theme defined)
1000
     *
1001
     * @param string $theme
1002
     */
1003
    public function setTheme($theme)
1004
    {
1005
        $themes = array_column(Service::getSiteThemes(), 'slug');
1006
1007
        if (!in_array($theme, $themes)) {
1008
            $theme = Service::getDefaultSiteTheme();
1009
        }
1010
1011
        return $this->updateProperty($this->theme, 'theme', $theme);
1012
    }
1013
1014
    /**
1015
     * Whether or not this player has color blind assistance enabled.
1016
     *
1017
     * @return bool
1018
     */
1019
    public function hasColorBlindAssist()
1020
    {
1021
        $this->lazyLoad();
1022
1023
        return (bool)$this->color_blind_enabled;
1024
    }
1025
1026
    /**
1027
     * Set a player's setting for color blind assistance.
1028
     *
1029
     * @param bool $enabled
1030
     *
1031
     * @return self
1032
     */
1033
    public function setColorBlindAssist($enabled)
1034
    {
1035
        return $this->updateProperty($this->color_blind_enabled, 'color_blind_enabled', $enabled);
1036
    }
1037
1038
    /**
1039
     * Updates this player's last login
1040
     */
1041
    public function updateLastLogin()
1042
    {
1043
        $this->update("last_login", TimeDate::now()->toMysql());
1044 76
    }
1045
1046 76
    /**
1047
     * Get the player's username
1048 76
     * @return string The username
1049 76
     */
1050 76
    public function getUsername()
1051
    {
1052
        return $this->name;
1053
    }
1054
1055
    /**
1056
     * Get the player's username, safe for use in your HTML
1057 76
     * @return string The username
1058
     */
1059
    public function getEscapedUsername()
1060
    {
1061
        return $this->getEscapedName();
1062
    }
1063
1064
    /**
1065
     * Alias for Player::setUsername()
1066
     *
1067
     * @param  string $username The new username
1068
     * @return self
1069 23
     */
1070
    public function setName($username)
1071 23
    {
1072
        return $this->setUsername($username);
1073
    }
1074
1075
    /**
1076
     * Mark all the unread messages of a player as read
1077
     *
1078
     * @return void
1079
     */
1080 1
    public function markMessagesAsRead()
1081
    {
1082 1
        $this->db->execute(
1083
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
1084 1
            array($this->id)
1085
        );
1086
    }
1087
1088
    /**
1089
     * Set the roles of a user
1090
     *
1091
     * @todo   Is it worth making this faster?
1092
     * @param  Role[] $roles The new roles of the user
1093
     * @return self
1094
     */
1095
    public function setRoles($roles)
1096
    {
1097
        $this->lazyLoad();
1098
1099
        $oldRoles = Role::mapToIds($this->roles);
1100
        $this->roles = $roles;
1101
        $roleIds = Role::mapToIds($roles);
1102 1
1103
        $newRoles     = array_diff($roleIds, $oldRoles);
1104 1
        $removedRoles = array_diff($oldRoles, $roleIds);
1105
1106
        foreach ($newRoles as $role) {
1107
            $this->modifyRole($role, 'add');
1108
        }
1109
1110
        foreach ($removedRoles as $role) {
1111
            $this->modifyRole($role, 'remove');
1112
        }
1113
1114
        $this->refresh();
1115
1116
        return $this;
1117
    }
1118
1119
    /**
1120
     * Give or remove a role to/form a player
1121
     *
1122
     * @param int    $role_id The role ID to add or remove
1123
     * @param string $action  Whether to "add" or "remove" a role for a player
1124
     *
1125
     * @return bool Whether the operation was successful or not
1126
     */
1127
    private function modifyRole($role_id, $action)
1128
    {
1129
        $role = Role::get($role_id);
1130
1131
        if ($role->isValid()) {
1132
            if ($action == "add") {
1133
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
1134
            } elseif ($action == "remove") {
1135
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
1136
            } else {
1137
                throw new Exception("Unrecognized role action");
1138
            }
1139
1140
            return true;
1141
        }
1142
1143
        return false;
1144
    }
1145
1146
    /**
1147
     * Given a player's BZID, get a player object
1148
     *
1149
     * @param  int    $bzid The player's BZID
1150
     * @return Player
1151
     */
1152
    public static function getFromBZID($bzid)
1153
    {
1154
        return self::get(self::fetchIdFrom($bzid, "bzid"));
1155
    }
1156
1157
    /**
1158
     * Get a single player by their username
1159
     *
1160
     * @param  string $username The username to look for
1161
     * @return Player
1162
     */
1163
    public static function getFromUsername($username)
1164
    {
1165
        $player = static::get(self::fetchIdFrom($username, 'username'));
1166
1167
        return $player->inject('name', $username);
1168
    }
1169
1170
    /**
1171
     * Get all the players in the database that have an active status
1172
     * @return Player[] An array of player BZIDs
1173
     */
1174
    public static function getPlayers()
1175
    {
1176
        return self::arrayIdToModel(
1177
            self::fetchIdsFrom("status", array("active", "test"), false)
1178
        );
1179
    }
1180
1181
    /**
1182
     * Show the number of notifications the user hasn't read yet
1183
     * @return int
1184
     */
1185
    public function countUnreadNotifications()
1186
    {
1187
        return Notification::countUnreadNotifications($this->id);
1188
    }
1189
1190
    /**
1191
     * Count the number of matches a player has participated in
1192
     * @return int
1193
     */
1194
    public function getMatchCount()
1195
    {
1196
        if ($this->cachedMatchCount === null) {
1197
            $this->cachedMatchCount = Match::getQueryBuilder()
1198
                ->active()
1199
                ->with($this)
1200
                ->count();
1201
        }
1202
1203
        return $this->cachedMatchCount;
1204
    }
1205
1206
    /**
1207
     * Get the (victory/total matches) ratio of the player
1208
     * @return float
1209
     */
1210
    public function getMatchWinRatio()
1211
    {
1212
        $count = $this->getMatchCount();
1213
1214
        if ($count == 0) {
1215
            return 0;
1216
        }
1217
1218
        $wins = Match::getQueryBuilder()
1219
            ->active()
1220
            ->with($this, 'win')
1221
            ->count();
1222
1223
        return $wins / $count;
1224
    }
1225
1226
    /**
1227
     * Get the (total caps made by team/total matches) ratio of the player
1228
     * @return float
1229 1
     */
1230
    public function getMatchAverageCaps()
1231 1
    {
1232 1
        $count = $this->getMatchCount();
1233
1234
        if ($count == 0) {
1235
            return 0;
1236
        }
1237
1238
        // Get the sum of team A points if the player was in team A, team B points if the player was in team B
1239
        $query = $this->db->query("
1240
            SELECT
1241 2
              SUM(
1242
                IF(mp.team_loyalty = 0, team_a_points, team_b_points)
1243 2
              ) AS sum
1244 2
            FROM
1245
              matches
1246
            INNER JOIN
1247
              match_participation mp ON mp.match_id = matches.id
1248
            WHERE
1249
              status = 'entered' AND mp.user_id = ?
1250
        ", [$this->id]);
1251 1
1252
        return $query[0]['sum'] / $count;
1253 1
    }
1254
1255
    /**
1256
     * Get the match activity in matches per day for a player
1257
     *
1258
     * @return float
1259 76
     */
1260
    public function getMatchActivity()
1261
    {
1262 76
        if ($this->matchActivity !== null) {
1263
            return $this->matchActivity;
1264
        }
1265
1266
        $activity = 0.0;
1267
1268
        $matches = Match::getQueryBuilder()
1269
            ->active()
1270
            ->with($this)
1271
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
1272 76
            ->getModels($fast = true);
1273
1274
        foreach ($matches as $match) {
1275
            $activity += $match->getActivity();
1276
        }
1277
1278 76
        return $activity;
1279
    }
1280
1281 76
    /**
1282
     * Return an array of matches this player participated in per month.
1283
     *
1284
     * ```
1285
     * ['yyyy-mm'] = <number of matches>
1286
     * ```
1287
     *
1288
     * @param TimeDate|string $timePeriod
1289
     *
1290
     * @return int[]
1291
     */
1292
    public function getMatchSummary($timePeriod = '1 year ago')
1293
    {
1294 76
        $since = ($timePeriod instanceof TimeDate) ? $timePeriod : TimeDate::from($timePeriod);
1295
1296
        if (!isset($this->cachedMatchSummary[(string)$timePeriod])) {
1297
            $this->cachedMatchSummary[(string)$timePeriod] = Match::getQueryBuilder()
1298
                ->active()
1299
                ->with($this)
1300
                ->where('time')->isAfter($since)
1301
                ->getSummary($since)
1302
            ;
1303
        }
1304
1305
        return $this->cachedMatchSummary[(string)$timePeriod];
1306
    }
1307
1308
    /**
1309
     * Show the number of messages the user hasn't read yet
1310
     * @return int
1311
     */
1312
    public function countUnreadMessages()
1313
    {
1314
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
1315
            $this->id, 'player_conversations'
0 ignored issues
show
Documentation introduced by
$this->id is of type integer, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1316
        );
1317
    }
1318
1319
    /**
1320
     * Get all of the members belonging to a team
1321
     * @param  int      $teamID The ID of the team to fetch the members of
1322
     * @return Player[] An array of Player objects of the team members
1323
     */
1324
    public static function getTeamMembers($teamID)
1325
    {
1326
        return self::arrayIdToModel(
1327
            self::fetchIds("WHERE team = ?", array($teamID))
1328
        );
1329 76
    }
1330
1331 76
    /**
1332 76
     * {@inheritdoc}
1333 76
     */
1334
    public static function getActiveStatuses()
1335 76
    {
1336 76
        return array('active', 'reported', 'test');
1337 76
    }
1338 76
1339 76
    /**
1340 76
     * {@inheritdoc}
1341 76
     */
1342 76
    public static function getEagerColumns($prefix = null)
1343 76
    {
1344 76
        $columns = [
1345 76
            'id',
1346 76
            'bzid',
1347
            'team',
1348
            'username',
1349 76
            'alias',
1350 76
            'status',
1351 76
            'avatar',
1352
            'country',
1353 76
        ];
1354
1355
        return self::formatColumns($prefix, $columns);
1356
    }
1357
1358
    /**
1359
     * {@inheritdoc}
1360
     */
1361
    public static function getLazyColumns($prefix = null)
1362
    {
1363
        $columns = [
1364
            'email',
1365
            'verified',
1366
            'receives',
1367
            'confirm_code',
1368
            'outdated',
1369
            'description',
1370
            'theme',
1371
            'color_blind_enabled',
1372
            'timezone',
1373 76
            'joined',
1374
            'last_login',
1375
            'last_match',
1376
            'admin_notes',
1377 76
        ];
1378
1379
        return self::formatColumns($prefix, $columns);
1380 76
    }
1381
1382 76
    /**
1383
     * Get a query builder for players
1384 76
     * @return PlayerQueryBuilder
1385
     */
1386
    public static function getQueryBuilder()
1387
    {
1388
        return new PlayerQueryBuilder('Player', array(
1389
            'columns' => array(
1390
                'name'     => 'username',
1391
                'team'     => 'team',
1392
                'outdated' => 'outdated',
1393
                'status'   => 'status',
1394
            ),
1395
            'name' => 'name',
1396
        ));
1397
    }
1398
1399
    /**
1400
     * Enter a new player to the database
1401
     * @param  int              $bzid        The player's bzid
1402
     * @param  string           $username    The player's username
1403
     * @param  int              $team        The player's team
1404
     * @param  string           $status      The player's status
1405
     * @param  int              $role_id     The player's role when they are first created
1406
     * @param  string           $avatar      The player's profile avatar
1407
     * @param  string           $description The player's profile description
1408
     * @param  int              $country     The player's country
1409
     * @param  string           $timezone    The player's timezone
1410
     * @param  string|\TimeDate $joined      The date the player joined
1411 1
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1412 1
     * @return Player           An object representing the player that was just entered
1413 1
     */
1414
    public static function newPlayer($bzid, $username, $team = null, $status = "active", $role_id = self::PLAYER, $avatar = "", $description = "", $country = 1, $timezone = null, $joined = "now", $last_login = "now")
1415
    {
1416
        $joined = TimeDate::from($joined);
1417
        $last_login = TimeDate::from($last_login);
1418
        $timezone = ($timezone) ?: date_default_timezone_get();
1419
1420 76
        $player = self::create(array(
1421
            'bzid'        => $bzid,
1422 76
            'team'        => $team,
1423
            'username'    => $username,
1424 76
            'alias'       => self::generateAlias($username),
1425 76
            'status'      => $status,
1426
            'avatar'      => $avatar,
1427
            'description' => $description,
1428
            'country'     => $country,
1429
            'timezone'    => $timezone,
1430
            'joined'      => $joined->toMysql(),
1431
            'last_login'  => $last_login->toMysql(),
1432
        ));
1433
1434 1
        $player->addRole($role_id);
1435
        $player->getIdenticon($player->getId());
1436 1
        $player->setUsername($username);
1437
1438
        return $player;
1439
    }
1440
1441
    /**
1442
     * Determine if a player exists in the database
1443
     * @param  int  $bzid The player's bzid
1444
     * @return bool Whether the player exists in the database
1445
     */
1446
    public static function playerBZIDExists($bzid)
1447 1
    {
1448
        return self::getFromBZID($bzid)->isValid();
1449 1
    }
1450
1451
    /**
1452 1
     * Change a player's callsign and add it to the database if it does not
1453
     * exist as a past callsign
1454
     *
1455
     * @param  string $username The new username of the player
1456
     * @return self
1457
     */
1458
    public function setUsername($username)
1459
    {
1460
        // The player's username was just fetched from BzDB, it's definitely not
1461
        // outdated
1462 1
        $this->setOutdated(false);
1463
1464 1
        // Players who have this player's username are considered outdated
1465
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1466
1467
        if ($username === $this->name) {
1468
            // The player's username hasn't changed, no need to do anything
1469
            return $this;
1470
        }
1471
1472
        // Players who used to have our player's username are not outdated anymore,
1473 1
        // unless they are more than one.
1474
        // Even though we are sure that the old and new usernames are not equal,
1475 1
        // MySQL makes a different type of string equality tests, which is why we
1476
        // also check IDs to make sure not to affect our own player's outdatedness.
1477
        $this->db->execute("
1478
            UPDATE {$this->table} SET outdated =
1479
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1480
            WHERE username = ? AND id != ?",
1481
            array($this->name, $this->id, $this->name, $this->id));
1482
1483
        $this->updateProperty($this->name, 'username', $username);
1484
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1485
        $this->resetAlias();
1486
1487
        return $this;
1488
    }
1489
1490
    /**
1491
     * Alphabetical order function for use in usort (case-insensitive)
1492
     * @return Closure The sort function
1493
     */
1494
    public static function getAlphabeticalSort()
1495
    {
1496
        return function (Player $a, Player $b) {
1497
            return strcasecmp($a->getUsername(), $b->getUsername());
1498
        };
1499
    }
1500
1501
    /**
1502
     * {@inheritdoc}
1503
     * @todo Add a constraint that does this automatically
1504
     */
1505
    public function wipe()
1506
    {
1507
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1508
1509
        parent::wipe();
1510
    }
1511
1512
    /**
1513
     * Find whether the player can delete a model
1514
     *
1515
     * @param  PermissionModel $model       The model that will be seen
1516
     * @param  bool         $showDeleted Whether to show deleted models to admins
1517
     * @return bool
1518
     */
1519
    public function canSee($model, $showDeleted = false)
1520
    {
1521
        return $model->canBeSeenBy($this, $showDeleted);
1522
    }
1523
1524
    /**
1525
     * Find whether the player can delete a model
1526
     *
1527
     * @param  PermissionModel $model The model that will be deleted
1528
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1529
     *                                to soft-delete ones
1530
     * @return bool
1531
     */
1532
    public function canDelete($model, $hard = false)
1533
    {
1534
        if ($hard) {
1535
            return $model->canBeHardDeletedBy($this);
1536
        } else {
1537
            return $model->canBeSoftDeletedBy($this);
1538
        }
1539
    }
1540
1541
    /**
1542
     * Find whether the player can create a model
1543
     *
1544
     * @param  string  $modelName The PHP class identifier of the model type
1545
     * @return bool
1546
     */
1547
    public function canCreate($modelName)
1548
    {
1549
        return $modelName::canBeCreatedBy($this);
1550
    }
1551
1552
    /**
1553
     * Find whether the player can edit a model
1554
     *
1555
     * @param  PermissionModel $model The model which will be edited
1556
     * @return bool
1557
     */
1558
    public function canEdit($model)
1559
    {
1560
        return $model->canBeEditedBy($this);
1561
    }
1562
}
1563