Completed
Push — master ( 8a441b...ebc209 )
by Vladimir
04:28
created

Player::getElo()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 8
cts 8
cp 1
rs 8.439
c 0
b 0
f 0
cc 5
eloc 14
nc 5
nop 2
crap 5
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
     * The player's timezone PHP identifier, e.g. "Europe/Paris"
95
     * @var string
96
     */
97
    protected $timezone;
98
99
    /**
100
     * The date the player joined the site
101
     * @var TimeDate
102
     */
103
    protected $joined;
104
105
    /**
106
     * The date of the player's last login
107
     * @var TimeDate
108
     */
109
    protected $last_login;
110
111
    /**
112
     * The date of the player's last match
113
     * @var Match
114
     */
115
    protected $last_match;
116
117
    /**
118
     * The roles a player belongs to
119
     * @var Role[]
120
     */
121
    protected $roles;
122
123
    /**
124
     * The permissions a player has
125
     * @var Permission[]
126
     */
127
    protected $permissions;
128
129
    /**
130
     * A section for admins to write notes about players
131
     * @var string
132
     */
133
    protected $admin_notes;
134
135
    /**
136
     * The ban of the player, or null if the player is not banned
137
     * @var Ban|null
138
     */
139
    protected $ban;
140
141
    /**
142
     * Cached results for match summaries
143
     *
144
     * @var array
145
     */
146
    private $cachedMatchSummary;
147
148
    /**
149
     * The cached match count for a player
150
     *
151
     * @var int
152
     */
153
    private $cachedMatchCount = null;
154
155
    /**
156
     * The Elo for this player that has been explicitly set for this player from a database query. This value will take
157
     * precedence over having to build to an Elo season history.
158
     *
159
     * @var int
160
     */
161
    private $elo;
162
    private $eloSeason;
163
    private $eloSeasonHistory;
164
165
    private $matchActivity;
166
167
    /**
168
     * The name of the database table used for queries
169
     */
170
    const TABLE = "players";
171
172
    /**
173
     * The location where avatars will be stored
174
     */
175
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
176
177 76
    const EDIT_PERMISSION = Permission::EDIT_USER;
178
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
179 76
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
180 76
181 76
    /**
182 76
     * {@inheritdoc}
183 76
     */
184 76
    protected function assignResult($player)
185 76
    {
186
        $this->bzid = $player['bzid'];
187 76
        $this->name = $player['username'];
188
        $this->alias = $player['alias'];
189
        $this->team = $player['team'];
190 76
        $this->status = $player['status'];
191
        $this->avatar = $player['avatar'];
192
        $this->country = $player['country'];
193
194
        if (array_key_exists('activity', $player)) {
195 76
            $this->matchActivity = ($player['activity'] != null) ? $player['activity'] : 0.0;
196
        }
197 76
198 76
        if (array_key_exists('elo', $player)) {
199 76
            $this->elo = $player['elo'];
200 76
        }
201 76
    }
202 76
203 76
    /**
204 76
     * {@inheritdoc}
205 76
     */
206 76
    protected function assignLazyResult($player)
207 76
    {
208 76
        $this->email = $player['email'];
209
        $this->verified = $player['verified'];
210 76
        $this->receives = $player['receives'];
211
        $this->confirmCode = $player['confirm_code'];
212 76
        $this->outdated = $player['outdated'];
213 76
        $this->description = $player['description'];
214
        $this->timezone = $player['timezone'];
215
        $this->joined = TimeDate::fromMysql($player['joined']);
216
        $this->last_login = TimeDate::fromMysql($player['last_login']);
217
        $this->last_match = Match::get($player['last_match']);
218
        $this->admin_notes = $player['admin_notes'];
219
        $this->ban = Ban::getBan($this->id);
220
221
        $this->cachedMatchSummary = [];
222 76
223
        // Theme user options
224 76
        if (isset($player['theme'])) {
225 1
            $this->theme = $player['theme'];
226
        } else {
227
            $themes = Service::getSiteThemes();
228 76
            $this->theme = $themes[0]['slug'];
229
        }
230
231 76
        $this->updateUserPermissions();
232 14
    }
233 14
234
    /**
235
     * Add a player a new role
236
     *
237 76
     * @param Role|int $role_id The role ID to add a player to
238 76
     *
239
     * @return bool Whether the operation was successful or not
240 76
     */
241
    public function addRole($role_id)
242
    {
243
        if ($role_id instanceof Role) {
244
            $role_id = $role_id->getId();
245
        }
246
247
        $this->lazyLoad();
248
249
        // Make sure the player doesn't already have the role
250
        foreach ($this->roles as $playerRole) {
251
            if ($playerRole->getId() == $role_id) {
252
                return false;
253
            }
254
        }
255
256
        $status = $this->modifyRole($role_id, "add");
257
        $this->refresh();
258 22
259
        return $status;
260 22
    }
261
262
    /**
263
     * Get the notes admins have left about a player
264
     * @return string The notes
265
     */
266
    public function getAdminNotes()
267
    {
268 1
        $this->lazyLoad();
269
270 1
        return $this->admin_notes;
271
    }
272
273
    /**
274
     * Get the player's BZID
275
     * @return int The BZID
276
     */
277
    public function getBZID()
278
    {
279
        return $this->bzid;
280
    }
281
282
    /**
283
     * Get the country a player belongs to
284
     *
285
     * @return Country The country belongs to
286
     */
287
    public function getCountry()
288
    {
289
        return Country::get($this->country);
290
    }
291
292
    /**
293 30
     * Get the e-mail address of the player
294
     *
295 30
     * @return string The address
296 29
     */
297
    public function getEmailAddress()
298
    {
299 30
        $this->lazyLoad();
300 29
301
        return $this->email;
302
    }
303 30
304
    /**
305
     * Build a key that we'll use for caching season Elo data in this model
306
     *
307
     * @param  string|null $season The season to get
308
     * @param  int|null    $year   The year of the season to get
309
     *
310
     * @return string
311
     */
312
    private function buildSeasonKey(&$season, &$year)
313 3
    {
314
        if ($season === null) {
315 3
            $season = Season::getCurrentSeason();
316
        }
317 3
318
        if ($year === null) {
319
            $year = Carbon::now()->year;
320
        }
321
322
        return sprintf('%s-%s', $year, $season);
323
    }
324
325
    /**
326
     * Build a key to use for caching season Elo data in this model from a timestamp
327
     *
328
     * @param DateTime $timestamp
329
     *
330
     * @return string
331
     */
332 3
    private function buildSeasonKeyFromTimestamp(\DateTime $timestamp)
333
    {
334 3
        $seasonInfo = Season::getSeason($timestamp);
335 3
336
        return sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
337
    }
338
339 3
    /**
340 2
     * Remove all Elo data for this model for matches occurring after the given match (inclusive)
341
     *
342
     * This function will not remove the Elo data for this match from the database. Ideally, this function should only
343
     * be called during Elo recalculation for this match.
344
     *
345 3
     * @internal
346
     *
347 3
     * @param Match $match
348 1
     *
349
     * @see Match::recalculateElo()
350
     */
351
    public function invalidateMatchFromCache(Match $match)
352
    {
353 2
        $seasonKey = $this->buildSeasonKeyFromTimestamp($match->getTimestamp());
354 2
        $seasonElo = null;
355
356 2
        // If we have an existing season history cached, save a reference to it for easy access. Don't create one if
357
        // nothing is cached or else it'll cause for an empty cache to be created
358
        if (isset($this->eloSeasonHistory[$seasonKey])) {
359 2
            $seasonElo = &$this->eloSeasonHistory[$seasonKey];
360
        }
361
362
        // Unset the currently cached Elo for a player so next time Player::getElo() is called, it'll pull the latest
363
        // available Elo
364
        unset($this->eloSeason[$seasonKey]);
365
366
        if ($seasonElo === null) {
367
            return;
368
        }
369 30
370
        // This function is called when we recalculate, so assume that the match will be recent, therefore towards the
371 30
        // end of the Elo history array. We splice the array to have all Elo data after this match to be removed.
372
        foreach (array_reverse($seasonElo) as $key => $match) {
373
            if ($match['match'] === $match) {
374 30
                $seasonElo = array_splice($seasonElo, $key);
375 29
                break;
376
            }
377
        }
378 30
    }
379
380
    /**
381
     * Get the Elo changes for a player for a given season
382
     *
383
     * @param  string|null $season The season to get
384
     * @param  int|null    $year   The year of the season to get
385
     *
386
     * @return array
387
     */
388
    public function getEloSeasonHistory($season = null, $year = null)
389
    {
390
        $seasonKey = $this->buildSeasonKey($season, $year);
391
392 30
        // This season's already been cached
393
        if (isset($this->eloSeasonHistory[$seasonKey])) {
394 30
            return $this->eloSeasonHistory[$seasonKey];
395 30
        }
396
397 30
        $this->eloSeasonHistory[$seasonKey] = $this->db->query('
398 30
          SELECT
399 30
            elo_new AS elo,
400
            match_id AS `match`,
401
            MONTH(matches.timestamp) AS `month`,
402 30
            YEAR(matches.timestamp) AS `year`,
403
            DAY(matches.timestamp) AS `day`
404
          FROM
405
            player_elo
406
            LEFT JOIN matches ON player_elo.match_id = matches.id
407
          WHERE
408
            user_id = ? AND season_period = ? AND season_year = ?
409
          ORDER BY
410
            match_id ASC
411
        ', [ $this->getId(), $season, $year ]);
412
413
        array_unshift($this->eloSeasonHistory[$seasonKey], [
414
            'elo' => 1200,
415 30
            'match' => null,
416
            'month' => Season::getCurrentSeasonRange($season)->getStartOfRange()->month,
417 30
            'year' => $year,
418 30
            'day' => 1
419
        ]);
420 30
421 29
        return $this->eloSeasonHistory[$seasonKey];
422
    }
423
424 30
    /**
425
     * Get the player's Elo for a season.
426 30
     *
427 30
     * With the default arguments, it will fetch the Elo for the current season.
428 30
     *
429
     * @param string|null $season The season we're looking for: winter, spring, summer, or fall
430
     * @param int|null    $year   The year of the season we're looking for
431
     *
432
     * @return int The player's Elo
433 30
     */
434
    public function getElo($season = null, $year = null)
435
    {
436
        // The Elo for this player has been forcefully set from a trusted database query, so just return that.
437
        if ($this->elo !== null) {
438
            return $this->elo;
439
        }
440
441
        $this->getEloSeasonHistory($season, $year);
442
        $seasonKey = $this->buildSeasonKey($season, $year);
443
444
        if (isset($this->eloSeason[$seasonKey])) {
445 30
            return $this->eloSeason[$seasonKey];
446
        }
447 30
448 30
        $season = &$this->eloSeasonHistory[$seasonKey];
449
450
        if (!empty($season)) {
451 30
            $elo = end($season);
452 30
            $this->eloSeason[$seasonKey] = ($elo !== false) ? $elo['elo'] : 1200;
453
        } else {
454 30
            $this->eloSeason[$seasonKey] = 1200;
455
        }
456 30
457 29
        return $this->eloSeason[$seasonKey];
458
    }
459 29
460
    /**
461 30
     * Adjust the Elo of a player for the current season based on a Match
462
     *
463
     * **Warning:** If $match is null, the Elo for the player will be modified but the value will not be persisted to
464
     * the database.
465
     *
466
     * @param int        $adjust The value to be added to the current ELO (negative to subtract)
467
     * @param Match|null $match  The match where this Elo change took place
468
     */
469
    public function adjustElo($adjust, Match $match = null)
470
    {
471
        $timestamp = ($match !== null) ? $match->getTimestamp() : (Carbon::now());
472
        $seasonInfo = Season::getSeason($timestamp);
473
474
        // Get the current Elo for the player, even if it's the default 1200. We need the value for adjusting
475
        $elo = $this->getElo($seasonInfo['season'], $seasonInfo['year']);
476
        $seasonKey = sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
477
478
        $this->eloSeason[$seasonKey] += $adjust;
479
480
        if ($match !== null && $this->isValid()) {
481
            $this->db->execute('
482
              INSERT INTO player_elo VALUES (?, ?, ?, ?, ?, ?)
483
            ', [ $this->getId(), $match->getId(), $seasonInfo['season'], $seasonInfo['year'], $elo, $this->eloSeason[$seasonKey] ]);
484
        }
485
    }
486
487
    /**
488
     * Returns whether the player has verified their e-mail address
489
     *
490
     * @return bool `true` for verified players
491
     */
492
    public function isVerified()
493
    {
494
        $this->lazyLoad();
495
496
        return $this->verified;
497
    }
498
499
    /**
500
     * Returns the confirmation code for the player's e-mail address verification
501
     *
502
     * @return string The player's confirmation code
503
     */
504
    public function getConfirmCode()
505
    {
506 1
        $this->lazyLoad();
507
508 1
        return $this->confirmCode;
509
    }
510 1
511
    /**
512 1
     * Returns what kind of events the player should be e-mailed about
513
     *
514
     * @return string The type of notifications
515
     */
516
    public function getReceives()
517
    {
518
        $this->lazyLoad();
519
520
        return $this->receives;
521
    }
522
523
    /**
524
     * Finds out whether the specified player wants and can receive an e-mail
525
     * message
526
     *
527
     * @param  string  $type
528
     * @return bool `true` if the player should be sent an e-mail
529
     */
530
    public function canReceive($type)
531
    {
532
        $this->lazyLoad();
533
534
        if (!$this->email || !$this->isVerified()) {
535
            // Unverified e-mail means the user will receive nothing
536
            return false;
537
        }
538
539
        if ($this->receives == 'everything') {
540
            return true;
541
        }
542
543
        return $this->receives == $type;
544
    }
545
546
    /**
547
     * Find out whether the specified confirmation code is correct
548
     *
549
     * This method protects against timing attacks
550
     *
551
     * @param  string $code The confirmation code to check
552
     * @return bool `true` for a correct e-mail verification code
553
     */
554
    public function isCorrectConfirmCode($code)
555
    {
556
        $this->lazyLoad();
557
558
        if ($this->confirmCode === null) {
559
            return false;
560
        }
561
562
        return StringUtils::equals($code, $this->confirmCode);
563
    }
564
565
    /**
566
     * Get the player's sanitized description
567
     * @return string The description
568
     */
569
    public function getDescription()
570
    {
571
        $this->lazyLoad();
572
573
        return $this->description;
574
    }
575
576
    /**
577
     * Get the joined date of the player
578
     * @return TimeDate The joined date of the player
579
     */
580
    public function getJoinedDate()
581
    {
582
        $this->lazyLoad();
583
584
        return $this->joined->copy();
585
    }
586
587
    /**
588
     * Get all of the known IPs used by the player
589
     *
590
     * @return string[][] An array containing IPs and hosts
591
     */
592
    public function getKnownIPs()
593
    {
594
        return $this->db->query(
595
            'SELECT DISTINCT ip, host FROM visits WHERE player = ? GROUP BY ip, host ORDER BY MAX(timestamp) DESC LIMIT 10',
596
            array($this->getId())
597
        );
598
    }
599
600
    /**
601
     * Get the last login for a player
602
     * @return TimeDate The date of the last login
603
     */
604
    public function getLastLogin()
605
    {
606
        $this->lazyLoad();
607
608
        return $this->last_login->copy();
609
    }
610
611 23
    /**
612
     * Get the last match
613 23
     * @return Match
614
     */
615
    public function getLastMatch()
616
    {
617
        $this->lazyLoad();
618
619
        return $this->last_match;
620 1
    }
621
622 1
    /**
623
     * Get all of the callsigns a player has used to log in to the website
624 1
     * @return string[] An array containing all of the past callsigns recorded for a player
625
     */
626
    public function getPastCallsigns()
627
    {
628
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
629
    }
630
631
    /**
632
     * Get the player's team
633
     * @return Team The object representing the team
634
     */
635
    public function getTeam()
636
    {
637
        return Team::get($this->team);
638
    }
639
640
    /**
641 76
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
642
     * @return string The timezone
643 76
     */
644 76
    public function getTimezone()
645
    {
646 76
        $this->lazyLoad();
647 76
648
        return ($this->timezone) ?: date_default_timezone_get();
649 76
    }
650
651
    /**
652
     * Get the roles of the player
653
     * @return Role[]
654
     */
655
    public function getRoles()
656
    {
657
        $this->lazyLoad();
658 2
659
        return $this->roles;
660 2
    }
661 1
662
    /**
663
     * Rebuild the list of permissions a user has been granted
664 2
     */
665
    private function updateUserPermissions()
666 2
    {
667
        $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...
668
        $this->permissions = array();
669
670
        foreach ($this->roles as $role) {
671
            $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...
672
        }
673
    }
674
675
    /**
676
     * Check if a player has a specific permission
677
     *
678
     * @param string|null $permission The permission to check for
679
     *
680
     * @return bool Whether or not the player has the permission
681
     */
682
    public function hasPermission($permission)
683
    {
684
        if ($permission === null) {
685
            return false;
686
        }
687
688
        $this->lazyLoad();
689
690
        return isset($this->permissions[$permission]);
691
    }
692
693
    /**
694
     * Check whether or not a player been in a match or has logged on in the specified amount of time to be considered
695
     * active
696
     *
697
     * @return bool True if the player has been active
698
     */
699
    public function hasBeenActive()
700
    {
701
        $this->lazyLoad();
702
703
        $interval  = Service::getParameter('bzion.miscellaneous.active_interval');
704
        $lastLogin = $this->last_login->copy()->modify($interval);
705
706
        $hasBeenActive = (TimeDate::now() <= $lastLogin);
707
708
        if ($this->last_match->isValid()) {
709
            $lastMatch = $this->last_match->getTimestamp()->copy()->modify($interval);
710
            $hasBeenActive = ($hasBeenActive || TimeDate::now() <= $lastMatch);
711
        }
712
713
        return $hasBeenActive;
714
    }
715
716
    /**
717
     * Check whether the callsign of the player is outdated
718
     *
719
     * Returns true if this player has probably changed their callsign, making
720
     * the current username stored in the database obsolete
721
     *
722 1
     * @return bool Whether or not the player is disabled
723
     */
724 1
    public function isOutdated()
725
    {
726
        $this->lazyLoad();
727
728
        return $this->outdated;
729
    }
730
731
    /**
732 55
     * Check if a player's account has been disabled
733
     *
734 55
     * @return bool Whether or not the player is disabled
735
     */
736
    public function isDisabled()
737
    {
738
        return $this->status == "disabled";
739
    }
740 1
741
    /**
742 1
     * Check if everyone can log in as this user on a test environment
743
     *
744
     * @return bool
745
     */
746 1
    public function isTestUser()
747
    {
748
        return $this->status == "test";
749
    }
750
751
    /**
752
     * Check if a player is teamless
753
     *
754
     * @return bool True if the player is teamless
755
     */
756
    public function isTeamless()
757
    {
758
        return empty($this->team);
759
    }
760
761
    /**
762
     * Mark a player's account as banned
763
     */
764
    public function markAsBanned()
765
    {
766 2
        if ($this->status != 'active') {
767
            return $this;
768 2
        }
769
770
        return $this->updateProperty($this->status, "status", "banned");
771
    }
772
773
    /**
774
     * Mark a player's account as unbanned
775
     */
776
    public function markAsUnbanned()
777
    {
778
        if ($this->status != 'banned') {
779
            return $this;
780
        }
781
782
        return $this->updateProperty($this->status, "status", "active");
783
    }
784
785
    /**
786
     * Find out if a player is banned
787
     *
788
     * @return bool
789
     */
790
    public function isBanned()
791
    {
792
        return Ban::getBan($this->id) !== null;
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
        $this->updateProperty($this->theme, 'theme', $theme);
1007
    }
1008
1009
    /**
1010
     * Updates this player's last login
1011
     */
1012
    public function updateLastLogin()
1013
    {
1014
        $this->update("last_login", TimeDate::now()->toMysql());
1015
    }
1016
1017
    /**
1018
     * Get the player's username
1019
     * @return string The username
1020
     */
1021
    public function getUsername()
1022
    {
1023
        return $this->name;
1024
    }
1025
1026
    /**
1027
     * Get the player's username, safe for use in your HTML
1028
     * @return string The username
1029
     */
1030
    public function getEscapedUsername()
1031
    {
1032
        return $this->getEscapedName();
1033
    }
1034
1035
    /**
1036
     * Alias for Player::setUsername()
1037
     *
1038
     * @param  string $username The new username
1039
     * @return self
1040
     */
1041
    public function setName($username)
1042
    {
1043
        return $this->setUsername($username);
1044 76
    }
1045
1046 76
    /**
1047
     * Mark all the unread messages of a player as read
1048 76
     *
1049 76
     * @return void
1050 76
     */
1051
    public function markMessagesAsRead()
1052
    {
1053
        $this->db->execute(
1054
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
1055
            array($this->id)
1056
        );
1057 76
    }
1058
1059
    /**
1060
     * Set the roles of a user
1061
     *
1062
     * @todo   Is it worth making this faster?
1063
     * @param  Role[] $roles The new roles of the user
1064
     * @return self
1065
     */
1066
    public function setRoles($roles)
1067
    {
1068
        $this->lazyLoad();
1069 23
1070
        $oldRoles = Role::mapToIds($this->roles);
1071 23
        $this->roles = $roles;
1072
        $roleIds = Role::mapToIds($roles);
1073
1074
        $newRoles     = array_diff($roleIds, $oldRoles);
1075
        $removedRoles = array_diff($oldRoles, $roleIds);
1076
1077
        foreach ($newRoles as $role) {
1078
            $this->modifyRole($role, 'add');
1079
        }
1080 1
1081
        foreach ($removedRoles as $role) {
1082 1
            $this->modifyRole($role, 'remove');
1083
        }
1084 1
1085
        $this->refresh();
1086
1087
        return $this;
1088
    }
1089
1090
    /**
1091
     * Give or remove a role to/form a player
1092
     *
1093
     * @param int    $role_id The role ID to add or remove
1094
     * @param string $action  Whether to "add" or "remove" a role for a player
1095
     *
1096
     * @return bool Whether the operation was successful or not
1097
     */
1098
    private function modifyRole($role_id, $action)
1099
    {
1100
        $role = Role::get($role_id);
1101
1102 1
        if ($role->isValid()) {
1103
            if ($action == "add") {
1104 1
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
1105
            } elseif ($action == "remove") {
1106
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
1107
            } else {
1108
                throw new Exception("Unrecognized role action");
1109
            }
1110
1111
            return true;
1112
        }
1113
1114
        return false;
1115
    }
1116
1117
    /**
1118
     * Given a player's BZID, get a player object
1119
     *
1120
     * @param  int    $bzid The player's BZID
1121
     * @return Player
1122
     */
1123
    public static function getFromBZID($bzid)
1124
    {
1125
        return self::get(self::fetchIdFrom($bzid, "bzid"));
1126
    }
1127
1128
    /**
1129
     * Get a single player by their username
1130
     *
1131
     * @param  string $username The username to look for
1132
     * @return Player
1133
     */
1134
    public static function getFromUsername($username)
1135
    {
1136
        $player = static::get(self::fetchIdFrom($username, 'username'));
1137
1138
        return $player->inject('name', $username);
1139
    }
1140
1141
    /**
1142
     * Get all the players in the database that have an active status
1143
     * @return Player[] An array of player BZIDs
1144
     */
1145
    public static function getPlayers()
1146
    {
1147
        return self::arrayIdToModel(
1148
            self::fetchIdsFrom("status", array("active", "test"), false)
1149
        );
1150
    }
1151
1152
    /**
1153
     * Show the number of notifications the user hasn't read yet
1154
     * @return int
1155
     */
1156
    public function countUnreadNotifications()
1157
    {
1158
        return Notification::countUnreadNotifications($this->id);
1159
    }
1160
1161
    /**
1162
     * Count the number of matches a player has participated in
1163
     * @return int
1164
     */
1165
    public function getMatchCount()
1166
    {
1167
        if ($this->cachedMatchCount === null) {
1168
            $this->cachedMatchCount = Match::getQueryBuilder()
1169
                ->active()
1170
                ->with($this)
1171
                ->count();
1172
        }
1173
1174
        return $this->cachedMatchCount;
1175
    }
1176
1177
    /**
1178
     * Get the (victory/total matches) ratio of the player
1179
     * @return float
1180
     */
1181
    public function getMatchWinRatio()
1182
    {
1183
        $count = $this->getMatchCount();
1184
1185
        if ($count == 0) {
1186
            return 0;
1187
        }
1188
1189
        $wins = Match::getQueryBuilder()
1190
            ->active()
1191
            ->with($this, 'win')
1192
            ->count();
1193
1194
        return $wins / $count;
1195
    }
1196
1197
    /**
1198
     * Get the (total caps made by team/total matches) ratio of the player
1199
     * @return float
1200
     */
1201
    public function getMatchAverageCaps()
1202
    {
1203
        $count = $this->getMatchCount();
1204
1205
        if ($count == 0) {
1206
            return 0;
1207
        }
1208
1209
        // Get the sum of team A points if the player was in team A, team B points if the player was in team B
1210
        $query = $this->db->query("
1211
            SELECT
1212
              SUM(
1213
                IF(mp.team_loyalty = 0, team_a_points, team_b_points)
1214
              ) AS sum
1215
            FROM
1216
              matches
1217
            INNER JOIN
1218
              match_participation mp ON mp.match_id = matches.id
1219
            WHERE
1220
              status = 'entered' AND mp.user_id = ?
1221
        ", [$this->id]);
1222
1223
        return $query[0]['sum'] / $count;
1224
    }
1225
1226
    /**
1227
     * Get the match activity in matches per day for a player
1228
     *
1229 1
     * @return float
1230
     */
1231 1
    public function getMatchActivity()
1232 1
    {
1233
        if ($this->matchActivity !== null) {
1234
            return $this->matchActivity;
1235
        }
1236
1237
        $activity = 0.0;
1238
1239
        $matches = Match::getQueryBuilder()
1240
            ->active()
1241 2
            ->with($this)
1242
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
1243 2
            ->getModels($fast = true);
1244 2
1245
        foreach ($matches as $match) {
1246
            $activity += $match->getActivity();
1247
        }
1248
1249
        return $activity;
1250
    }
1251 1
1252
    /**
1253 1
     * Return an array of matches this player participated in per month.
1254
     *
1255
     * ```
1256
     * ['yyyy-mm'] = <number of matches>
1257
     * ```
1258
     *
1259 76
     * @param TimeDate|string $timePeriod
1260
     *
1261
     * @return int[]
1262 76
     */
1263
    public function getMatchSummary($timePeriod = '1 year ago')
1264
    {
1265
        $since = ($timePeriod instanceof TimeDate) ? $timePeriod : TimeDate::from($timePeriod);
1266
1267
        if (!isset($this->cachedMatchSummary[(string)$timePeriod])) {
1268
            $this->cachedMatchSummary[(string)$timePeriod] = Match::getQueryBuilder()
1269
                ->active()
1270
                ->with($this)
1271
                ->where('time')->isAfter($since)
1272 76
                ->getSummary($since)
1273
            ;
1274
        }
1275
1276
        return $this->cachedMatchSummary[(string)$timePeriod];
1277
    }
1278 76
1279
    /**
1280
     * Show the number of messages the user hasn't read yet
1281 76
     * @return int
1282
     */
1283
    public function countUnreadMessages()
1284
    {
1285
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
1286
            $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...
1287
        );
1288
    }
1289
1290
    /**
1291
     * Get all of the members belonging to a team
1292
     * @param  int      $teamID The ID of the team to fetch the members of
1293
     * @return Player[] An array of Player objects of the team members
1294 76
     */
1295
    public static function getTeamMembers($teamID)
1296
    {
1297
        return self::arrayIdToModel(
1298
            self::fetchIds("WHERE team = ?", array($teamID))
1299
        );
1300
    }
1301
1302
    /**
1303
     * {@inheritdoc}
1304
     */
1305
    public static function getActiveStatuses()
1306
    {
1307
        return array('active', 'reported', 'test');
1308
    }
1309
1310
    /**
1311
     * {@inheritdoc}
1312
     */
1313
    public static function getEagerColumns($prefix = null)
1314
    {
1315
        $columns = [
1316
            'id',
1317
            'bzid',
1318
            'team',
1319
            'username',
1320
            'alias',
1321
            'status',
1322
            'avatar',
1323
            'country',
1324
        ];
1325
1326
        return self::formatColumns($prefix, $columns);
1327
    }
1328
1329 76
    /**
1330
     * {@inheritdoc}
1331 76
     */
1332 76
    public static function getLazyColumns($prefix = null)
1333 76
    {
1334
        $columns = [
1335 76
            'email',
1336 76
            'verified',
1337 76
            'receives',
1338 76
            'confirm_code',
1339 76
            'outdated',
1340 76
            'description',
1341 76
            'theme',
1342 76
            'timezone',
1343 76
            'joined',
1344 76
            'last_login',
1345 76
            'last_match',
1346 76
            'admin_notes',
1347
        ];
1348
1349 76
        return self::formatColumns($prefix, $columns);
1350 76
    }
1351 76
1352
    /**
1353 76
     * Get a query builder for players
1354
     * @return PlayerQueryBuilder
1355
     */
1356
    public static function getQueryBuilder()
1357
    {
1358
        return new PlayerQueryBuilder('Player', array(
1359
            'columns' => array(
1360
                'name'     => 'username',
1361
                'team'     => 'team',
1362
                'outdated' => 'outdated',
1363
                'status'   => 'status',
1364
            ),
1365
            'name' => 'name',
1366
        ));
1367
    }
1368
1369
    /**
1370
     * Enter a new player to the database
1371
     * @param  int              $bzid        The player's bzid
1372
     * @param  string           $username    The player's username
1373 76
     * @param  int              $team        The player's team
1374
     * @param  string           $status      The player's status
1375
     * @param  int              $role_id     The player's role when they are first created
1376
     * @param  string           $avatar      The player's profile avatar
1377 76
     * @param  string           $description The player's profile description
1378
     * @param  int              $country     The player's country
1379
     * @param  string           $timezone    The player's timezone
1380 76
     * @param  string|\TimeDate $joined      The date the player joined
1381
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1382 76
     * @return Player           An object representing the player that was just entered
1383
     */
1384 76
    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")
1385
    {
1386
        $joined = TimeDate::from($joined);
1387
        $last_login = TimeDate::from($last_login);
1388
        $timezone = ($timezone) ?: date_default_timezone_get();
1389
1390
        $player = self::create(array(
1391
            'bzid'        => $bzid,
1392
            'team'        => $team,
1393
            'username'    => $username,
1394
            'alias'       => self::generateAlias($username),
1395
            'status'      => $status,
1396
            'avatar'      => $avatar,
1397
            'description' => $description,
1398
            'country'     => $country,
1399
            'timezone'    => $timezone,
1400
            'joined'      => $joined->toMysql(),
1401
            'last_login'  => $last_login->toMysql(),
1402
        ));
1403
1404
        $player->addRole($role_id);
1405
        $player->getIdenticon($player->getId());
1406
        $player->setUsername($username);
1407
1408
        return $player;
1409
    }
1410
1411 1
    /**
1412 1
     * Determine if a player exists in the database
1413 1
     * @param  int  $bzid The player's bzid
1414
     * @return bool Whether the player exists in the database
1415
     */
1416
    public static function playerBZIDExists($bzid)
1417
    {
1418
        return self::getFromBZID($bzid)->isValid();
1419
    }
1420 76
1421
    /**
1422 76
     * Change a player's callsign and add it to the database if it does not
1423
     * exist as a past callsign
1424 76
     *
1425 76
     * @param  string $username The new username of the player
1426
     * @return self
1427
     */
1428
    public function setUsername($username)
1429
    {
1430
        // The player's username was just fetched from BzDB, it's definitely not
1431
        // outdated
1432
        $this->setOutdated(false);
1433
1434 1
        // Players who have this player's username are considered outdated
1435
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1436 1
1437
        if ($username === $this->name) {
1438
            // The player's username hasn't changed, no need to do anything
1439
            return $this;
1440
        }
1441
1442
        // Players who used to have our player's username are not outdated anymore,
1443
        // unless they are more than one.
1444
        // Even though we are sure that the old and new usernames are not equal,
1445
        // MySQL makes a different type of string equality tests, which is why we
1446
        // also check IDs to make sure not to affect our own player's outdatedness.
1447 1
        $this->db->execute("
1448
            UPDATE {$this->table} SET outdated =
1449 1
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1450
            WHERE username = ? AND id != ?",
1451
            array($this->name, $this->id, $this->name, $this->id));
1452 1
1453
        $this->updateProperty($this->name, 'username', $username);
1454
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1455
        $this->resetAlias();
1456
1457
        return $this;
1458
    }
1459
1460
    /**
1461
     * Alphabetical order function for use in usort (case-insensitive)
1462 1
     * @return Closure The sort function
1463
     */
1464 1
    public static function getAlphabeticalSort()
1465
    {
1466
        return function (Player $a, Player $b) {
1467
            return strcasecmp($a->getUsername(), $b->getUsername());
1468
        };
1469
    }
1470
1471
    /**
1472
     * {@inheritdoc}
1473 1
     * @todo Add a constraint that does this automatically
1474
     */
1475 1
    public function wipe()
1476
    {
1477
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1478
1479
        parent::wipe();
1480
    }
1481
1482
    /**
1483
     * Find whether the player can delete a model
1484
     *
1485
     * @param  PermissionModel $model       The model that will be seen
1486
     * @param  bool         $showDeleted Whether to show deleted models to admins
1487
     * @return bool
1488
     */
1489
    public function canSee($model, $showDeleted = false)
1490
    {
1491
        return $model->canBeSeenBy($this, $showDeleted);
1492
    }
1493
1494
    /**
1495
     * Find whether the player can delete a model
1496
     *
1497
     * @param  PermissionModel $model The model that will be deleted
1498
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1499
     *                                to soft-delete ones
1500
     * @return bool
1501
     */
1502
    public function canDelete($model, $hard = false)
1503
    {
1504
        if ($hard) {
1505
            return $model->canBeHardDeletedBy($this);
1506
        } else {
1507
            return $model->canBeSoftDeletedBy($this);
1508
        }
1509
    }
1510
1511
    /**
1512
     * Find whether the player can create a model
1513
     *
1514
     * @param  string  $modelName The PHP class identifier of the model type
1515
     * @return bool
1516
     */
1517
    public function canCreate($modelName)
1518
    {
1519
        return $modelName::canBeCreatedBy($this);
1520
    }
1521
1522
    /**
1523
     * Find whether the player can edit a model
1524
     *
1525
     * @param  PermissionModel $model The model which will be edited
1526
     * @return bool
1527
     */
1528
    public function canEdit($model)
1529
    {
1530
        return $model->canBeEditedBy($this);
1531
    }
1532
}
1533