Completed
Pull Request — master (#51)
by Konstantinos
04:39
created

Player::getMatchAverageCaps()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 3.6875

Importance

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

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
916
        );
917
918
        return $query[0]['sum']/$count;
919
    }
920
921
    /**
922
     * Show the number of messages the user hasn't read yet
923
     * @return int
924
     */
925
    public function countUnreadMessages()
926
    {
927
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
928
            $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...
929
        );
930
    }
931
932 39
    /**
933
     * Get all of the members belonging to a team
934 39
     * @param  int      $teamID The ID of the team to fetch the members of
935 39
     * @return Player[] An array of Player objects of the team members
936 39
     */
937
    public static function getTeamMembers($teamID)
938 39
    {
939 39
        return self::arrayIdToModel(
940 39
            self::fetchIds("WHERE team = ?", array($teamID))
941 39
        );
942 39
    }
943 39
944 39
    /**
945 39
     * {@inheritdoc}
946 39
     */
947 39
    public static function getActiveStatuses()
948 39
    {
949 39
        return array('active', 'reported', 'test');
950
    }
951
952 39
    /**
953 39
     * {@inheritdoc}
954 39
     */
955
    public static function getEagerColumns()
956 39
    {
957
        return 'id,bzid,team,username,alias,status,avatar,country';
958
    }
959
960
    /**
961
     * {@inheritdoc}
962
     */
963
    public static function getLazyColumns()
964
    {
965
        return 'email,verified,receives,confirm_code,outdated,description,timezone,joined,last_login,admin_notes';
966
    }
967
968
    /**
969
     * Get a query builder for players
970
     * @return QueryBuilder
971
     */
972 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...
973
    {
974
        return new QueryBuilder('Player', array(
975
            'columns' => array(
976 39
                'name'     => 'username',
977
                'team'     => 'team',
978
                'outdated' => 'outdated',
979
                'status'   => 'status'
980 39
            ),
981
            'name' => 'name',
982
        ));
983 39
    }
984
985 39
    /**
986
     * Enter a new player to the database
987 1
     * @param  int              $bzid        The player's bzid
988
     * @param  string           $username    The player's username
989
     * @param  int              $team        The player's team
990
     * @param  string           $status      The player's status
991
     * @param  int              $role_id     The player's role when they are first created
992
     * @param  string           $avatar      The player's profile avatar
993
     * @param  string           $description The player's profile description
994
     * @param  int              $country     The player's country
995 38
     * @param  string           $timezone    The player's timezone
996 38
     * @param  string|\TimeDate $joined      The date the player joined
997 38
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
998 38
     * @return Player           An object representing the player that was just entered
999 38
     */
1000
    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")
1001 38
    {
1002 38
        $joined = TimeDate::from($joined);
1003 38
        $last_login = TimeDate::from($last_login);
1004
        $timezone = ($timezone) ?: date_default_timezone_get();
1005 38
1006
        $player = self::create(array(
1007
            'bzid'        => $bzid,
1008
            'team'        => $team,
1009
            'username'    => $username,
1010
            'alias'       => self::generateAlias($username),
1011
            'status'      => $status,
1012
            'avatar'      => $avatar,
1013
            'description' => $description,
1014 1
            'country'     => $country,
1015 1
            'timezone'    => $timezone,
1016 1
            'joined'      => $joined->toMysql(),
1017
            'last_login'  => $last_login->toMysql(),
1018
        ));
1019
1020
        $player->addRole($role_id);
1021
        $player->getIdenticon($player->getId());
1022
        $player->setUsername($username);
1023 39
1024
        return $player;
1025 39
    }
1026
1027 39
    /**
1028 39
     * Determine if a player exists in the database
1029
     * @param  int  $bzid The player's bzid
1030
     * @return bool Whether the player exists in the database
1031
     */
1032
    public static function playerBZIDExists($bzid)
1033
    {
1034
        return self::getFromBZID($bzid)->isValid();
1035
    }
1036
1037 1
    /**
1038
     * Change a player's callsign and add it to the database if it does not
1039 1
     * exist as a past callsign
1040
     *
1041
     * @param  string $username The new username of the player
1042
     * @return self
1043
     */
1044
    public function setUsername($username)
1045
    {
1046
        // The player's username was just fetched from BzDB, it's definitely not
1047
        // outdated
1048
        $this->setOutdated(false);
1049
1050 1
        // Players who have this player's username are considered outdated
1051
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1052 1
1053
        if ($username === $this->name) {
1054
            // The player's username hasn't changed, no need to do anything
1055 1
            return $this;
1056
        }
1057
1058
        // Players who used to have our player's username are not outdated anymore,
1059
        // unless they are more than one.
1060
        // Even though we are sure that the old and new usernames are not equal,
1061
        // MySQL makes a different type of string equality tests, which is why we
1062
        // also check IDs to make sure not to affect our own player's outdatedness.
1063
        $this->db->execute("
1064
            UPDATE {$this->table} SET outdated =
1065 1
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1066
            WHERE username = ? AND id != ?",
1067 1
            array($this->name, $this->id, $this->name, $this->id));
1068
1069
        $this->updateProperty($this->name, 'username', $username);
1070
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1071
        $this->resetAlias();
1072
1073
        return $this;
1074
    }
1075
1076 1
    /**
1077
     * Alphabetical order function for use in usort (case-insensitive)
1078 1
     * @return Closure The sort function
1079
     */
1080
    public static function getAlphabeticalSort()
1081
    {
1082
        return function (Player $a, Player $b) {
1083
            return strcasecmp($a->getUsername(), $b->getUsername());
1084
        };
1085
    }
1086
1087
    /**
1088
     * {@inheritdoc}
1089
     * @todo Add a constraint that does this automatically
1090
     */
1091
    public function wipe()
1092
    {
1093
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1094
1095
        parent::wipe();
1096
    }
1097
1098
    /**
1099
     * Find whether the player can delete a model
1100
     *
1101
     * @param  PermissionModel $model       The model that will be seen
1102
     * @param  bool         $showDeleted Whether to show deleted models to admins
1103
     * @return bool
1104
     */
1105
    public function canSee($model, $showDeleted = false)
1106
    {
1107
        return $model->canBeSeenBy($this, $showDeleted);
1108
    }
1109
1110
    /**
1111
     * Find whether the player can delete a model
1112
     *
1113
     * @param  PermissionModel $model The model that will be deleted
1114
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1115
     *                                to soft-delete ones
1116
     * @return bool
1117
     */
1118
    public function canDelete($model, $hard = false)
1119
    {
1120
        if ($hard) {
1121
            return $model->canBeHardDeletedBy($this);
1122
        } else {
1123
            return $model->canBeSoftDeletedBy($this);
1124
        }
1125
    }
1126
1127
    /**
1128
     * Find whether the player can create a model
1129
     *
1130
     * @param  string  $modelName The PHP class identifier of the model type
1131
     * @return bool
1132
     */
1133
    public function canCreate($modelName)
1134
    {
1135
        return $modelName::canBeCreatedBy($this);
1136
    }
1137
1138
    /**
1139
     * Find whether the player can edit a model
1140
     *
1141
     * @param  PermissionModel $model The model which will be edited
1142
     * @return bool
1143
     */
1144
    public function canEdit($model)
1145
    {
1146
        return $model->canBeEditedBy($this);
1147
    }
1148
}
1149