Completed
Push — feature/player-elo ( 127bff...a3fab4 )
by Vladimir
07:14
created

Player::getEagerColumns()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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