Completed
Push — feature/player-elo-v3 ( e6aec1 )
by Vladimir
02:55
created

Player::markAsBanned()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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