Completed
Push — feature/player-elo ( 97e749...21d941 )
by Vladimir
04:51
created

Player::buildSeasonKey()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 4
nop 2
crap 3
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 status
47
     * @var string
48
     */
49
    protected $status;
50
51
    /**
52
     * The player's e-mail address
53
     * @var string
54
     */
55
    protected $email;
56
57
    /**
58
     * Whether the player has verified their e-mail address
59
     * @var bool
60
     */
61
    protected $verified;
62
63
    /**
64
     * What kind of events the player should be e-mailed about
65
     * @var string
66
     */
67
    protected $receives;
68
69
    /**
70
     * A confirmation code for the player's e-mail address verification
71
     * @var string
72
     */
73
    protected $confirmCode;
74
75
    /**
76
     * Whether the callsign of the player is outdated
77
     * @var bool
78
     */
79
    protected $outdated;
80
81
    /**
82
     * The player's profile description
83
     * @var string
84
     */
85
    protected $description;
86
87
    /**
88
     * The id of the player's country
89
     * @var int
90
     */
91
    protected $country;
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
    private $eloSeason;
156
    private $eloSeasonHistory;
157
158
    private $matchActivity;
159
160
    /**
161
     * The name of the database table used for queries
162
     */
163
    const TABLE = "players";
164
165
    /**
166
     * The location where avatars will be stored
167
     */
168
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
169
170
    const EDIT_PERMISSION = Permission::EDIT_USER;
171
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
172
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 72
    protected function assignResult($player)
178
    {
179 72
        $this->bzid = $player['bzid'];
180 72
        $this->name = $player['username'];
181 72
        $this->alias = $player['alias'];
182 72
        $this->team = $player['team'];
183 72
        $this->status = $player['status'];
184 72
        $this->avatar = $player['avatar'];
185 72
        $this->country = $player['country'];
186
187 72
        if (key_exists('activity', $player)) {
188
            $this->matchActivity = ($player['activity'] != null) ? $player['activity'] : 0.0;
189
        }
190 72
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 72
    protected function assignLazyResult($player)
196
    {
197 72
        $this->email = $player['email'];
198 72
        $this->verified = $player['verified'];
199 72
        $this->receives = $player['receives'];
200 72
        $this->confirmCode = $player['confirm_code'];
201 72
        $this->outdated = $player['outdated'];
202 72
        $this->description = $player['description'];
203 72
        $this->timezone = $player['timezone'];
204 72
        $this->joined = TimeDate::fromMysql($player['joined']);
205 72
        $this->last_login = TimeDate::fromMysql($player['last_login']);
206 72
        $this->last_match = Match::get($player['last_match']);
207 72
        $this->admin_notes = $player['admin_notes'];
208 72
        $this->ban = Ban::getBan($this->id);
209
210 72
        $this->cachedMatchSummary = [];
211
212 72
        $this->updateUserPermissions();
213 72
    }
214
215
    /**
216
     * Add a player a new role
217
     *
218
     * @param Role|int $role_id The role ID to add a player to
219
     *
220
     * @return bool Whether the operation was successful or not
221
     */
222 72
    public function addRole($role_id)
223
    {
224 72
        if ($role_id instanceof Role) {
225 1
            $role_id = $role_id->getId();
226
        }
227
228 72
        $this->lazyLoad();
229
230
        // Make sure the player doesn't already have the role
231 72
        foreach ($this->roles as $playerRole) {
232 14
            if ($playerRole->getId() == $role_id) {
233 14
                return false;
234
            }
235
        }
236
237 72
        $status = $this->modifyRole($role_id, "add");
238 72
        $this->refresh();
239
240 72
        return $status;
241
    }
242
243
    /**
244
     * Get the notes admins have left about a player
245
     * @return string The notes
246
     */
247
    public function getAdminNotes()
248
    {
249
        $this->lazyLoad();
250
251
        return $this->admin_notes;
252
    }
253
254
    /**
255
     * Get the player's BZID
256
     * @return int The BZID
257
     */
258 22
    public function getBZID()
259
    {
260 22
        return $this->bzid;
261
    }
262
263
    /**
264
     * Get the country a player belongs to
265
     *
266
     * @return Country The country belongs to
267
     */
268 1
    public function getCountry()
269
    {
270 1
        return Country::get($this->country);
271
    }
272
273
    /**
274
     * Get the e-mail address of the player
275
     *
276
     * @return string The address
277
     */
278
    public function getEmailAddress()
279
    {
280
        $this->lazyLoad();
281
282
        return $this->email;
283
    }
284
285
    /**
286
     * Build a key that we'll use for caching season Elo data in this model
287
     *
288
     * @param  string|null $season The season to get
289
     * @param  int|null    $year   The year of the season to get
290
     *
291
     * @return string
292
     */
293 29
    private function buildSeasonKey(&$season, &$year)
294
    {
295 29
        if ($season === null) {
296 28
            $season = Season::getCurrentSeason();
297
        }
298
299 29
        if ($year === null) {
300 28
            $year = Carbon::now()->year;
301
        }
302
303 29
        return sprintf('%s-%s', $year, $season);
304
    }
305
306
    /**
307
     * Build a key to use for caching season Elo data in this model from a timestamp
308
     *
309
     * @param DateTime $timestamp
310
     *
311
     * @return string
312
     */
313 2
    private function buildSeasonKeyFromTimestamp(\DateTime $timestamp)
314
    {
315 2
        $seasonInfo = Season::getSeason($timestamp);
316
317 2
        return sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
318
    }
319
320
    /**
321
     * Remove all Elo data for this model for matches occurring after the given match (inclusive)
322
     *
323
     * This function will not remove the Elo data for this match from the database. Ideally, this function should only
324
     * be called during Elo recalculation for this match.
325
     *
326
     * @internal
327
     *
328
     * @param Match $match
329
     *
330
     * @see Match::recalculateElo()
331
     */
332 2
    public function invalidateMatchFromCache(Match $match)
333
    {
334 2
        $seasonKey = $this->buildSeasonKeyFromTimestamp($match->getTimestamp());
335 2
        $seasonElo = &$this->eloSeasonHistory[$seasonKey];
336
337
        // Unset the currently cached Elo for a player so next time Player::getElo() is called, it'll pull the latest
338
        // available Elo
339 2
        unset($this->eloSeason[$seasonKey]);
340
341 2
        if ($seasonElo === null) {
342 1
            return;
343
        }
344
345
        // This function is called when we recalculate, so assume that the match will be recent, therefore towards the
346
        // end of the Elo history array. We splice the array to have all Elo data after this match to be removed.
347 1
        foreach (array_reverse($seasonElo) as $key => $match) {
348
            if ($match['match'] === $match) {
349
                $seasonElo = array_splice($seasonElo, $key);
350
                break;
351
            }
352
        }
353 1
    }
354
355
    /**
356
     * Get the Elo changes for a player for a given season
357
     *
358
     * @param  string|null $season The season to get
359
     * @param  int|null    $year   The year of the season to get
360
     *
361
     * @return array
362
     */
363 29
    public function getEloSeasonHistory($season = null, $year = null)
364
    {
365 29
        $seasonKey = $this->buildSeasonKey($season, $year);
366
367
        // This season's already been cached
368 29
        if ($this->eloSeasonHistory !== null && array_key_exists($seasonKey, $this->eloSeasonHistory)) {
369 28
            return $this->eloSeasonHistory[$seasonKey];
370
        }
371
372 29
        $this->eloSeasonHistory[$seasonKey] = $this->db->query('
373
          SELECT
374
            elo_new AS elo,
375
            match_id AS `match`,
376
            MONTH(matches.timestamp) AS `month`,
377
            YEAR(matches.timestamp) AS `year`,
378
            DAY(matches.timestamp) AS `day`
379
          FROM
380
            player_elo
381
            LEFT JOIN matches ON player_elo.match_id = matches.id
382
          WHERE
383
            user_id = ? AND season_period = ? AND season_year = ?
384
          ORDER BY
385
            match_id ASC
386 29
        ', [ $this->getId(), $season, $year ]);
387
388 29
        return $this->eloSeasonHistory[$seasonKey];
389
    }
390
391
    /**
392
     * Get the player's Elo for a season.
393
     *
394
     * With the default arguments, it will fetch the Elo for the current season.
395
     *
396
     * @param string|null $season The season we're looking for: winter, spring, summer, or fall
397
     * @param int|null    $year   The year of the season we're looking for
398
     *
399
     * @return int The player's Elo
400
     */
401 29
    public function getElo($season = null, $year = null)
402
    {
403 29
        $this->getEloSeasonHistory($season, $year);
404 29
        $seasonKey = $this->buildSeasonKey($season, $year);
405
406 29
        if (isset($this->eloSeason[$seasonKey])) {
407 28
            return $this->eloSeason[$seasonKey];
408
        }
409
410 29
        $season = &$this->eloSeasonHistory[$seasonKey];
411
412 29
        if (!empty($season)) {
413
            $elo = end($season);
414
            $this->eloSeason[$seasonKey] = ($elo !== false) ? $elo['elo'] : 1200;
415
        } else {
416 29
            $this->eloSeason[$seasonKey] = 1200;
417
        }
418
419 29
        return $this->eloSeason[$seasonKey];
420
    }
421
422
    /**
423
     * Adjust the Elo of a player for the current season based on a Match
424
     *
425
     * **Warning:** If $match is null, the Elo for the player will be modified but the value will not be persisted to
426
     * the database.
427
     *
428
     * @param int        $adjust The value to be added to the current ELO (negative to subtract)
429
     * @param Match|null $match  The match where this Elo change took place
430
     */
431 29
    public function adjustElo($adjust, Match $match = null)
432
    {
433 29
        $timestamp = ($match !== null) ? $match->getTimestamp() : (Carbon::now());
434 29
        $seasonInfo = Season::getSeason($timestamp);
435
436
        // Get the current Elo for the player, even if it's the default 1200. We need the value for adjusting
437 29
        $elo = $this->getElo($seasonInfo['season'], $seasonInfo['year']);
438 29
        $seasonKey = sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
439
440 29
        $this->eloSeason[$seasonKey] += $adjust;
441
442 29
        if ($match !== null && $this->isValid()) {
443 28
            $this->db->execute('
444
              INSERT INTO player_elo VALUES (?, ?, ?, ?, ?, ?)
445 28
            ', [ $this->getId(), $match->getId(), $seasonInfo['season'], $seasonInfo['year'], $elo, $this->eloSeason[$seasonKey] ]);
446
        }
447 29
    }
448
449
    /**
450
     * Returns whether the player has verified their e-mail address
451
     *
452
     * @return bool `true` for verified players
453
     */
454
    public function isVerified()
455
    {
456
        $this->lazyLoad();
457
458
        return $this->verified;
459
    }
460
461
    /**
462
     * Returns the confirmation code for the player's e-mail address verification
463
     *
464
     * @return string The player's confirmation code
465
     */
466
    public function getConfirmCode()
467
    {
468
        $this->lazyLoad();
469
470
        return $this->confirmCode;
471
    }
472
473
    /**
474
     * Returns what kind of events the player should be e-mailed about
475
     *
476
     * @return string The type of notifications
477
     */
478
    public function getReceives()
479
    {
480
        $this->lazyLoad();
481
482
        return $this->receives;
483
    }
484
485
    /**
486
     * Finds out whether the specified player wants and can receive an e-mail
487
     * message
488
     *
489
     * @param  string  $type
490
     * @return bool `true` if the player should be sent an e-mail
491
     */
492 1
    public function canReceive($type)
493
    {
494 1
        $this->lazyLoad();
495
496 1
        if (!$this->email || !$this->isVerified()) {
497
            // Unverified e-mail means the user will receive nothing
498 1
            return false;
499
        }
500
501
        if ($this->receives == 'everything') {
502
            return true;
503
        }
504
505
        return $this->receives == $type;
506
    }
507
508
    /**
509
     * Find out whether the specified confirmation code is correct
510
     *
511
     * This method protects against timing attacks
512
     *
513
     * @param  string $code The confirmation code to check
514
     * @return bool `true` for a correct e-mail verification code
515
     */
516
    public function isCorrectConfirmCode($code)
517
    {
518
        $this->lazyLoad();
519
520
        if ($this->confirmCode === null) {
521
            return false;
522
        }
523
524
        return StringUtils::equals($code, $this->confirmCode);
525
    }
526
527
    /**
528
     * Get the player's sanitized description
529
     * @return string The description
530
     */
531
    public function getDescription()
532
    {
533
        $this->lazyLoad();
534
535
        return $this->description;
536
    }
537
538
    /**
539
     * Get the joined date of the player
540
     * @return TimeDate The joined date of the player
541
     */
542
    public function getJoinedDate()
543
    {
544
        $this->lazyLoad();
545
546
        return $this->joined->copy();
547
    }
548
549
    /**
550
     * Get all of the known IPs used by the player
551
     *
552
     * @return string[][] An array containing IPs and hosts
553
     */
554
    public function getKnownIPs()
555
    {
556
        return $this->db->query(
557
            'SELECT DISTINCT ip, host FROM visits WHERE player = ? GROUP BY ip, host ORDER BY MAX(timestamp) DESC LIMIT 10',
558
            array($this->getId())
559
        );
560
    }
561
562
    /**
563
     * Get the last login for a player
564
     * @return TimeDate The date of the last login
565
     */
566
    public function getLastLogin()
567
    {
568
        $this->lazyLoad();
569
570
        return $this->last_login->copy();
571
    }
572
573
    /**
574
     * Get the last match
575
     * @return Match
576
     */
577
    public function getLastMatch()
578
    {
579
        $this->lazyLoad();
580
581
        return $this->last_match;
582
    }
583
584
    /**
585
     * Get all of the callsigns a player has used to log in to the website
586
     * @return string[] An array containing all of the past callsigns recorded for a player
587
     */
588
    public function getPastCallsigns()
589
    {
590
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
591
    }
592
593
    /**
594
     * Get the player's team
595
     * @return Team The object representing the team
596
     */
597 23
    public function getTeam()
598
    {
599 23
        return Team::get($this->team);
600
    }
601
602
    /**
603
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
604
     * @return string The timezone
605
     */
606 1
    public function getTimezone()
607
    {
608 1
        $this->lazyLoad();
609
610 1
        return ($this->timezone) ?: date_default_timezone_get();
611
    }
612
613
    /**
614
     * Get the roles of the player
615
     * @return Role[]
616
     */
617
    public function getRoles()
618
    {
619
        $this->lazyLoad();
620
621
        return $this->roles;
622
    }
623
624
    /**
625
     * Rebuild the list of permissions a user has been granted
626
     */
627 72
    private function updateUserPermissions()
628
    {
629 72
        $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...
630 72
        $this->permissions = array();
631
632 72
        foreach ($this->roles as $role) {
633 72
            $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...
634
        }
635 72
    }
636
637
    /**
638
     * Check if a player has a specific permission
639
     *
640
     * @param string|null $permission The permission to check for
641
     *
642
     * @return bool Whether or not the player has the permission
643
     */
644 2
    public function hasPermission($permission)
645
    {
646 2
        if ($permission === null) {
647 1
            return false;
648
        }
649
650 2
        $this->lazyLoad();
651
652 2
        return isset($this->permissions[$permission]);
653
    }
654
655
    /**
656
     * Check whether or not a player been in a match or has logged on in the specified amount of time to be considered
657
     * active
658
     *
659
     * @return bool True if the player has been active
660
     */
661
    public function hasBeenActive()
662
    {
663
        $this->lazyLoad();
664
665
        $interval  = Service::getParameter('bzion.miscellaneous.active_interval');
666
        $lastLogin = $this->last_login->copy()->modify($interval);
667
668
        $hasBeenActive = (TimeDate::now() <= $lastLogin);
669
670
        if ($this->last_match->isValid()) {
671
            $lastMatch = $this->last_match->getTimestamp()->copy()->modify($interval);
672
            $hasBeenActive = ($hasBeenActive || TimeDate::now() <= $lastMatch);
673
        }
674
675
        return $hasBeenActive;
676
    }
677
678
    /**
679
     * Check whether the callsign of the player is outdated
680
     *
681
     * Returns true if this player has probably changed their callsign, making
682
     * the current username stored in the database obsolete
683
     *
684
     * @return bool Whether or not the player is disabled
685
     */
686
    public function isOutdated()
687
    {
688
        $this->lazyLoad();
689
690
        return $this->outdated;
691
    }
692
693
    /**
694
     * Check if a player's account has been disabled
695
     *
696
     * @return bool Whether or not the player is disabled
697
     */
698
    public function isDisabled()
699
    {
700
        return $this->status == "disabled";
701
    }
702
703
    /**
704
     * Check if everyone can log in as this user on a test environment
705
     *
706
     * @return bool
707
     */
708 1
    public function isTestUser()
709
    {
710 1
        return $this->status == "test";
711
    }
712
713
    /**
714
     * Check if a player is teamless
715
     *
716
     * @return bool True if the player is teamless
717
     */
718 51
    public function isTeamless()
719
    {
720 51
        return empty($this->team);
721
    }
722
723
    /**
724
     * Mark a player's account as banned
725
     */
726 1
    public function markAsBanned()
727
    {
728 1
        if ($this->status != 'active') {
729
            return $this;
730
        }
731
732 1
        return $this->updateProperty($this->status, "status", "banned");
733
    }
734
735
    /**
736
     * Mark a player's account as unbanned
737
     */
738
    public function markAsUnbanned()
739
    {
740
        if ($this->status != 'banned') {
741
            return $this;
742
        }
743
744
        return $this->updateProperty($this->status, "status", "active");
745
    }
746
747
    /**
748
     * Find out if a player is banned
749
     *
750
     * @return bool
751
     */
752 2
    public function isBanned()
753
    {
754 2
        return Ban::getBan($this->id) !== null;
755
    }
756
757
    /**
758
     * Get the ban of the player
759
     *
760
     * This method performs a load of all the lazy parameters of the Player
761
     *
762
     * @return Ban|null The current ban of the player, or null if the player is
763
     *                  is not banned
764
     */
765
    public function getBan()
766
    {
767
        $this->lazyLoad();
768
769
        return $this->ban;
770
    }
771
772
    /**
773
     * Remove a player from a role
774
     *
775
     * @param int $role_id The role ID to add or remove
776
     *
777
     * @return bool Whether the operation was successful or not
778
     */
779
    public function removeRole($role_id)
780
    {
781
        $status = $this->modifyRole($role_id, "remove");
782
        $this->refresh();
783
784
        return $status;
785
    }
786
787
    /**
788
     * Set the player's email address and reset their verification status
789
     * @param string $email The address
790
     */
791
    public function setEmailAddress($email)
792
    {
793
        $this->lazyLoad();
794
795
        if ($this->email == $email) {
796
            // The e-mail hasn't changed, don't do anything
797
            return;
798
        }
799
800
        $this->setVerified(false);
801
        $this->generateNewConfirmCode();
802
803
        $this->updateProperty($this->email, 'email', $email);
804
    }
805
806
    /**
807
     * Set whether the player has verified their e-mail address
808
     *
809
     * @param  bool $verified Whether the player is verified or not
810
     * @return self
811
     */
812
    public function setVerified($verified)
813
    {
814
        $this->lazyLoad();
815
816
        if ($verified) {
817
            $this->setConfirmCode(null);
818
        }
819
820
        return $this->updateProperty($this->verified, 'verified', $verified);
821
    }
822
823
    /**
824
     * Generate a new random confirmation token for e-mail address verification
825
     *
826
     * @return self
827
     */
828
    public function generateNewConfirmCode()
829
    {
830
        $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...
831
        $random = $generator->nextBytes(16);
832
833
        return $this->setConfirmCode(bin2hex($random));
834
    }
835
836
    /**
837
     * Set the confirmation token for e-mail address verification
838
     *
839
     * @param  string $code The confirmation code
840
     * @return self
841
     */
842
    private function setConfirmCode($code)
843
    {
844
        $this->lazyLoad();
845
846
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
847
    }
848
849
    /**
850
     * Set what kind of events the player should be e-mailed about
851
     *
852
     * @param  string $receives The type of notification
853
     * @return self
854
     */
855
    public function setReceives($receives)
856
    {
857
        $this->lazyLoad();
858
859
        return $this->updateProperty($this->receives, 'receives', $receives);
860
    }
861
862
    /**
863
     * Set whether the callsign of the player is outdated
864
     *
865
     * @param  bool $outdated Whether the callsign is outdated
866
     * @return self
867
     */
868 72
    public function setOutdated($outdated)
869
    {
870 72
        $this->lazyLoad();
871
872 72
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
873
    }
874
875
    /**
876
     * Set the player's description
877
     * @param string $description The description
878
     */
879
    public function setDescription($description)
880
    {
881
        $this->updateProperty($this->description, "description", $description);
882
    }
883
884
    /**
885
     * Set the player's timezone
886
     * @param string $timezone The timezone
887
     */
888
    public function setTimezone($timezone)
889
    {
890
        $this->updateProperty($this->timezone, "timezone", $timezone);
891
    }
892
893
    /**
894
     * Set the player's team
895
     * @param int $team The team's ID
896
     */
897 51
    public function setTeam($team)
898
    {
899 51
        $this->updateProperty($this->team, "team", $team);
900 51
    }
901
902
    /**
903
     * Set the match the player last participated in
904
     *
905
     * @param int $match The match's ID
906
     */
907 28
    public function setLastMatch($match)
908
    {
909 28
        $this->updateProperty($this->last_match, 'last_match', $match);
910 28
    }
911
912
    /**
913
     * Set the player's status
914
     * @param string $status The new status
915
     */
916
    public function setStatus($status)
917
    {
918
        $this->updateProperty($this->status, 'status', $status);
919
    }
920
921
    /**
922
     * Set the player's admin notes
923
     * @param  string $admin_notes The new admin notes
924
     * @return self
925
     */
926
    public function setAdminNotes($admin_notes)
927
    {
928
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
929
    }
930
931
    /**
932
     * Set the player's country
933
     * @param  int   $country The ID of the new country
934
     * @return self
935
     */
936
    public function setCountry($country)
937
    {
938
        return $this->updateProperty($this->country, 'country', $country);
939
    }
940
941
    /**
942
     * Updates this player's last login
943
     */
944
    public function updateLastLogin()
945
    {
946
        $this->update("last_login", TimeDate::now()->toMysql());
947
    }
948
949
    /**
950
     * Get the player's username
951
     * @return string The username
952
     */
953 1
    public function getUsername()
954
    {
955 1
        return $this->name;
956
    }
957
958
    /**
959
     * Get the player's username, safe for use in your HTML
960
     * @return string The username
961
     */
962 1
    public function getEscapedUsername()
963
    {
964 1
        return $this->getEscapedName();
965
    }
966
967
    /**
968
     * Alias for Player::setUsername()
969
     *
970
     * @param  string $username The new username
971
     * @return self
972
     */
973
    public function setName($username)
974
    {
975
        return $this->setUsername($username);
976
    }
977
978
    /**
979
     * Mark all the unread messages of a player as read
980
     *
981
     * @return void
982
     */
983
    public function markMessagesAsRead()
984
    {
985
        $this->db->execute(
986
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
987
            array($this->id)
988
        );
989
    }
990
991
    /**
992
     * Set the roles of a user
993
     *
994
     * @todo   Is it worth making this faster?
995
     * @param  Role[] $roles The new roles of the user
996
     * @return self
997
     */
998
    public function setRoles($roles)
999
    {
1000
        $this->lazyLoad();
1001
1002
        $oldRoles = Role::mapToIds($this->roles);
1003
        $this->roles = $roles;
1004
        $roleIds = Role::mapToIds($roles);
1005
1006
        $newRoles     = array_diff($roleIds, $oldRoles);
1007
        $removedRoles = array_diff($oldRoles, $roleIds);
1008
1009
        foreach ($newRoles as $role) {
1010
            $this->modifyRole($role, 'add');
1011
        }
1012
1013
        foreach ($removedRoles as $role) {
1014
            $this->modifyRole($role, 'remove');
1015
        }
1016
1017
        $this->refresh();
1018
1019
        return $this;
1020
    }
1021
1022
    /**
1023
     * Give or remove a role to/form a player
1024
     *
1025
     * @param int    $role_id The role ID to add or remove
1026
     * @param string $action  Whether to "add" or "remove" a role for a player
1027
     *
1028
     * @return bool Whether the operation was successful or not
1029
     */
1030 72
    private function modifyRole($role_id, $action)
1031
    {
1032 72
        $role = Role::get($role_id);
1033
1034 72
        if ($role->isValid()) {
1035 72
            if ($action == "add") {
1036 72
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
1037
            } elseif ($action == "remove") {
1038
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
1039
            } else {
1040
                throw new Exception("Unrecognized role action");
1041
            }
1042
1043 72
            return true;
1044
        }
1045
1046
        return false;
1047
    }
1048
1049
    /**
1050
     * Given a player's BZID, get a player object
1051
     *
1052
     * @param  int    $bzid The player's BZID
1053
     * @return Player
1054
     */
1055 23
    public static function getFromBZID($bzid)
1056
    {
1057 23
        return self::get(self::fetchIdFrom($bzid, "bzid"));
1058
    }
1059
1060
    /**
1061
     * Get a single player by their username
1062
     *
1063
     * @param  string $username The username to look for
1064
     * @return Player
1065
     */
1066 1
    public static function getFromUsername($username)
1067
    {
1068 1
        $player = static::get(self::fetchIdFrom($username, 'username'));
1069
1070 1
        return $player->inject('name', $username);
1071
    }
1072
1073
    /**
1074
     * Get all the players in the database that have an active status
1075
     * @return Player[] An array of player BZIDs
1076
     */
1077
    public static function getPlayers()
1078
    {
1079
        return self::arrayIdToModel(
1080
            self::fetchIdsFrom("status", array("active", "test"), false)
1081
        );
1082
    }
1083
1084
    /**
1085
     * Show the number of notifications the user hasn't read yet
1086
     * @return int
1087
     */
1088 1
    public function countUnreadNotifications()
1089
    {
1090 1
        return Notification::countUnreadNotifications($this->id);
1091
    }
1092
1093
    /**
1094
     * Count the number of matches a player has participated in
1095
     * @return int
1096
     */
1097
    public function getMatchCount()
1098
    {
1099
        if ($this->cachedMatchCount === null) {
1100
            $this->cachedMatchCount = Match::getQueryBuilder()
1101
                ->active()
1102
                ->with($this)
1103
                ->count();
1104
        }
1105
1106
        return $this->cachedMatchCount;
1107
    }
1108
1109
    /**
1110
     * Get the (victory/total matches) ratio of the player
1111
     * @return float
1112
     */
1113
    public function getMatchWinRatio()
1114
    {
1115
        $count = $this->getMatchCount();
1116
1117
        if ($count == 0) {
1118
            return 0;
1119
        }
1120
1121
        $wins = Match::getQueryBuilder()
1122
            ->active()
1123
            ->with($this, 'win')
1124
            ->count();
1125
1126
        return $wins / $count;
1127
    }
1128
1129
    /**
1130
     * Get the (total caps made by team/total matches) ratio of the player
1131
     * @return float
1132
     */
1133
    public function getMatchAverageCaps()
1134
    {
1135
        $count = $this->getMatchCount();
1136
1137
        if ($count == 0) {
1138
            return 0;
1139
        }
1140
1141
        // Get the sum of team A points if the player was in team A, team B
1142
        // points if the player was in team B, and their average if the player
1143
        // was on both teams for some reason
1144
        $query = $this->db->query(
1145
            "SELECT SUM(
1146
                IF(
1147
                    FIND_IN_SET(?, team_a_players) AND FIND_IN_SET(?, team_b_players),
1148
                    (team_a_points+team_b_points)/2,
1149
                    IF(FIND_IN_SET(?, team_a_players), team_a_points, team_b_points)
1150
                )
1151
            ) AS sum FROM matches WHERE status='entered' AND (FIND_IN_SET(?, team_a_players) OR FIND_IN_SET(?, team_b_players))",
1152
            array_fill(0, 5, $this->id)
1153
        );
1154
1155
        return $query[0]['sum'] / $count;
1156
    }
1157
1158
    /**
1159
     * Get the match activity in matches per day for a player
1160
     *
1161
     * @return float
1162
     */
1163
    public function getMatchActivity()
1164
    {
1165
        if ($this->matchActivity !== null) {
1166
            return $this->matchActivity;
1167
        }
1168
1169
        $activity = 0.0;
1170
1171
        $matches = Match::getQueryBuilder()
1172
            ->active()
1173
            ->with($this)
1174
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
1175
            ->getModels($fast = true);
1176
1177
        foreach ($matches as $match) {
1178
            $activity += $match->getActivity();
1179
        }
1180
1181
        return $activity;
1182
    }
1183
1184
    /**
1185
     * Return an array of matches this player participated in per month.
1186
     *
1187
     * ```
1188
     * ['yyyy-mm'] = <number of matches>
1189
     * ```
1190
     *
1191
     * @param TimeDate|string $timePeriod
1192
     *
1193
     * @return int[]
1194
     */
1195
    public function getMatchSummary($timePeriod = '1 year ago')
1196
    {
1197
        $since = ($timePeriod instanceof TimeDate) ? $timePeriod : TimeDate::from($timePeriod);
1198
1199
        if (!isset($this->cachedMatchSummary[(string)$timePeriod])) {
1200
            $this->cachedMatchSummary[(string)$timePeriod] = Match::getQueryBuilder()
1201
                ->active()
1202
                ->with($this)
1203
                ->where('time')->isAfter($since)
1204
                ->getSummary($since)
1205
            ;
1206
        }
1207
1208
        return $this->cachedMatchSummary[(string)$timePeriod];
1209
    }
1210
1211
    /**
1212
     * Show the number of messages the user hasn't read yet
1213
     * @return int
1214
     */
1215 1
    public function countUnreadMessages()
1216
    {
1217 1
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
1218 1
            $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...
1219
        );
1220
    }
1221
1222
    /**
1223
     * Get all of the members belonging to a team
1224
     * @param  int      $teamID The ID of the team to fetch the members of
1225
     * @return Player[] An array of Player objects of the team members
1226
     */
1227 2
    public static function getTeamMembers($teamID)
1228
    {
1229 2
        return self::arrayIdToModel(
1230 2
            self::fetchIds("WHERE team = ?", array($teamID))
1231
        );
1232
    }
1233
1234
    /**
1235
     * {@inheritdoc}
1236
     */
1237 1
    public static function getActiveStatuses()
1238
    {
1239 1
        return array('active', 'reported', 'test');
1240
    }
1241
1242
    /**
1243
     * {@inheritdoc}
1244
     */
1245 72
    public static function getEagerColumns($prefix = null)
1246
    {
1247
        $columns = [
1248 72
            'id',
1249
            'bzid',
1250
            'team',
1251
            'username',
1252
            'alias',
1253
            'status',
1254
            'avatar',
1255
            'country',
1256
        ];
1257
1258 72
        return self::formatColumns($prefix, $columns);
1259
    }
1260
1261
    /**
1262
     * {@inheritdoc}
1263
     */
1264 72
    public static function getLazyColumns($prefix = null)
1265
    {
1266
        $columns = [
1267 72
            'email',
1268
            'verified',
1269
            'receives',
1270
            'confirm_code',
1271
            'outdated',
1272
            'description',
1273
            'timezone',
1274
            'joined',
1275
            'last_login',
1276
            'last_match',
1277
            'admin_notes',
1278
        ];
1279
1280 72
        return self::formatColumns($prefix, $columns);
1281
    }
1282
1283
    /**
1284
     * Get a query builder for players
1285
     * @return PlayerQueryBuilder
1286
     */
1287
    public static function getQueryBuilder()
1288
    {
1289
        return new PlayerQueryBuilder('Player', array(
1290
            'columns' => array(
1291
                'name'     => 'username',
1292
                'team'     => 'team',
1293
                'outdated' => 'outdated',
1294
                'status'   => 'status',
1295
            ),
1296
            'name' => 'name',
1297
        ));
1298
    }
1299
1300
    /**
1301
     * Enter a new player to the database
1302
     * @param  int              $bzid        The player's bzid
1303
     * @param  string           $username    The player's username
1304
     * @param  int              $team        The player's team
1305
     * @param  string           $status      The player's status
1306
     * @param  int              $role_id     The player's role when they are first created
1307
     * @param  string           $avatar      The player's profile avatar
1308
     * @param  string           $description The player's profile description
1309
     * @param  int              $country     The player's country
1310
     * @param  string           $timezone    The player's timezone
1311
     * @param  string|\TimeDate $joined      The date the player joined
1312
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1313
     * @return Player           An object representing the player that was just entered
1314
     */
1315 72
    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")
1316
    {
1317 72
        $joined = TimeDate::from($joined);
1318 72
        $last_login = TimeDate::from($last_login);
1319 72
        $timezone = ($timezone) ?: date_default_timezone_get();
1320
1321 72
        $player = self::create(array(
1322 72
            'bzid'        => $bzid,
1323 72
            'team'        => $team,
1324 72
            'username'    => $username,
1325 72
            'alias'       => self::generateAlias($username),
1326 72
            'status'      => $status,
1327 72
            'avatar'      => $avatar,
1328 72
            'description' => $description,
1329 72
            'country'     => $country,
1330 72
            'timezone'    => $timezone,
1331 72
            'joined'      => $joined->toMysql(),
1332 72
            'last_login'  => $last_login->toMysql(),
1333
        ));
1334
1335 72
        $player->addRole($role_id);
1336 72
        $player->getIdenticon($player->getId());
1337 72
        $player->setUsername($username);
1338
1339 72
        return $player;
1340
    }
1341
1342
    /**
1343
     * Determine if a player exists in the database
1344
     * @param  int  $bzid The player's bzid
1345
     * @return bool Whether the player exists in the database
1346
     */
1347
    public static function playerBZIDExists($bzid)
1348
    {
1349
        return self::getFromBZID($bzid)->isValid();
1350
    }
1351
1352
    /**
1353
     * Change a player's callsign and add it to the database if it does not
1354
     * exist as a past callsign
1355
     *
1356
     * @param  string $username The new username of the player
1357
     * @return self
1358
     */
1359 72
    public function setUsername($username)
1360
    {
1361
        // The player's username was just fetched from BzDB, it's definitely not
1362
        // outdated
1363 72
        $this->setOutdated(false);
1364
1365
        // Players who have this player's username are considered outdated
1366 72
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1367
1368 72
        if ($username === $this->name) {
1369
            // The player's username hasn't changed, no need to do anything
1370 72
            return $this;
1371
        }
1372
1373
        // Players who used to have our player's username are not outdated anymore,
1374
        // unless they are more than one.
1375
        // Even though we are sure that the old and new usernames are not equal,
1376
        // MySQL makes a different type of string equality tests, which is why we
1377
        // also check IDs to make sure not to affect our own player's outdatedness.
1378
        $this->db->execute("
1379
            UPDATE {$this->table} SET outdated =
1380
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1381
            WHERE username = ? AND id != ?",
1382
            array($this->name, $this->id, $this->name, $this->id));
1383
1384
        $this->updateProperty($this->name, 'username', $username);
1385
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1386
        $this->resetAlias();
1387
1388
        return $this;
1389
    }
1390
1391
    /**
1392
     * Alphabetical order function for use in usort (case-insensitive)
1393
     * @return Closure The sort function
1394
     */
1395
    public static function getAlphabeticalSort()
1396
    {
1397 1
        return function (Player $a, Player $b) {
1398 1
            return strcasecmp($a->getUsername(), $b->getUsername());
1399 1
        };
1400
    }
1401
1402
    /**
1403
     * {@inheritdoc}
1404
     * @todo Add a constraint that does this automatically
1405
     */
1406 72
    public function wipe()
1407
    {
1408 72
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1409
1410 72
        parent::wipe();
1411 72
    }
1412
1413
    /**
1414
     * Find whether the player can delete a model
1415
     *
1416
     * @param  PermissionModel $model       The model that will be seen
1417
     * @param  bool         $showDeleted Whether to show deleted models to admins
1418
     * @return bool
1419
     */
1420 1
    public function canSee($model, $showDeleted = false)
1421
    {
1422 1
        return $model->canBeSeenBy($this, $showDeleted);
1423
    }
1424
1425
    /**
1426
     * Find whether the player can delete a model
1427
     *
1428
     * @param  PermissionModel $model The model that will be deleted
1429
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1430
     *                                to soft-delete ones
1431
     * @return bool
1432
     */
1433 1
    public function canDelete($model, $hard = false)
1434
    {
1435 1
        if ($hard) {
1436
            return $model->canBeHardDeletedBy($this);
1437
        } else {
1438 1
            return $model->canBeSoftDeletedBy($this);
1439
        }
1440
    }
1441
1442
    /**
1443
     * Find whether the player can create a model
1444
     *
1445
     * @param  string  $modelName The PHP class identifier of the model type
1446
     * @return bool
1447
     */
1448 1
    public function canCreate($modelName)
1449
    {
1450 1
        return $modelName::canBeCreatedBy($this);
1451
    }
1452
1453
    /**
1454
     * Find whether the player can edit a model
1455
     *
1456
     * @param  PermissionModel $model The model which will be edited
1457
     * @return bool
1458
     */
1459 1
    public function canEdit($model)
1460
    {
1461 1
        return $model->canBeEditedBy($this);
1462
    }
1463
}
1464