Completed
Pull Request — master (#178)
by Vladimir
05:41 queued 02:51
created

Player::setRoles()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 5.667

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 2
cts 6
cp 0.3333
rs 9.0856
c 0
b 0
f 0
cc 3
eloc 13
nc 4
nop 1
crap 5.667
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;
0 ignored issues
show
Unused Code introduced by
$seasonElo is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
362
363
        if (!isset($this->eloSeasonHistory[$seasonKey][$match->getId()])) {
364
            return;
365
        }
366
367
        $eloChangelogIndex = array_search($match->getId(), array_keys($this->eloSeasonHistory[$seasonKey]));
368
        $slicedChangeLog = array_slice($this->eloSeasonHistory[$seasonKey], 0, $eloChangelogIndex, true);
369 30
370
        $this->eloSeasonHistory[$seasonKey] = $slicedChangeLog;
371 30
        $this->eloSeason[$seasonKey] = end($slicedChangeLog)['elo'];
372
    }
373
374 30
    /**
375 29
     * Get the Elo changes for a player for a given season
376
     *
377
     * @param  string|null $season The season to get
378 30
     * @param  int|null    $year   The year of the season to get
379
     *
380
     * @return array
381
     */
382
    public function getEloSeasonHistory($season = null, $year = null)
383
    {
384
        $seasonKey = $this->buildSeasonKey($season, $year);
385
386
        // This season's already been cached
387
        if (isset($this->eloSeasonHistory[$seasonKey])) {
388
            return $this->eloSeasonHistory[$seasonKey];
389
        }
390
391
        $this->eloSeasonHistory[$seasonKey] = $this->db->query('
392 30
          SELECT
393
            elo_new AS elo,
394 30
            match_id AS `match`,
395 30
            MONTH(matches.timestamp) AS `month`,
396
            YEAR(matches.timestamp) AS `year`,
397 30
            DAY(matches.timestamp) AS `day`
398 30
          FROM
399 30
            player_elo
400
            LEFT JOIN matches ON player_elo.match_id = matches.id
401
          WHERE
402 30
            user_id = ? AND season_period = ? AND season_year = ?
403
          ORDER BY
404
            match_id ASC
405
        ', [ $this->getId(), $season, $year ]);
406
407
        array_unshift($this->eloSeasonHistory[$seasonKey], [
408
            'elo' => 1200,
409
            'match' => null,
410
            'month' => Season::getCurrentSeasonRange($season)->getStartOfRange()->month,
411
            'year' => $year,
412
            'day' => 1
413
        ]);
414
415 30
        return $this->eloSeasonHistory[$seasonKey];
416
    }
417 30
418 30
    /**
419
     * Get the player's Elo for a season.
420 30
     *
421 29
     * With the default arguments, it will fetch the Elo for the current season.
422
     *
423
     * @param string|null $season The season we're looking for: winter, spring, summer, or fall
424 30
     * @param int|null    $year   The year of the season we're looking for
425
     *
426 30
     * @return int The player's Elo
427 30
     */
428 30
    public function getElo($season = null, $year = null)
429
    {
430
        // The Elo for this player has been forcefully set from a trusted database query, so just return that.
431
        if ($this->elo !== null) {
432
            return $this->elo;
433 30
        }
434
435
        $this->getEloSeasonHistory($season, $year);
436
        $seasonKey = $this->buildSeasonKey($season, $year);
437
438
        if (isset($this->eloSeason[$seasonKey])) {
439
            return $this->eloSeason[$seasonKey];
440
        }
441
442
        $season = &$this->eloSeasonHistory[$seasonKey];
443
444
        if (!empty($season)) {
445 30
            $elo = end($season);
446
            $this->eloSeason[$seasonKey] = ($elo !== false) ? $elo['elo'] : 1200;
447 30
        } else {
448 30
            $this->eloSeason[$seasonKey] = 1200;
449
        }
450
451 30
        return $this->eloSeason[$seasonKey];
452 30
    }
453
454 30
    /**
455
     * Adjust the Elo of a player for the current season based on a Match
456 30
     *
457 29
     * **Warning:** If $match is null, the Elo for the player will be modified but the value will not be persisted to
458
     * the database.
459 29
     *
460
     * @param int        $adjust The value to be added to the current ELO (negative to subtract)
461 30
     * @param Match|null $match  The match where this Elo change took place
462
     */
463
    public function adjustElo($adjust, Match $match = null)
464
    {
465
        $timestamp = ($match !== null) ? $match->getTimestamp() : (Carbon::now());
466
        $seasonInfo = Season::getSeason($timestamp);
467
468
        // Get the current Elo for the player, even if it's the default 1200. We need the value for adjusting
469
        $elo = $this->getElo($seasonInfo['season'], $seasonInfo['year']);
470
        $seasonKey = sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
471
472
        $this->eloSeason[$seasonKey] += $adjust;
473
474
        if ($match !== null && $this->isValid()) {
475
            $this->eloSeasonHistory[$seasonKey][$match->getId()] = [
476
                'elo' => $this->eloSeason[$seasonKey],
477
                'match' => $match->getId(),
478
                'month' => $match->getTimestamp()->month,
479
                'year' => $match->getTimestamp()->year,
480
                'day' => null,
481
            ];
482
483
            $this->db->execute('
484
              INSERT INTO player_elo VALUES (?, ?, ?, ?, ?, ?)
485
            ', [ $this->getId(), $match->getId(), $seasonInfo['season'], $seasonInfo['year'], $elo, $this->eloSeason[$seasonKey] ]);
486
        }
487
    }
488
489
    /**
490
     * Returns whether the player has verified their e-mail address
491
     *
492
     * @return bool `true` for verified players
493
     */
494
    public function isVerified()
495
    {
496
        $this->lazyLoad();
497
498
        return $this->verified;
499
    }
500
501
    /**
502
     * Returns the confirmation code for the player's e-mail address verification
503
     *
504
     * @return string The player's confirmation code
505
     */
506 1
    public function getConfirmCode()
507
    {
508 1
        $this->lazyLoad();
509
510 1
        return $this->confirmCode;
511
    }
512 1
513
    /**
514
     * Returns what kind of events the player should be e-mailed about
515
     *
516
     * @return string The type of notifications
517
     */
518
    public function getReceives()
519
    {
520
        $this->lazyLoad();
521
522
        return $this->receives;
523
    }
524
525
    /**
526
     * Finds out whether the specified player wants and can receive an e-mail
527
     * message
528
     *
529
     * @param  string  $type
530
     * @return bool `true` if the player should be sent an e-mail
531
     */
532
    public function canReceive($type)
533
    {
534
        $this->lazyLoad();
535
536
        if (!$this->email || !$this->isVerified()) {
537
            // Unverified e-mail means the user will receive nothing
538
            return false;
539
        }
540
541
        if ($this->receives == 'everything') {
542
            return true;
543
        }
544
545
        return $this->receives == $type;
546
    }
547
548
    /**
549
     * Find out whether the specified confirmation code is correct
550
     *
551
     * This method protects against timing attacks
552
     *
553
     * @param  string $code The confirmation code to check
554
     * @return bool `true` for a correct e-mail verification code
555
     */
556
    public function isCorrectConfirmCode($code)
557
    {
558
        $this->lazyLoad();
559
560
        if ($this->confirmCode === null) {
561
            return false;
562
        }
563
564
        return StringUtils::equals($code, $this->confirmCode);
565
    }
566
567
    /**
568
     * Get the player's sanitized description
569
     * @return string The description
570
     */
571
    public function getDescription()
572
    {
573
        $this->lazyLoad();
574
575
        return $this->description;
576
    }
577
578
    /**
579
     * Get the joined date of the player
580
     * @return TimeDate The joined date of the player
581
     */
582
    public function getJoinedDate()
583
    {
584
        $this->lazyLoad();
585
586
        return $this->joined->copy();
587
    }
588
589
    /**
590
     * Get all of the known IPs used by the player
591
     *
592
     * @return string[][] An array containing IPs and hosts
593
     */
594
    public function getKnownIPs()
595
    {
596
        return $this->db->query(
597
            'SELECT DISTINCT ip, host FROM visits WHERE player = ? GROUP BY ip, host ORDER BY MAX(timestamp) DESC LIMIT 10',
598
            array($this->getId())
599
        );
600
    }
601
602
    /**
603
     * Get the last login for a player
604
     * @return TimeDate The date of the last login
605
     */
606
    public function getLastLogin()
607
    {
608
        $this->lazyLoad();
609
610
        return $this->last_login->copy();
611 23
    }
612
613 23
    /**
614
     * Get the last match
615
     * @return Match
616
     */
617
    public function getLastMatch()
618
    {
619
        $this->lazyLoad();
620 1
621
        return $this->last_match;
622 1
    }
623
624 1
    /**
625
     * Get all of the callsigns a player has used to log in to the website
626
     * @return string[] An array containing all of the past callsigns recorded for a player
627
     */
628
    public function getPastCallsigns()
629
    {
630
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
631
    }
632
633
    /**
634
     * Get the player's team
635
     * @return Team The object representing the team
636
     */
637
    public function getTeam()
638
    {
639
        return Team::get($this->team);
640
    }
641 76
642
    /**
643 76
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
644 76
     * @return string The timezone
645
     */
646 76
    public function getTimezone()
647 76
    {
648
        $this->lazyLoad();
649 76
650
        return ($this->timezone) ?: date_default_timezone_get();
651
    }
652
653
    /**
654
     * Get the roles of the player
655
     * @return Role[]
656
     */
657
    public function getRoles()
658 2
    {
659
        $this->lazyLoad();
660 2
661 1
        return $this->roles;
662
    }
663
664 2
    /**
665
     * Rebuild the list of permissions a user has been granted
666 2
     */
667
    private function updateUserPermissions()
668
    {
669
        $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...
670
        $this->permissions = array();
671
672
        foreach ($this->roles as $role) {
673
            $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...
674
        }
675
    }
676
677
    /**
678
     * Check if a player has a specific permission
679
     *
680
     * @param string|null $permission The permission to check for
681
     *
682
     * @return bool Whether or not the player has the permission
683
     */
684
    public function hasPermission($permission)
685
    {
686
        if ($permission === null) {
687
            return false;
688
        }
689
690
        $this->lazyLoad();
691
692
        return isset($this->permissions[$permission]);
693
    }
694
695
    /**
696
     * Check whether or not a player been in a match or has logged on in the specified amount of time to be considered
697
     * active
698
     *
699
     * @return bool True if the player has been active
700
     */
701
    public function hasBeenActive()
702
    {
703
        $this->lazyLoad();
704
705
        $interval  = Service::getParameter('bzion.miscellaneous.active_interval');
706
        $lastLogin = $this->last_login->copy()->modify($interval);
707
708
        $hasBeenActive = (TimeDate::now() <= $lastLogin);
709
710
        if ($this->last_match->isValid()) {
711
            $lastMatch = $this->last_match->getTimestamp()->copy()->modify($interval);
712
            $hasBeenActive = ($hasBeenActive || TimeDate::now() <= $lastMatch);
713
        }
714
715
        return $hasBeenActive;
716
    }
717
718
    /**
719
     * Check whether the callsign of the player is outdated
720
     *
721
     * Returns true if this player has probably changed their callsign, making
722 1
     * the current username stored in the database obsolete
723
     *
724 1
     * @return bool Whether or not the player is disabled
725
     */
726
    public function isOutdated()
727
    {
728
        $this->lazyLoad();
729
730
        return $this->outdated;
731
    }
732 55
733
    /**
734 55
     * Check if a player's account has been disabled
735
     *
736
     * @return bool Whether or not the player is disabled
737
     */
738
    public function isDisabled()
739
    {
740 1
        return $this->status == "disabled";
741
    }
742 1
743
    /**
744
     * Check if everyone can log in as this user on a test environment
745
     *
746 1
     * @return bool
747
     */
748
    public function isTestUser()
749
    {
750
        return $this->status == "test";
751
    }
752
753
    /**
754
     * Check if a player is teamless
755
     *
756
     * @return bool True if the player is teamless
757
     */
758
    public function isTeamless()
759
    {
760
        return empty($this->team);
761
    }
762
763
    /**
764
     * Mark a player's account as banned
765
     *
766 2
     * @deprecated The players table shouldn't have any indicators of banned status, the Bans table is the authoritative source
767
     */
768 2
    public function markAsBanned()
769
    {
770
        return;
0 ignored issues
show
Coding Style introduced by
Empty return statement not required here
Loading history...
771
    }
772
773
    /**
774
     * Mark a player's account as unbanned
775
     *
776
     * @deprecated The players table shouldn't have any indicators of banned status, the Bans table is the authoritative source
777
     */
778
    public function markAsUnbanned()
779
    {
780
        return;
0 ignored issues
show
Coding Style introduced by
Empty return statement not required here
Loading history...
781
    }
782
783
    /**
784
     * Find out if a player is hard banned
785
     *
786
     * @return bool
787
     */
788
    public function isBanned()
789
    {
790
        $ban = Ban::getBan($this->id);
791
792
        return ($ban !== null && !$ban->isSoftBan());
793
    }
794
795
    /**
796
     * Get the ban of the player
797
     *
798
     * This method performs a load of all the lazy parameters of the Player
799
     *
800
     * @return Ban|null The current ban of the player, or null if the player is
801
     *                  is not banned
802
     */
803
    public function getBan()
804
    {
805
        $this->lazyLoad();
806
807
        return $this->ban;
808
    }
809
810
    /**
811
     * Remove a player from a role
812
     *
813
     * @param int $role_id The role ID to add or remove
814
     *
815
     * @return bool Whether the operation was successful or not
816
     */
817
    public function removeRole($role_id)
818
    {
819
        $status = $this->modifyRole($role_id, "remove");
820
        $this->refresh();
821
822
        return $status;
823
    }
824
825
    /**
826
     * Set the player's email address and reset their verification status
827
     * @param string $email The address
828
     */
829
    public function setEmailAddress($email)
830
    {
831
        $this->lazyLoad();
832
833
        if ($this->email == $email) {
834
            // The e-mail hasn't changed, don't do anything
835
            return;
836
        }
837
838
        $this->setVerified(false);
839
        $this->generateNewConfirmCode();
840
841
        $this->updateProperty($this->email, 'email', $email);
842
    }
843
844
    /**
845
     * Set whether the player has verified their e-mail address
846
     *
847
     * @param  bool $verified Whether the player is verified or not
848
     * @return self
849
     */
850
    public function setVerified($verified)
851
    {
852
        $this->lazyLoad();
853
854
        if ($verified) {
855
            $this->setConfirmCode(null);
856
        }
857
858
        return $this->updateProperty($this->verified, 'verified', $verified);
859
    }
860
861
    /**
862
     * Generate a new random confirmation token for e-mail address verification
863
     *
864
     * @return self
865
     */
866
    public function generateNewConfirmCode()
867
    {
868
        $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...
869
        $random = $generator->nextBytes(16);
870
871
        return $this->setConfirmCode(bin2hex($random));
872
    }
873
874
    /**
875
     * Set the confirmation token for e-mail address verification
876
     *
877
     * @param  string $code The confirmation code
878
     * @return self
879
     */
880
    private function setConfirmCode($code)
881
    {
882 76
        $this->lazyLoad();
883
884 76
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
885
    }
886 76
887
    /**
888
     * Set what kind of events the player should be e-mailed about
889
     *
890
     * @param  string $receives The type of notification
891
     * @return self
892
     */
893
    public function setReceives($receives)
894
    {
895
        $this->lazyLoad();
896
897
        return $this->updateProperty($this->receives, 'receives', $receives);
898
    }
899
900
    /**
901
     * Set whether the callsign of the player is outdated
902
     *
903
     * @param  bool $outdated Whether the callsign is outdated
904
     * @return self
905
     */
906
    public function setOutdated($outdated)
907
    {
908
        $this->lazyLoad();
909
910
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
911 55
    }
912
913 55
    /**
914 55
     * Set the player's description
915
     * @param string $description The description
916
     */
917
    public function setDescription($description)
918
    {
919
        $this->updateProperty($this->description, "description", $description);
920
    }
921 29
922
    /**
923 29
     * Set the player's timezone
924 29
     * @param string $timezone The timezone
925
     */
926
    public function setTimezone($timezone)
927
    {
928
        $this->updateProperty($this->timezone, "timezone", $timezone);
929
    }
930
931
    /**
932
     * Set the player's team
933
     * @param int $team The team's ID
934
     */
935
    public function setTeam($team)
936
    {
937
        $this->updateProperty($this->team, "team", $team);
938
    }
939
940
    /**
941
     * Set the match the player last participated in
942
     *
943
     * @param int $match The match's ID
944
     */
945
    public function setLastMatch($match)
946
    {
947
        $this->updateProperty($this->last_match, 'last_match', $match);
948
    }
949
950
    /**
951
     * Set the player's status
952
     * @param string $status The new status
953
     */
954
    public function setStatus($status)
955
    {
956
        $this->updateProperty($this->status, 'status', $status);
957
    }
958
959
    /**
960
     * Set the player's admin notes
961
     * @param  string $admin_notes The new admin notes
962
     * @return self
963
     */
964
    public function setAdminNotes($admin_notes)
965
    {
966
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
967 1
    }
968
969 1
    /**
970
     * Set the player's country
971
     * @param  int   $country The ID of the new country
972
     * @return self
973
     */
974
    public function setCountry($country)
975
    {
976 1
        return $this->updateProperty($this->country, 'country', $country);
977
    }
978 1
979
    /**
980
     * Get the player's chosen theme preference
981
     *
982
     * @return string
983
     */
984
    public function getTheme()
985
    {
986
        $this->lazyLoad();
987
988
        return $this->theme;
989
    }
990
991
    /**
992
     * Set the site theme for the player
993
     *
994
     * If the chosen site theme is invalid, it'll be defaulted to the site default (the first theme defined)
995
     *
996
     * @param string $theme
997
     */
998
    public function setTheme($theme)
999
    {
1000
        $themes = array_column(Service::getSiteThemes(), 'slug');
1001
1002
        if (!in_array($theme, $themes)) {
1003
            $theme = Service::getDefaultSiteTheme();
1004
        }
1005
1006
        return $this->updateProperty($this->theme, 'theme', $theme);
1007
    }
1008
1009
    /**
1010
     * Whether or not this player has color blind assistance enabled.
1011
     *
1012
     * @return bool
1013
     */
1014
    public function hasColorBlindAssist()
1015
    {
1016
        $this->lazyLoad();
1017
1018
        return (bool)$this->color_blind_enabled;
1019
    }
1020
1021
    /**
1022
     * Set a player's setting for color blind assistance.
1023
     *
1024
     * @param bool $enabled
1025
     *
1026
     * @return self
1027
     */
1028
    public function setColorBlindAssist($enabled)
1029
    {
1030
        return $this->updateProperty($this->color_blind_enabled, 'color_blind_enabled', $enabled);
1031
    }
1032
1033
    /**
1034
     * Updates this player's last login
1035
     */
1036
    public function updateLastLogin()
1037
    {
1038
        $this->update("last_login", TimeDate::now()->toMysql());
1039
    }
1040
1041
    /**
1042
     * Get the player's username
1043
     * @return string The username
1044 76
     */
1045
    public function getUsername()
1046 76
    {
1047
        return $this->name;
1048 76
    }
1049 76
1050 76
    /**
1051
     * Get the player's username, safe for use in your HTML
1052
     * @return string The username
1053
     */
1054
    public function getEscapedUsername()
1055
    {
1056
        return $this->getEscapedName();
1057 76
    }
1058
1059
    /**
1060
     * Alias for Player::setUsername()
1061
     *
1062
     * @param  string $username The new username
1063
     * @return self
1064
     */
1065
    public function setName($username)
1066
    {
1067
        return $this->setUsername($username);
1068
    }
1069 23
1070
    /**
1071 23
     * Mark all the unread messages of a player as read
1072
     *
1073
     * @return void
1074
     */
1075
    public function markMessagesAsRead()
1076
    {
1077
        $this->db->execute(
1078
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
1079
            array($this->id)
1080 1
        );
1081
    }
1082 1
1083
    /**
1084 1
     * Set the roles of a user
1085
     *
1086
     * @todo   Is it worth making this faster?
1087
     * @param  Role[] $roles The new roles of the user
1088
     * @return self
1089
     */
1090
    public function setRoles($roles)
1091
    {
1092
        $this->lazyLoad();
1093
1094
        $oldRoles = Role::mapToIds($this->roles);
1095
        $this->roles = $roles;
1096
        $roleIds = Role::mapToIds($roles);
1097
1098
        $newRoles     = array_diff($roleIds, $oldRoles);
1099
        $removedRoles = array_diff($oldRoles, $roleIds);
1100
1101
        foreach ($newRoles as $role) {
1102 1
            $this->modifyRole($role, 'add');
1103
        }
1104 1
1105
        foreach ($removedRoles as $role) {
1106
            $this->modifyRole($role, 'remove');
1107
        }
1108
1109
        $this->refresh();
1110
1111
        return $this;
1112
    }
1113
1114
    /**
1115
     * Give or remove a role to/form a player
1116
     *
1117
     * @param int    $role_id The role ID to add or remove
1118
     * @param string $action  Whether to "add" or "remove" a role for a player
1119
     *
1120
     * @return bool Whether the operation was successful or not
1121
     */
1122
    private function modifyRole($role_id, $action)
1123
    {
1124
        $role = Role::get($role_id);
1125
1126
        if ($role->isValid()) {
1127
            if ($action == "add") {
1128
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
1129
            } elseif ($action == "remove") {
1130
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
1131
            } else {
1132
                throw new Exception("Unrecognized role action");
1133
            }
1134
1135
            return true;
1136
        }
1137
1138
        return false;
1139
    }
1140
1141
    /**
1142
     * Given a player's BZID, get a player object
1143
     *
1144
     * @param  int    $bzid The player's BZID
1145
     * @return Player
1146
     */
1147
    public static function getFromBZID($bzid)
1148
    {
1149
        return self::get(self::fetchIdFrom($bzid, "bzid"));
1150
    }
1151
1152
    /**
1153
     * Get a single player by their username
1154
     *
1155
     * @param  string $username The username to look for
1156
     * @return Player
1157
     */
1158
    public static function getFromUsername($username)
1159
    {
1160
        $player = static::get(self::fetchIdFrom($username, 'username'));
1161
1162
        return $player->inject('name', $username);
1163
    }
1164
1165
    /**
1166
     * Get all the players in the database that have an active status
1167
     * @return Player[] An array of player BZIDs
1168
     */
1169
    public static function getPlayers()
1170
    {
1171
        return self::arrayIdToModel(
1172
            self::fetchIdsFrom("status", array("active", "test"), false)
1173
        );
1174
    }
1175
1176
    /**
1177
     * Show the number of notifications the user hasn't read yet
1178
     * @return int
1179
     */
1180
    public function countUnreadNotifications()
1181
    {
1182
        return Notification::countUnreadNotifications($this->id);
1183
    }
1184
1185
    /**
1186
     * Count the number of matches a player has participated in
1187
     * @return int
1188
     */
1189
    public function getMatchCount()
1190
    {
1191
        if ($this->cachedMatchCount === null) {
1192
            $this->cachedMatchCount = Match::getQueryBuilder()
1193
                ->active()
1194
                ->with($this)
1195
                ->count();
1196
        }
1197
1198
        return $this->cachedMatchCount;
1199
    }
1200
1201
    /**
1202
     * Get the (victory/total matches) ratio of the player
1203
     * @return float
1204
     */
1205
    public function getMatchWinRatio()
1206
    {
1207
        $count = $this->getMatchCount();
1208
1209
        if ($count == 0) {
1210
            return 0;
1211
        }
1212
1213
        $wins = Match::getQueryBuilder()
1214
            ->active()
1215
            ->with($this, 'win')
1216
            ->count();
1217
1218
        return $wins / $count;
1219
    }
1220
1221
    /**
1222
     * Get the (total caps made by team/total matches) ratio of the player
1223
     * @return float
1224
     */
1225
    public function getMatchAverageCaps()
1226
    {
1227
        $count = $this->getMatchCount();
1228
1229 1
        if ($count == 0) {
1230
            return 0;
1231 1
        }
1232 1
1233
        // Get the sum of team A points if the player was in team A, team B points if the player was in team B
1234
        $query = $this->db->query("
1235
            SELECT
1236
              SUM(
1237
                IF(mp.team_loyalty = 0, team_a_points, team_b_points)
1238
              ) AS sum
1239
            FROM
1240
              matches
1241 2
            INNER JOIN
1242
              match_participation mp ON mp.match_id = matches.id
1243 2
            WHERE
1244 2
              status = 'entered' AND mp.user_id = ?
1245
        ", [$this->id]);
1246
1247
        return $query[0]['sum'] / $count;
1248
    }
1249
1250
    /**
1251 1
     * Get the match activity in matches per day for a player
1252
     *
1253 1
     * @return float
1254
     */
1255
    public function getMatchActivity()
1256
    {
1257
        if ($this->matchActivity !== null) {
1258
            return $this->matchActivity;
1259 76
        }
1260
1261
        $activity = 0.0;
1262 76
1263
        $matches = Match::getQueryBuilder()
1264
            ->active()
1265
            ->with($this)
1266
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
1267
            ->getModels($fast = true);
1268
1269
        foreach ($matches as $match) {
1270
            $activity += $match->getActivity();
1271
        }
1272 76
1273
        return $activity;
1274
    }
1275
1276
    /**
1277
     * Return an array of matches this player participated in per month.
1278 76
     *
1279
     * ```
1280
     * ['yyyy-mm'] = <number of matches>
1281 76
     * ```
1282
     *
1283
     * @param TimeDate|string $timePeriod
1284
     *
1285
     * @return int[]
1286
     */
1287
    public function getMatchSummary($timePeriod = '1 year ago')
1288
    {
1289
        $since = ($timePeriod instanceof TimeDate) ? $timePeriod : TimeDate::from($timePeriod);
1290
1291
        if (!isset($this->cachedMatchSummary[(string)$timePeriod])) {
1292
            $this->cachedMatchSummary[(string)$timePeriod] = Match::getQueryBuilder()
1293
                ->active()
1294 76
                ->with($this)
1295
                ->where('time')->isAfter($since)
1296
                ->getSummary($since)
1297
            ;
1298
        }
1299
1300
        return $this->cachedMatchSummary[(string)$timePeriod];
1301
    }
1302
1303
    /**
1304
     * Show the number of messages the user hasn't read yet
1305
     * @return int
1306
     */
1307
    public function countUnreadMessages()
1308
    {
1309
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
1310
            $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...
1311
        );
1312
    }
1313
1314
    /**
1315
     * Get all of the members belonging to a team
1316
     * @param  int      $teamID The ID of the team to fetch the members of
1317
     * @return Player[] An array of Player objects of the team members
1318
     */
1319
    public static function getTeamMembers($teamID)
1320
    {
1321
        return self::arrayIdToModel(
1322
            self::fetchIds("WHERE team = ?", array($teamID))
1323
        );
1324
    }
1325
1326
    /**
1327
     * {@inheritdoc}
1328
     */
1329 76
    public static function getActiveStatuses()
1330
    {
1331 76
        return array('active', 'reported', 'test');
1332 76
    }
1333 76
1334
    /**
1335 76
     * {@inheritdoc}
1336 76
     */
1337 76
    public static function getEagerColumns($prefix = null)
1338 76
    {
1339 76
        $columns = [
1340 76
            'id',
1341 76
            'bzid',
1342 76
            'team',
1343 76
            'username',
1344 76
            'alias',
1345 76
            'status',
1346 76
            'avatar',
1347
            'country',
1348
        ];
1349 76
1350 76
        return self::formatColumns($prefix, $columns);
1351 76
    }
1352
1353 76
    /**
1354
     * {@inheritdoc}
1355
     */
1356
    public static function getLazyColumns($prefix = null)
1357
    {
1358
        $columns = [
1359
            'email',
1360
            'verified',
1361
            'receives',
1362
            'confirm_code',
1363
            'outdated',
1364
            'description',
1365
            'theme',
1366
            'color_blind_enabled',
1367
            'timezone',
1368
            'joined',
1369
            'last_login',
1370
            'last_match',
1371
            'admin_notes',
1372
        ];
1373 76
1374
        return self::formatColumns($prefix, $columns);
1375
    }
1376
1377 76
    /**
1378
     * Get a query builder for players
1379
     * @return PlayerQueryBuilder
1380 76
     */
1381
    public static function getQueryBuilder()
1382 76
    {
1383
        return new PlayerQueryBuilder('Player', array(
1384 76
            'columns' => array(
1385
                'name'     => 'username',
1386
                'team'     => 'team',
1387
                'outdated' => 'outdated',
1388
                'status'   => 'status',
1389
            ),
1390
            'name' => 'name',
1391
        ));
1392
    }
1393
1394
    /**
1395
     * Enter a new player to the database
1396
     * @param  int              $bzid        The player's bzid
1397
     * @param  string           $username    The player's username
1398
     * @param  int              $team        The player's team
1399
     * @param  string           $status      The player's status
1400
     * @param  int              $role_id     The player's role when they are first created
1401
     * @param  string           $avatar      The player's profile avatar
1402
     * @param  string           $description The player's profile description
1403
     * @param  int              $country     The player's country
1404
     * @param  string           $timezone    The player's timezone
1405
     * @param  string|\TimeDate $joined      The date the player joined
1406
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1407
     * @return Player           An object representing the player that was just entered
1408
     */
1409
    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")
1410
    {
1411 1
        $joined = TimeDate::from($joined);
1412 1
        $last_login = TimeDate::from($last_login);
1413 1
        $timezone = ($timezone) ?: date_default_timezone_get();
1414
1415
        $player = self::create(array(
1416
            'bzid'        => $bzid,
1417
            'team'        => $team,
1418
            'username'    => $username,
1419
            'alias'       => self::generateAlias($username),
1420 76
            'status'      => $status,
1421
            'avatar'      => $avatar,
1422 76
            'description' => $description,
1423
            'country'     => $country,
1424 76
            'timezone'    => $timezone,
1425 76
            'joined'      => $joined->toMysql(),
1426
            'last_login'  => $last_login->toMysql(),
1427
        ));
1428
1429
        $player->addRole($role_id);
1430
        $player->getIdenticon($player->getId());
1431
        $player->setUsername($username);
1432
1433
        return $player;
1434 1
    }
1435
1436 1
    /**
1437
     * Determine if a player exists in the database
1438
     * @param  int  $bzid The player's bzid
1439
     * @return bool Whether the player exists in the database
1440
     */
1441
    public static function playerBZIDExists($bzid)
1442
    {
1443
        return self::getFromBZID($bzid)->isValid();
1444
    }
1445
1446
    /**
1447 1
     * Change a player's callsign and add it to the database if it does not
1448
     * exist as a past callsign
1449 1
     *
1450
     * @param  string $username The new username of the player
1451
     * @return self
1452 1
     */
1453
    public function setUsername($username)
1454
    {
1455
        // The player's username was just fetched from BzDB, it's definitely not
1456
        // outdated
1457
        $this->setOutdated(false);
1458
1459
        // Players who have this player's username are considered outdated
1460
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1461
1462 1
        if ($username === $this->name) {
1463
            // The player's username hasn't changed, no need to do anything
1464 1
            return $this;
1465
        }
1466
1467
        // Players who used to have our player's username are not outdated anymore,
1468
        // unless they are more than one.
1469
        // Even though we are sure that the old and new usernames are not equal,
1470
        // MySQL makes a different type of string equality tests, which is why we
1471
        // also check IDs to make sure not to affect our own player's outdatedness.
1472
        $this->db->execute("
1473 1
            UPDATE {$this->table} SET outdated =
1474
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1475 1
            WHERE username = ? AND id != ?",
1476
            array($this->name, $this->id, $this->name, $this->id));
1477
1478
        $this->updateProperty($this->name, 'username', $username);
1479
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1480
        $this->resetAlias();
1481
1482
        return $this;
1483
    }
1484
1485
    /**
1486
     * Alphabetical order function for use in usort (case-insensitive)
1487
     * @return Closure The sort function
1488
     */
1489
    public static function getAlphabeticalSort()
1490
    {
1491
        return function (Player $a, Player $b) {
1492
            return strcasecmp($a->getUsername(), $b->getUsername());
1493
        };
1494
    }
1495
1496
    /**
1497
     * {@inheritdoc}
1498
     * @todo Add a constraint that does this automatically
1499
     */
1500
    public function wipe()
1501
    {
1502
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1503
1504
        parent::wipe();
1505
    }
1506
1507
    /**
1508
     * Find whether the player can delete a model
1509
     *
1510
     * @param  PermissionModel $model       The model that will be seen
1511
     * @param  bool         $showDeleted Whether to show deleted models to admins
1512
     * @return bool
1513
     */
1514
    public function canSee($model, $showDeleted = false)
1515
    {
1516
        return $model->canBeSeenBy($this, $showDeleted);
1517
    }
1518
1519
    /**
1520
     * Find whether the player can delete a model
1521
     *
1522
     * @param  PermissionModel $model The model that will be deleted
1523
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1524
     *                                to soft-delete ones
1525
     * @return bool
1526
     */
1527
    public function canDelete($model, $hard = false)
1528
    {
1529
        if ($hard) {
1530
            return $model->canBeHardDeletedBy($this);
1531
        } else {
1532
            return $model->canBeSoftDeletedBy($this);
1533
        }
1534
    }
1535
1536
    /**
1537
     * Find whether the player can create a model
1538
     *
1539
     * @param  string  $modelName The PHP class identifier of the model type
1540
     * @return bool
1541
     */
1542
    public function canCreate($modelName)
1543
    {
1544
        return $modelName::canBeCreatedBy($this);
1545
    }
1546
1547
    /**
1548
     * Find whether the player can edit a model
1549
     *
1550
     * @param  PermissionModel $model The model which will be edited
1551
     * @return bool
1552
     */
1553
    public function canEdit($model)
1554
    {
1555
        return $model->canBeEditedBy($this);
1556
    }
1557
}
1558