Completed
Push — feature/player-elo ( 44acf1...97e749 )
by Vladimir
05:17
created

Player::invalidateMatchFromCache()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4.25

Importance

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