Completed
Push — hotfix/issue-170 ( ac6752 )
by Vladimir
06:34
created

Player::invalidateMatchFromCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 1
1
<?php
2
/**
3
 * This file contains functionality relating to a league player
4
 *
5
 * @package    BZiON\Models
6
 * @license    https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3
7
 */
8
9
use Carbon\Carbon;
10
use Symfony\Component\Security\Core\Util\SecureRandom;
11
use Symfony\Component\Security\Core\Util\StringUtils;
12
13
/**
14
 * A league player
15
 * @package    BZiON\Models
16
 */
17
class Player extends AvatarModel implements NamedModel, DuplexUrlInterface, EloInterface
18
{
19
    /**
20
     * These are built-in roles that cannot be deleted via the web interface so we will be storing these values as
21
     * constant variables. Hopefully, a user won't be silly enough to delete them manually from the database.
22
     *
23
     * @TODO Deprecate these and use the Role constants
24
     */
25
    const DEVELOPER    = Role::DEVELOPER;
26
    const ADMIN        = Role::ADMINISTRATOR;
27
    const COP          = Role::COP;
28
    const REFEREE      = Role::REFEREE;
29
    const S_ADMIN      = Role::SYSADMIN;
30
    const PLAYER       = Role::PLAYER;
31
    const PLAYER_NO_PM = Role::PLAYER_NO_PM;
32
33
    /**
34
     * The bzid of the player
35
     * @var int
36
     */
37
    protected $bzid;
38
39
    /**
40
     * The id of the player's team
41
     * @var int
42
     */
43
    protected $team;
44
45
    /**
46
     * The player's e-mail address
47
     * @var string
48
     */
49
    protected $email;
50
51
    /**
52
     * Whether the player has verified their e-mail address
53
     * @var bool
54
     */
55
    protected $verified;
56
57
    /**
58
     * What kind of events the player should be e-mailed about
59
     * @var string
60
     */
61
    protected $receives;
62
63
    /**
64
     * A confirmation code for the player's e-mail address verification
65
     * @var string
66
     */
67
    protected $confirmCode;
68
69
    /**
70
     * Whether the callsign of the player is outdated
71
     * @var bool
72
     */
73
    protected $outdated;
74
75
    /**
76
     * The player's profile description
77
     * @var string
78
     */
79
    protected $description;
80
81
    /**
82
     * The id of the player's country
83
     * @var int
84
     */
85
    protected $country;
86
87
    /**
88
     * The site theme this player has chosen
89
     * @var string
90
     */
91
    protected $theme;
92
93
    /**
94
     * Whether or not this player has opted-in for color blindness assistance.
95
     * @var bool
96
     */
97
    protected $color_blind_enabled;
98
99
    /**
100
     * The player's timezone PHP identifier, e.g. "Europe/Paris"
101
     * @var string
102
     */
103
    protected $timezone;
104
105
    /**
106
     * The date the player joined the site
107
     * @var TimeDate
108
     */
109
    protected $joined;
110
111
    /**
112
     * The date of the player's last login
113
     * @var TimeDate
114
     */
115
    protected $last_login;
116
117
    /**
118
     * The date of the player's last match
119
     * @var Match
120
     */
121
    protected $last_match;
122
123
    /**
124
     * The roles a player belongs to
125
     * @var Role[]
126
     */
127
    protected $roles;
128
129
    /**
130
     * The permissions a player has
131
     * @var Permission[]
132
     */
133
    protected $permissions;
134
135
    /**
136
     * A section for admins to write notes about players
137
     * @var string
138
     */
139
    protected $admin_notes;
140
141
    /**
142
     * The ban of the player, or null if the player is not banned
143
     * @var Ban|null
144
     */
145
    protected $ban;
146
147
    /**
148
     * Cached results for match summaries
149
     *
150
     * @var array
151
     */
152
    private $cachedMatchSummary;
153
154
    /**
155
     * The cached match count for a player
156
     *
157
     * @var int
158
     */
159
    private $cachedMatchCount = null;
160
161
    /**
162
     * The Elo for this player that has been explicitly set for this player from a database query. This value will take
163
     * precedence over having to build to an Elo season history.
164
     *
165
     * @var int
166
     */
167
    private $elo;
168
    private $eloSeason;
169
    private $eloSeasonHistory;
170
171
    private $matchActivity;
172
173
    /**
174
     * The name of the database table used for queries
175
     */
176
    const TABLE = "players";
177
178
    /**
179
     * The location where avatars will be stored
180
     */
181
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
182
183
    const EDIT_PERMISSION = Permission::EDIT_USER;
184
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
185
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
186
187
    /**
188
     * {@inheritdoc}
189
     */
190
    protected function assignResult($player)
191
    {
192
        $this->bzid = $player['bzid'];
193
        $this->name = $player['username'];
194
        $this->alias = $player['alias'];
195
        $this->team = $player['team'];
196
        $this->status = $player['status'];
197
        $this->avatar = $player['avatar'];
198
        $this->country = $player['country'];
199
200
        if (array_key_exists('activity', $player)) {
201
            $this->matchActivity = ($player['activity'] != null) ? $player['activity'] : 0.0;
202
        }
203
204
        if (array_key_exists('elo', $player)) {
205
            $this->elo = $player['elo'];
206
        }
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    protected function assignLazyResult($player)
213
    {
214
        $this->email = $player['email'];
215
        $this->verified = $player['verified'];
216
        $this->receives = $player['receives'];
217
        $this->confirmCode = $player['confirm_code'];
218
        $this->outdated = $player['outdated'];
219
        $this->description = $player['description'];
220
        $this->timezone = $player['timezone'];
221
        $this->joined = TimeDate::fromMysql($player['joined']);
222
        $this->last_login = TimeDate::fromMysql($player['last_login']);
223
        $this->last_match = Match::get($player['last_match']);
224
        $this->admin_notes = $player['admin_notes'];
225
        $this->ban = Ban::getBan($this->id);
226
        $this->color_blind_enabled = $player['color_blind_enabled'];
227
228
        $this->cachedMatchSummary = [];
229
230
        // Theme user options
231
        if (isset($player['theme'])) {
232
            $this->theme = $player['theme'];
233
        } else {
234
            $themes = Service::getSiteThemes();
235
            $this->theme = $themes[0]['slug'];
236
        }
237
238
        $this->updateUserPermissions();
239
    }
240
241
    /**
242
     * Add a player a new role
243
     *
244
     * @param Role|int $role_id The role ID to add a player to
245
     *
246
     * @return bool Whether the operation was successful or not
247
     */
248
    public function addRole($role_id)
249
    {
250
        if ($role_id instanceof Role) {
251
            $role_id = $role_id->getId();
252
        }
253
254
        $this->lazyLoad();
255
256
        // Make sure the player doesn't already have the role
257
        foreach ($this->roles as $playerRole) {
258
            if ($playerRole->getId() == $role_id) {
259
                return false;
260
            }
261
        }
262
263
        $status = $this->modifyRole($role_id, "add");
264
        $this->refresh();
265
266
        return $status;
267
    }
268
269
    /**
270
     * Get the notes admins have left about a player
271
     * @return string The notes
272
     */
273
    public function getAdminNotes()
274
    {
275
        $this->lazyLoad();
276
277
        return $this->admin_notes;
278
    }
279
280
    /**
281
     * Get the player's BZID
282
     * @return int The BZID
283
     */
284
    public function getBZID()
285
    {
286
        return $this->bzid;
287
    }
288
289
    /**
290
     * Get the country a player belongs to
291
     *
292
     * @return Country The country belongs to
293
     */
294
    public function getCountry()
295
    {
296
        return Country::get($this->country);
297
    }
298
299
    /**
300
     * Get the e-mail address of the player
301
     *
302
     * @return string The address
303
     */
304
    public function getEmailAddress()
305
    {
306
        $this->lazyLoad();
307
308
        return $this->email;
309
    }
310
311
    /**
312
     * Build a key that we'll use for caching season Elo data in this model
313
     *
314
     * @param  string|null $season The season to get
315
     * @param  int|null    $year   The year of the season to get
316
     *
317
     * @return string
318
     */
319
    private function buildSeasonKey(&$season, &$year)
320
    {
321
        if ($season === null) {
322
            $season = Season::getCurrentSeason();
323
        }
324
325
        if ($year === null) {
326
            $year = Carbon::now()->year;
327
        }
328
329
        return sprintf('%s-%s', $year, $season);
330
    }
331
332
    /**
333
     * Build a key to use for caching season Elo data in this model from a timestamp
334
     *
335
     * @param DateTime $timestamp
336
     *
337
     * @return string
338
     */
339
    private function buildSeasonKeyFromTimestamp(\DateTime $timestamp)
340
    {
341
        $seasonInfo = Season::getSeason($timestamp);
342
343
        return sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
344
    }
345
346
    /**
347
     * Remove all Elo data for this model for matches occurring after the given match (inclusive)
348
     *
349
     * This function will not remove the Elo data for this match from the database. Ideally, this function should only
350
     * be called during Elo recalculation for this match.
351
     *
352
     * @internal
353
     *
354
     * @param Match $match
355
     *
356
     * @see Match::recalculateElo()
357
     */
358
    public function invalidateMatchFromCache(Match $match)
359
    {
360
        $seasonKey = $this->buildSeasonKeyFromTimestamp($match->getTimestamp());
361
        $seasonElo = null;
0 ignored issues
show
Unused Code introduced by
$seasonElo is not used, you could remove the assignment.

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

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

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

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

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