Completed
Push — feature/sass-standard ( a5bd48...aade0e )
by Vladimir
14:40 queued 11:12
created

Player::addRole()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4.016

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 9
cts 10
cp 0.9
rs 9.2
c 0
b 0
f 0
cc 4
eloc 10
nc 6
nop 1
crap 4.016
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 AvatarModel 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
     * The cached match count for a player
142
     *
143
     * @var int
144
     */
145
    private $cachedMatchCount = null;
146
147
    /**
148
     * The name of the database table used for queries
149
     */
150
    const TABLE = "players";
151
152
    /**
153
     * The location where avatars will be stored
154
     */
155
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
156
157
    const EDIT_PERMISSION = Permission::EDIT_USER;
158
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
159
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 39
    protected function assignResult($player)
165
    {
166 39
        $this->bzid = $player['bzid'];
167 39
        $this->name = $player['username'];
168 39
        $this->alias = $player['alias'];
169 39
        $this->team = $player['team'];
170 39
        $this->status = $player['status'];
171 39
        $this->avatar = $player['avatar'];
172 39
        $this->country = $player['country'];
173 39
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 39
    protected function assignLazyResult($player)
179
    {
180 39
        $this->email = $player['email'];
181 39
        $this->verified = $player['verified'];
182 39
        $this->receives = $player['receives'];
183 39
        $this->confirmCode = $player['confirm_code'];
184 39
        $this->outdated = $player['outdated'];
185 39
        $this->description = $player['description'];
186 39
        $this->timezone = $player['timezone'];
187 39
        $this->joined = TimeDate::fromMysql($player['joined']);
188 39
        $this->last_login = TimeDate::fromMysql($player['last_login']);
189 39
        $this->last_match = Match::get($player['last_match']);
190 39
        $this->admin_notes = $player['admin_notes'];
191 39
        $this->ban = Ban::getBan($this->id);
192
193 39
        $this->updateUserPermissions();
194 39
    }
195
196
    /**
197
     * Add a player a new role
198
     *
199
     * @param Role|int $role_id The role ID to add a player to
200
     *
201
     * @return bool Whether the operation was successful or not
202
     */
203 39
    public function addRole($role_id)
204
    {
205 39
        if ($role_id instanceof Role) {
206 1
            $role_id = $role_id->getId();
207
        }
208
209 39
        $this->lazyLoad();
210
211
        // Make sure the player doesn't already have the role
212 39
        foreach ($this->roles as $playerRole) {
213 14
            if ($playerRole->getId() == $role_id) {
214
                return false;
215
            }
216
        }
217
218 39
        $status = $this->modifyRole($role_id, "add");
219 39
        $this->refresh();
220
221 39
        return $status;
222
    }
223
224
    /**
225
     * Get the notes admins have left about a player
226
     * @return string The notes
227
     */
228
    public function getAdminNotes()
229
    {
230
        $this->lazyLoad();
231
232
        return $this->admin_notes;
233
    }
234
235
    /**
236
     * Get the player's BZID
237
     * @return int The BZID
238
     */
239
    public function getBZID()
240
    {
241
        return $this->bzid;
242
    }
243
244
    /**
245
     * Get the country a player belongs to
246
     *
247
     * @return Country The country belongs to
248
     */
249 1
    public function getCountry()
250
    {
251 1
        return Country::get($this->country);
252
    }
253
254
    /**
255
     * Get the e-mail address of the player
256
     *
257
     * @return string The address
258
     */
259
    public function getEmailAddress()
260
    {
261
        $this->lazyLoad();
262
263
        return $this->email;
264
    }
265
266
    /**
267
     * Returns whether the player has verified their e-mail address
268
     *
269
     * @return bool `true` for verified players
270
     */
271
    public function isVerified()
272
    {
273
        $this->lazyLoad();
274
275
        return $this->verified;
276
    }
277
278
    /**
279
     * Returns the confirmation code for the player's e-mail address verification
280
     *
281
     * @return string The player's confirmation code
282
     */
283
    public function getConfirmCode()
284
    {
285
        $this->lazyLoad();
286
287
        return $this->confirmCode;
288
    }
289
290
    /**
291
     * Returns what kind of events the player should be e-mailed about
292
     *
293
     * @return string The type of notifications
294
     */
295
    public function getReceives()
296
    {
297
        $this->lazyLoad();
298
299
        return $this->receives;
300
    }
301
302
    /**
303
     * Finds out whether the specified player wants and can receive an e-mail
304
     * message
305
     *
306
     * @param  string  $type
307
     * @return bool `true` if the player should be sent an e-mail
308
     */
309
    public function canReceive($type)
310
    {
311
        $this->lazyLoad();
312
313
        if (!$this->email || !$this->isVerified()) {
314
            // Unverified e-mail means the user will receive nothing
315
            return false;
316
        }
317
318
        if ($this->receives == 'everything') {
319
            return true;
320
        }
321
322
        return $this->receives == $type;
323
    }
324
325
    /**
326
     * Find out whether the specified confirmation code is correct
327
     *
328
     * This method protects against timing attacks
329
     *
330
     * @param  string $code The confirmation code to check
331
     * @return bool `true` for a correct e-mail verification code
332
     */
333
    public function isCorrectConfirmCode($code)
334
    {
335
        $this->lazyLoad();
336
337
        if ($this->confirmCode === null) {
338
            return false;
339
        }
340
341
        return StringUtils::equals($code, $this->confirmCode);
342
    }
343
344
    /**
345
     * Get the player's sanitized description
346
     * @return string The description
347
     */
348
    public function getDescription()
349
    {
350
        $this->lazyLoad();
351
352
        return $this->description;
353
    }
354
355
    /**
356
     * Get the joined date of the player
357
     * @return TimeDate The joined date of the player
358
     */
359
    public function getJoinedDate()
360
    {
361
        $this->lazyLoad();
362
363
        return $this->joined->copy();
364
    }
365
366
    /**
367
     * Get all of the known IPs used by the player
368
     *
369
     * @return string[][] An array containing IPs and hosts
370
     */
371
    public function getKnownIPs()
372
    {
373
        return $this->db->query("SELECT DISTINCT ip, host FROM visits WHERE player = ? LIMIT 10", array($this->getId()));
374
    }
375
376
    /**
377
     * Get the last login for a player
378
     * @return TimeDate The date of the last login
379
     */
380
    public function getLastLogin()
381
    {
382
        $this->lazyLoad();
383
384
        return $this->last_login->copy();
385
    }
386
387
    /**
388
     * Get the last match
389
     * @return Match
390
     */
391
    public function getLastMatch()
392
    {
393
        $this->lazyLoad();
394
395
        return $this->last_match;
396
    }
397
398
    /**
399
     * Get all of the callsigns a player has used to log in to the website
400
     * @return string[] An array containing all of the past callsigns recorded for a player
401
     */
402
    public function getPastCallsigns()
403
    {
404
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
405
    }
406
407
    /**
408
     * Get the player's team
409
     * @return Team The object representing the team
410
     */
411 2
    public function getTeam()
412
    {
413 2
        return Team::get($this->team);
414
    }
415
416
    /**
417
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
418
     * @return string The timezone
419
     */
420 1
    public function getTimezone()
421
    {
422 1
        $this->lazyLoad();
423
424 1
        return ($this->timezone) ?: date_default_timezone_get();
425
    }
426
427
    /**
428
     * Get the roles of the player
429
     * @return Role[]
430
     */
431
    public function getRoles()
432
    {
433
        $this->lazyLoad();
434
435
        return $this->roles;
436
    }
437
438
    /**
439
     * Rebuild the list of permissions a user has been granted
440
     */
441 39
    private function updateUserPermissions()
442
    {
443 39
        $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...
444 39
        $this->permissions = array();
445
446 39
        foreach ($this->roles as $role) {
447 39
            $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...
448
        }
449 39
    }
450
451
    /**
452
     * Check if a player has a specific permission
453
     *
454
     * @param string|null $permission The permission to check for
455
     *
456
     * @return bool Whether or not the player has the permission
457
     */
458 2
    public function hasPermission($permission)
459
    {
460 2
        if ($permission === null) {
461 1
            return false;
462
        }
463
464 2
        $this->lazyLoad();
465
466 2
        return isset($this->permissions[$permission]);
467
    }
468
469
    /**
470
     * Check whether or not a player been in a match or has logged on in the specified amount of time to be considered
471
     * active
472
     *
473
     * @return bool True if the player has been active
474
     */
475
    public function hasBeenActive()
476
    {
477
        $this->lazyLoad();
478
479
        $interval  = Service::getParameter('bzion.miscellaneous.active_interval');
480
        $lastLogin = $this->last_login->copy()->modify($interval);
481
482
        $hasBeenActive = (TimeDate::now() <= $lastLogin);
483
484
        if ($this->last_match->isValid()) {
485
            $lastMatch = $this->last_match->getTimestamp()->copy()->modify($interval);
486
            $hasBeenActive = ($hasBeenActive || TimeDate::now() <= $lastMatch);
487
        }
488
489
        return $hasBeenActive;
490
    }
491
492
    /**
493
     * Check whether the callsign of the player is outdated
494
     *
495
     * Returns true if this player has probably changed their callsign, making
496
     * the current username stored in the database obsolete
497
     *
498
     * @return bool Whether or not the player is disabled
499
     */
500
    public function isOutdated()
501
    {
502
        $this->lazyLoad();
503
504
        return $this->outdated;
505
    }
506
507
    /**
508
     * Check if a player's account has been disabled
509
     *
510
     * @return bool Whether or not the player is disabled
511
     */
512
    public function isDisabled()
513
    {
514
        return $this->status == "disabled";
515
    }
516
517
    /**
518
     * Check if everyone can log in as this user on a test environment
519
     *
520
     * @return bool
521
     */
522 1
    public function isTestUser()
523
    {
524 1
        return $this->status == "test";
525
    }
526
527
    /**
528
     * Check if a player is teamless
529
     *
530
     * @return bool True if the player is teamless
531
     */
532 18
    public function isTeamless()
533
    {
534 18
        return empty($this->team);
535
    }
536
537
    /**
538
     * Mark a player's account as banned
539
     */
540 1
    public function markAsBanned()
541
    {
542 1
        if ($this->status != 'active') {
543
            return $this;
544
        }
545
546 1
        return $this->updateProperty($this->status, "status", "banned");
547
    }
548
549
    /**
550
     * Mark a player's account as unbanned
551
     */
552
    public function markAsUnbanned()
553
    {
554
        if ($this->status != 'banned') {
555
            return $this;
556
        }
557
558
        return $this->updateProperty($this->status, "status", "active");
559
    }
560
561
    /**
562
     * Find out if a player is banned
563
     *
564
     * @return bool
565
     */
566 2
    public function isBanned()
567
    {
568 2
        return Ban::getBan($this->id) !== null;
569
    }
570
571
    /**
572
     * Get the ban of the player
573
     *
574
     * This method performs a load of all the lazy parameters of the Player
575
     *
576
     * @return Ban|null The current ban of the player, or null if the player is
577
     *                  is not banned
578
     */
579
    public function getBan()
580
    {
581
        $this->lazyLoad();
582
583
        return $this->ban;
584
    }
585
586
    /**
587
     * Remove a player from a role
588
     *
589
     * @param int $role_id The role ID to add or remove
590
     *
591
     * @return bool Whether the operation was successful or not
592
     */
593
    public function removeRole($role_id)
594
    {
595
        $status = $this->modifyRole($role_id, "remove");
596
        $this->refresh();
597
598
        return $status;
599
    }
600
601
    /**
602
     * Set the player's email address and reset their verification status
603
     * @param string $email The address
604
     */
605
    public function setEmailAddress($email)
606
    {
607
        $this->lazyLoad();
608
609
        if ($this->email == $email) {
610
            // The e-mail hasn't changed, don't do anything
611
            return;
612
        }
613
614
        $this->setVerified(false);
615
        $this->generateNewConfirmCode();
616
617
        $this->updateProperty($this->email, 'email', $email);
618
    }
619
620
    /**
621
     * Set whether the player has verified their e-mail address
622
     *
623
     * @param  bool $verified Whether the player is verified or not
624
     * @return self
625
     */
626
    public function setVerified($verified)
627
    {
628
        $this->lazyLoad();
629
630
        if ($verified) {
631
            $this->setConfirmCode(null);
632
        }
633
634
        return $this->updateProperty($this->verified, 'verified', $verified);
635
    }
636
637
    /**
638
     * Generate a new random confirmation token for e-mail address verification
639
     *
640
     * @return self
641
     */
642
    public function generateNewConfirmCode()
643
    {
644
        $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...
645
        $random = $generator->nextBytes(16);
646
647
        return $this->setConfirmCode(bin2hex($random));
648
    }
649
650
    /**
651
     * Set the confirmation token for e-mail address verification
652
     *
653
     * @param  string $code The confirmation code
654
     * @return self
655
     */
656
    private function setConfirmCode($code)
657
    {
658
        $this->lazyLoad();
659
660
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
661
    }
662
663
    /**
664
     * Set what kind of events the player should be e-mailed about
665
     *
666
     * @param  string $receives The type of notification
667
     * @return self
668
     */
669
    public function setReceives($receives)
670
    {
671
        $this->lazyLoad();
672
673
        return $this->updateProperty($this->receives, 'receives', $receives);
674
    }
675
676
    /**
677
     * Set whether the callsign of the player is outdated
678
     *
679
     * @param  bool $outdated Whether the callsign is outdated
680
     * @return self
681
     */
682 39
    public function setOutdated($outdated)
683
    {
684 39
        $this->lazyLoad();
685
686 39
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
687
    }
688
689
    /**
690
     * Set the player's description
691
     * @param string $description The description
692
     */
693
    public function setDescription($description)
694
    {
695
        $this->updateProperty($this->description, "description", $description);
696
    }
697
698
    /**
699
     * Set the player's timezone
700
     * @param string $timezone The timezone
701
     */
702
    public function setTimezone($timezone)
703
    {
704
        $this->updateProperty($this->timezone, "timezone", $timezone);
705
    }
706
707
    /**
708
     * Set the player's team
709
     * @param int $team The team's ID
710
     */
711 18
    public function setTeam($team)
712
    {
713 18
        $this->updateProperty($this->team, "team", $team);
714 18
    }
715
716
    /**
717
     * Set the match the player last participated in
718
     *
719
     * @param int $match The match's ID
720
     */
721 9
    public function setLastMatch($match)
722
    {
723 9
        $this->updateProperty($this->last_match, 'last_match', $match);
724 9
    }
725
726
    /**
727
     * Set the player's status
728
     * @param string $status The new status
729
     */
730
    public function setStatus($status)
731
    {
732
        $this->updateProperty($this->status, 'status', $status);
733
    }
734
735
    /**
736
     * Set the player's admin notes
737
     * @param  string $admin_notes The new admin notes
738
     * @return self
739
     */
740
    public function setAdminNotes($admin_notes)
741
    {
742
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
743
    }
744
745
    /**
746
     * Set the player's country
747
     * @param  int   $country The ID of the new country
748
     * @return self
749
     */
750
    public function setCountry($country)
751
    {
752
        return $this->updateProperty($this->country, 'country', $country);
753
    }
754
755
    /**
756
     * Updates this player's last login
757
     */
758
    public function updateLastLogin()
759
    {
760
        $this->update("last_login", TimeDate::now()->toMysql());
761
    }
762
763
    /**
764
     * Get the player's username
765
     * @return string The username
766
     */
767 1
    public function getUsername()
768
    {
769 1
        return $this->name;
770
    }
771
772
    /**
773
     * Get the player's username, safe for use in your HTML
774
     * @return string The username
775
     */
776
    public function getEscapedUsername()
777
    {
778
        return $this->getEscapedName();
779
    }
780
781
    /**
782
     * Alias for Player::setUsername()
783
     *
784
     * @param  string $username The new username
785
     * @return self
786
     */
787
    public function setName($username)
788
    {
789
        return $this->setUsername($username);
790
    }
791
792
    /**
793
     * Mark all the unread messages of a player as read
794
     *
795
     * @return void
796
     */
797
    public function markMessagesAsRead()
798
    {
799
        $this->db->execute(
800
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
801
            array($this->id)
802
        );
803
    }
804
805
    /**
806
     * Set the roles of a user
807
     *
808
     * @todo   Is it worth making this faster?
809
     * @param  Role[] $roles The new roles of the user
810
     * @return self
811
     */
812
    public function setRoles($roles)
813
    {
814
        $this->lazyLoad();
815
816
        $oldRoles = Role::mapToIds($this->roles);
817
        $this->roles = $roles;
818
        $roleIds = Role::mapToIds($roles);
819
820
        $newRoles     = array_diff($roleIds, $oldRoles);
821
        $removedRoles = array_diff($oldRoles, $roleIds);
822
823
        foreach ($newRoles as $role) {
824
            $this->modifyRole($role, 'add');
825
        }
826
827
        foreach ($removedRoles as $role) {
828
            $this->modifyRole($role, 'remove');
829
        }
830
831
        $this->refresh();
832
833
        return $this;
834
    }
835
836
    /**
837
     * Give or remove a role to/form a player
838
     *
839
     * @param int    $role_id The role ID to add or remove
840
     * @param string $action  Whether to "add" or "remove" a role for a player
841
     *
842
     * @return bool Whether the operation was successful or not
843
     */
844 39
    private function modifyRole($role_id, $action)
845
    {
846 39
        $role = Role::get($role_id);
847
848 39
        if ($role->isValid()) {
849 39
            if ($action == "add") {
850 39
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
851
            } elseif ($action == "remove") {
852
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
853
            } else {
854
                throw new Exception("Unrecognized role action");
855
            }
856
857 39
            return true;
858
        }
859
860
        return false;
861
    }
862
863
    /**
864
     * Given a player's BZID, get a player object
865
     *
866
     * @param  int    $bzid The player's BZID
867
     * @return Player
868
     */
869 1
    public static function getFromBZID($bzid)
870
    {
871 1
        return self::get(self::fetchIdFrom($bzid, "bzid"));
872
    }
873
874
    /**
875
     * Get a single player by their username
876
     *
877
     * @param  string $username The username to look for
878
     * @return Player
879
     */
880 1
    public static function getFromUsername($username)
881
    {
882 1
        $player = static::get(self::fetchIdFrom($username, 'username'));
883
884 1
        return $player->inject('name', $username);
885
    }
886
887
    /**
888
     * Get all the players in the database that have an active status
889
     * @return Player[] An array of player BZIDs
890
     */
891
    public static function getPlayers()
892
    {
893
        return self::arrayIdToModel(
894
            self::fetchIdsFrom("status", array("active", "test"), false)
895
        );
896
    }
897
898
    /**
899
     * Show the number of notifications the user hasn't read yet
900
     * @return int
901
     */
902 1
    public function countUnreadNotifications()
903
    {
904 1
        return Notification::countUnreadNotifications($this->id);
905
    }
906
907
    /**
908
     * Count the number of matches a player has participated in
909
     * @return int
910
     */
911
    public function getMatchCount()
912
    {
913
        if ($this->cachedMatchCount === null) {
914
            $this->cachedMatchCount = Match::getQueryBuilder()
915
                ->active()
916
                ->with($this)
917
                ->count();
918
        }
919
920
        return $this->cachedMatchCount;
921
    }
922
923
    /**
924
     * Get the (victory/total matches) ratio of the player
925
     * @return float
926
     */
927
    public function getMatchWinRatio()
928
    {
929
        $count = $this->getMatchCount();
930
931
        if ($count == 0) {
932
            return 0;
933
        }
934
935
        $wins = Match::getQueryBuilder()
936
            ->active()
937
            ->with($this, 'win')
938
            ->count();
939
940
        return $wins / $count;
941
    }
942
943
    /**
944
     * Get the (total caps made by team/total matches) ratio of the player
945
     * @return float
946
     */
947
    public function getMatchAverageCaps()
948
    {
949
        $count = $this->getMatchCount();
950
951
        if ($count == 0) {
952
            return 0;
953
        }
954
955
        // Get the sum of team A points if the player was in team A, team B
956
        // points if the player was in team B, and their average if the player
957
        // was on both teams for some reason
958
        $query = $this->db->query(
959
            "SELECT SUM(
960
                IF(
961
                    FIND_IN_SET(?, team_a_players) AND FIND_IN_SET(?, team_b_players),
962
                    (team_a_points+team_b_points)/2,
963
                    IF(FIND_IN_SET(?, team_a_players), team_a_points, team_b_points)
964
                )
965
            ) AS sum FROM matches WHERE status='entered' AND (FIND_IN_SET(?, team_a_players) OR FIND_IN_SET(?, team_b_players))",
966
            array_fill(0, 5, $this->id)
967
        );
968
969
        return $query[0]['sum'] / $count;
970
    }
971
972
    /**
973
     * Get the match activity in matches per day for a player
974
     *
975
     * @return float
976
     */
977
    public function getMatchActivity()
978
    {
979
        $activity = 0.0;
980
981
        $matches = Match::getQueryBuilder()
982
            ->active()
983
            ->with($this)
984
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
985
            ->getModels($fast = true);
986
987
        foreach ($matches as $match) {
988
            $activity += $match->getActivity();
989
        }
990
991
        return $activity;
992
    }
993
994
    /**
995
     * Show the number of messages the user hasn't read yet
996
     * @return int
997
     */
998 1
    public function countUnreadMessages()
999
    {
1000 1
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
1001 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...
1002
        );
1003
    }
1004
1005
    /**
1006
     * Get all of the members belonging to a team
1007
     * @param  int      $teamID The ID of the team to fetch the members of
1008
     * @return Player[] An array of Player objects of the team members
1009
     */
1010 2
    public static function getTeamMembers($teamID)
1011
    {
1012 2
        return self::arrayIdToModel(
1013 2
            self::fetchIds("WHERE team = ?", array($teamID))
1014
        );
1015
    }
1016
1017
    /**
1018
     * {@inheritdoc}
1019
     */
1020 1
    public static function getActiveStatuses()
1021
    {
1022 1
        return array('active', 'reported', 'test');
1023
    }
1024
1025
    /**
1026
     * {@inheritdoc}
1027
     */
1028 39
    public static function getEagerColumns()
1029
    {
1030 39
        return 'id,bzid,team,username,alias,status,avatar,country';
1031
    }
1032
1033
    /**
1034
     * {@inheritdoc}
1035
     */
1036 39
    public static function getLazyColumns()
1037
    {
1038 39
        return 'email,verified,receives,confirm_code,outdated,description,timezone,joined,last_login,last_match,admin_notes';
1039
    }
1040
1041
    /**
1042
     * Get a query builder for players
1043
     * @return QueryBuilder
1044
     */
1045 View Code Duplication
    public static function getQueryBuilder()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1046
    {
1047
        return new QueryBuilder('Player', array(
1048
            'columns' => array(
1049
                'name'     => 'username',
1050
                'team'     => 'team',
1051
                'outdated' => 'outdated',
1052
                'status'   => 'status'
1053
            ),
1054
            'name' => 'name',
1055
        ));
1056
    }
1057
1058
    /**
1059
     * Enter a new player to the database
1060
     * @param  int              $bzid        The player's bzid
1061
     * @param  string           $username    The player's username
1062
     * @param  int              $team        The player's team
1063
     * @param  string           $status      The player's status
1064
     * @param  int              $role_id     The player's role when they are first created
1065
     * @param  string           $avatar      The player's profile avatar
1066
     * @param  string           $description The player's profile description
1067
     * @param  int              $country     The player's country
1068
     * @param  string           $timezone    The player's timezone
1069
     * @param  string|\TimeDate $joined      The date the player joined
1070
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1071
     * @return Player           An object representing the player that was just entered
1072
     */
1073 39
    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")
1074
    {
1075 39
        $joined = TimeDate::from($joined);
1076 39
        $last_login = TimeDate::from($last_login);
1077 39
        $timezone = ($timezone) ?: date_default_timezone_get();
1078
1079 39
        $player = self::create(array(
1080 39
            'bzid'        => $bzid,
1081 39
            'team'        => $team,
1082 39
            'username'    => $username,
1083 39
            'alias'       => self::generateAlias($username),
1084 39
            'status'      => $status,
1085 39
            'avatar'      => $avatar,
1086 39
            'description' => $description,
1087 39
            'country'     => $country,
1088 39
            'timezone'    => $timezone,
1089 39
            'joined'      => $joined->toMysql(),
1090 39
            'last_login'  => $last_login->toMysql(),
1091
        ));
1092
1093 39
        $player->addRole($role_id);
1094 39
        $player->getIdenticon($player->getId());
1095 39
        $player->setUsername($username);
1096
1097 39
        return $player;
1098
    }
1099
1100
    /**
1101
     * Determine if a player exists in the database
1102
     * @param  int  $bzid The player's bzid
1103
     * @return bool Whether the player exists in the database
1104
     */
1105
    public static function playerBZIDExists($bzid)
1106
    {
1107
        return self::getFromBZID($bzid)->isValid();
1108
    }
1109
1110
    /**
1111
     * Change a player's callsign and add it to the database if it does not
1112
     * exist as a past callsign
1113
     *
1114
     * @param  string $username The new username of the player
1115
     * @return self
1116
     */
1117 39
    public function setUsername($username)
1118
    {
1119
        // The player's username was just fetched from BzDB, it's definitely not
1120
        // outdated
1121 39
        $this->setOutdated(false);
1122
1123
        // Players who have this player's username are considered outdated
1124 39
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1125
1126 39
        if ($username === $this->name) {
1127
            // The player's username hasn't changed, no need to do anything
1128 1
            return $this;
1129
        }
1130
1131
        // Players who used to have our player's username are not outdated anymore,
1132
        // unless they are more than one.
1133
        // Even though we are sure that the old and new usernames are not equal,
1134
        // MySQL makes a different type of string equality tests, which is why we
1135
        // also check IDs to make sure not to affect our own player's outdatedness.
1136 38
        $this->db->execute("
1137 38
            UPDATE {$this->table} SET outdated =
1138 38
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1139
            WHERE username = ? AND id != ?",
1140 38
            array($this->name, $this->id, $this->name, $this->id));
1141
1142 38
        $this->updateProperty($this->name, 'username', $username);
1143 38
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1144 38
        $this->resetAlias();
1145
1146 38
        return $this;
1147
    }
1148
1149
    /**
1150
     * Alphabetical order function for use in usort (case-insensitive)
1151
     * @return Closure The sort function
1152
     */
1153
    public static function getAlphabeticalSort()
1154
    {
1155 1
        return function (Player $a, Player $b) {
1156 1
            return strcasecmp($a->getUsername(), $b->getUsername());
1157 1
        };
1158
    }
1159
1160
    /**
1161
     * {@inheritdoc}
1162
     * @todo Add a constraint that does this automatically
1163
     */
1164 39
    public function wipe()
1165
    {
1166 39
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1167
1168 39
        parent::wipe();
1169 39
    }
1170
1171
    /**
1172
     * Find whether the player can delete a model
1173
     *
1174
     * @param  PermissionModel $model       The model that will be seen
1175
     * @param  bool         $showDeleted Whether to show deleted models to admins
1176
     * @return bool
1177
     */
1178 1
    public function canSee($model, $showDeleted = false)
1179
    {
1180 1
        return $model->canBeSeenBy($this, $showDeleted);
1181
    }
1182
1183
    /**
1184
     * Find whether the player can delete a model
1185
     *
1186
     * @param  PermissionModel $model The model that will be deleted
1187
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1188
     *                                to soft-delete ones
1189
     * @return bool
1190
     */
1191 1
    public function canDelete($model, $hard = false)
1192
    {
1193 1
        if ($hard) {
1194
            return $model->canBeHardDeletedBy($this);
1195
        } else {
1196 1
            return $model->canBeSoftDeletedBy($this);
1197
        }
1198
    }
1199
1200
    /**
1201
     * Find whether the player can create a model
1202
     *
1203
     * @param  string  $modelName The PHP class identifier of the model type
1204
     * @return bool
1205
     */
1206 1
    public function canCreate($modelName)
1207
    {
1208 1
        return $modelName::canBeCreatedBy($this);
1209
    }
1210
1211
    /**
1212
     * Find whether the player can edit a model
1213
     *
1214
     * @param  PermissionModel $model The model which will be edited
1215
     * @return bool
1216
     */
1217 1
    public function canEdit($model)
1218
    {
1219 1
        return $model->canBeEditedBy($this);
1220
    }
1221
}
1222