Completed
Push — fm-support ( 0de470...22a3ee )
by Konstantinos
04:53
created

Player::getMatchActivity()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 16
ccs 0
cts 10
cp 0
rs 9.4285
cc 2
eloc 10
nc 2
nop 0
crap 6
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
    const EDIT_PERMISSION = Permission::EDIT_USER;
152
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
153
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 39
    protected function assignResult($player)
159
    {
160 39
        $this->bzid = $player['bzid'];
161 39
        $this->name = $player['username'];
162 39
        $this->alias = $player['alias'];
163 39
        $this->team = $player['team'];
164 39
        $this->status = $player['status'];
165 39
        $this->avatar = $player['avatar'];
166 39
        $this->country = $player['country'];
167 39
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172 39
    protected function assignLazyResult($player)
173
    {
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 39
        $this->outdated = $player['outdated'];
179 39
        $this->description = $player['description'];
180 39
        $this->timezone = $player['timezone'];
181 39
        $this->joined = TimeDate::fromMysql($player['joined']);
182 39
        $this->last_login = TimeDate::fromMysql($player['last_login']);
183 39
        $this->admin_notes = $player['admin_notes'];
184 39
        $this->ban = Ban::getBan($this->id);
185
186 39
        $this->updateUserPermissions();
187 39
    }
188
189
    /**
190
     * Add a player a new role
191
     *
192
     * @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
     */
196 39
    public function addRole($role_id)
197
    {
198 39
        if ($role_id instanceof Role) {
199 1
            $role_id = $role_id->getId();
200
        }
201
202 39
        $this->lazyLoad();
203
204
        // Make sure the player doesn't already have the role
205 39
        foreach ($this->roles as $playerRole) {
206 14
            if ($playerRole->getId() == $role_id) {
207 14
                return false;
208
            }
209
        }
210
211 39
        $status = $this->modifyRole($role_id, "add");
212 39
        $this->refresh();
213
214 39
        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
    }
236
237
    /**
238
     * Get the country a player belongs to
239
     *
240
     * @return Country The country belongs to
241
     */
242 1
    public function getCountry()
243
    {
244 1
        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
    /**
296
     * Finds out whether the specified player wants and can receive an e-mail
297
     * message
298
     *
299
     * @param  string  $type
300
     * @return bool `true` if the player should be sent an e-mail
301
     */
302 1
    public function canReceive($type)
303
    {
304 1
        $this->lazyLoad();
305
306 1
        if (!$this->email || !$this->isVerified()) {
307
            // Unverified e-mail means the user will receive nothing
308 1
            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
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
387
    }
388
389
    /**
390
     * Get the player's team
391
     * @return Team The object representing the team
392
     */
393 2
    public function getTeam()
394
    {
395 2
        return Team::get($this->team);
396
    }
397
398
    /**
399
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
400
     * @return string The timezone
401
     */
402 1
    public function getTimezone()
403
    {
404 1
        $this->lazyLoad();
405
406 1
        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
417
        return $this->roles;
418
    }
419
420
    /**
421
     * Rebuild the list of permissions a user has been granted
422
     */
423 39
    private function updateUserPermissions()
424
    {
425 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...
426 39
        $this->permissions = array();
427
428 39
        foreach ($this->roles as $role) {
429 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...
430
        }
431 39
    }
432
433
    /**
434
     * Check if a player has a specific permission
435
     *
436
     * @param string|null $permission The permission to check for
437
     *
438
     * @return bool Whether or not the player has the permission
439
     */
440 2
    public function hasPermission($permission)
441
    {
442 2
        if ($permission === null) {
443 1
            return false;
444
        }
445
446 2
        $this->lazyLoad();
447
448 2
        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
    }
475
476
    /**
477
     * Check if everyone can log in as this user on a test environment
478
     *
479
     * @return bool
480
     */
481 1
    public function isTestUser()
482
    {
483 1
        return $this->status == "test";
484
    }
485
486
    /**
487
     * Check if a player is teamless
488
     *
489
     * @return bool True if the player is teamless
490
     */
491 18
    public function isTeamless()
492
    {
493 18
        return empty($this->team);
494
    }
495
496
    /**
497
     * Mark a player's account as banned
498
     */
499 1
    public function markAsBanned()
500
    {
501 1
        if ($this->status != 'active') {
502
            return $this;
503
        }
504
505 1
        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
    }
519
520
    /**
521
     * Find out if a player is banned
522
     *
523
     * @return bool
524
     */
525 2
    public function isBanned()
526
    {
527 2
        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
    /**
636
     * Set whether the callsign of the player is outdated
637
     *
638
     * @param  bool $outdated Whether the callsign is outdated
639
     * @return self
640
     */
641 39
    public function setOutdated($outdated)
642
    {
643 39
        $this->lazyLoad();
644
645 39
        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
    /**
667
     * Set the player's team
668
     * @param int $team The team's ID
669
     */
670 18
    public function setTeam($team)
671
    {
672 18
        $this->updateProperty($this->team, "team", $team);
673 18
    }
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
     * Get the player's username
714
     * @return string The username
715
     */
716 1
    public function getUsername()
717
    {
718 1
        return $this->name;
719
    }
720
721
    /**
722
     * Get the player's username, safe for use in your HTML
723
     * @return string The username
724
     */
725 1
    public function getEscapedUsername()
726
    {
727 1
        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
     *
791
     * @return bool Whether the operation was successful or not
792
     */
793 39
    private function modifyRole($role_id, $action)
794
    {
795 39
        $role = Role::get($role_id);
796
797 39
        if ($role->isValid()) {
798 39
            if ($action == "add") {
799 39
                $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
                throw new Exception("Unrecognized role action");
804
            }
805
806 39
            return true;
807
        }
808
809
        return false;
810
    }
811
812
    /**
813
     * Given a player's BZID, get a player object
814
     *
815
     * @param  int    $bzid The player's BZID
816
     * @return Player
817
     */
818 1
    public static function getFromBZID($bzid)
819
    {
820 1
        return self::get(self::fetchIdFrom($bzid, "bzid"));
821
    }
822
823
    /**
824
     * Get a single player by their username
825
     *
826
     * @param  string $username The username to look for
827
     * @return Player
828
     */
829 1
    public static function getFromUsername($username)
830
    {
831 1
        $player = static::get(self::fetchIdFrom($username, 'username'));
832
833 1
        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
     * Show the number of notifications the user hasn't read yet
849
     * @return int
850
     */
851 1
    public function countUnreadNotifications()
852
    {
853 1
        return Notification::countUnreadNotifications($this->id);
854
    }
855
856
    /**
857
     * Count the number of matches a player has participated in
858
     * @return int
859
     */
860
    public function getMatchCount()
861
    {
862
        if ($this->cachedMatchCount === null) {
863
            $this->cachedMatchCount = Match::getQueryBuilder()
864
                ->active()
865
                ->with($this)
866
                ->count();
867
        }
868
869
        return $this->cachedMatchCount;
870
    }
871
872
    /**
873
     * Get the (victory/total matches) ratio of the player
874
     * @return float
875
     */
876
    public function getMatchWinRatio()
877
    {
878
        $count = $this->getMatchCount();
879
880
        if ($count == 0) {
881
            return 0;
882
        }
883
884
        $wins = Match::getQueryBuilder()
885
            ->active()
886
            ->with($this, 'win')
887
            ->count();
888
889
        return $wins/$count;
890
    }
891
892
    /**
893
     * Get the (total caps made by team/total matches) ratio of the player
894
     * @return float
895
     */
896
    public function getMatchAverageCaps()
897
    {
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
            array_fill(0, 5, $this->id)
916
        );
917
918
        return $query[0]['sum']/$count;
919
    }
920
921
    public function getMatchActivity()
922
    {
923
        $activity = 0;
924
925
        $matches = Match::getQueryBuilder()
926
            ->active()
927
            ->with($this)
928
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
929
            ->getModels($fast = true);
930
931
        foreach ($matches as $match) {
932
            $activity += $match->getActivity();
933
        }
934
935
        return $activity;
936
    }
937
938
    /**
939
     * Show the number of messages the user hasn't read yet
940
     * @return int
941
     */
942 1
    public function countUnreadMessages()
943
    {
944 1
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
945 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...
946
        );
947
    }
948
949
    /**
950
     * Get all of the members belonging to a team
951
     * @param  int      $teamID The ID of the team to fetch the members of
952
     * @return Player[] An array of Player objects of the team members
953
     */
954 2
    public static function getTeamMembers($teamID)
955
    {
956 2
        return self::arrayIdToModel(
957 2
            self::fetchIds("WHERE team = ?", array($teamID))
958
        );
959
    }
960
961
    /**
962
     * {@inheritdoc}
963
     */
964 1
    public static function getActiveStatuses()
965
    {
966 1
        return array('active', 'reported', 'test');
967
    }
968
969
    /**
970
     * {@inheritdoc}
971
     */
972 39
    public static function getEagerColumns()
973
    {
974 39
        return 'id,bzid,team,username,alias,status,avatar,country';
975
    }
976
977
    /**
978
     * {@inheritdoc}
979
     */
980 39
    public static function getLazyColumns()
981
    {
982 39
        return 'email,verified,receives,confirm_code,outdated,description,timezone,joined,last_login,admin_notes';
983
    }
984
985
    /**
986
     * Get a query builder for players
987
     * @return QueryBuilder
988
     */
989 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...
990
    {
991
        return new QueryBuilder('Player', array(
992
            'columns' => array(
993
                'name'     => 'username',
994
                'team'     => 'team',
995
                'outdated' => 'outdated',
996
                'status'   => 'status'
997
            ),
998
            'name' => 'name',
999
        ));
1000
    }
1001
1002
    /**
1003
     * Enter a new player to the database
1004
     * @param  int              $bzid        The player's bzid
1005
     * @param  string           $username    The player's username
1006
     * @param  int              $team        The player's team
1007
     * @param  string           $status      The player's status
1008
     * @param  int              $role_id     The player's role when they are first created
1009
     * @param  string           $avatar      The player's profile avatar
1010
     * @param  string           $description The player's profile description
1011
     * @param  int              $country     The player's country
1012
     * @param  string           $timezone    The player's timezone
1013
     * @param  string|\TimeDate $joined      The date the player joined
1014
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1015
     * @return Player           An object representing the player that was just entered
1016
     */
1017 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")
1018
    {
1019 39
        $joined = TimeDate::from($joined);
1020 39
        $last_login = TimeDate::from($last_login);
1021 39
        $timezone = ($timezone) ?: date_default_timezone_get();
1022
1023 39
        $player = self::create(array(
1024 39
            'bzid'        => $bzid,
1025 39
            'team'        => $team,
1026 39
            'username'    => $username,
1027 39
            'alias'       => self::generateAlias($username),
1028 39
            'status'      => $status,
1029 39
            'avatar'      => $avatar,
1030 39
            'description' => $description,
1031 39
            'country'     => $country,
1032 39
            'timezone'    => $timezone,
1033 39
            'joined'      => $joined->toMysql(),
1034 39
            'last_login'  => $last_login->toMysql(),
1035
        ));
1036
1037 39
        $player->addRole($role_id);
1038 39
        $player->getIdenticon($player->getId());
1039 39
        $player->setUsername($username);
1040
1041 39
        return $player;
1042
    }
1043
1044
    /**
1045
     * Determine if a player exists in the database
1046
     * @param  int  $bzid The player's bzid
1047
     * @return bool Whether the player exists in the database
1048
     */
1049
    public static function playerBZIDExists($bzid)
1050
    {
1051
        return self::getFromBZID($bzid)->isValid();
1052
    }
1053
1054
    /**
1055
     * Change a player's callsign and add it to the database if it does not
1056
     * exist as a past callsign
1057
     *
1058
     * @param  string $username The new username of the player
1059
     * @return self
1060
     */
1061 39
    public function setUsername($username)
1062
    {
1063
        // The player's username was just fetched from BzDB, it's definitely not
1064
        // outdated
1065 39
        $this->setOutdated(false);
1066
1067
        // Players who have this player's username are considered outdated
1068 39
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1069
1070 39
        if ($username === $this->name) {
1071
            // The player's username hasn't changed, no need to do anything
1072 1
            return $this;
1073
        }
1074
1075
        // Players who used to have our player's username are not outdated anymore,
1076
        // unless they are more than one.
1077
        // Even though we are sure that the old and new usernames are not equal,
1078
        // MySQL makes a different type of string equality tests, which is why we
1079
        // also check IDs to make sure not to affect our own player's outdatedness.
1080 38
        $this->db->execute("
1081 38
            UPDATE {$this->table} SET outdated =
1082 38
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1083 38
            WHERE username = ? AND id != ?",
1084 38
            array($this->name, $this->id, $this->name, $this->id));
1085
1086 38
        $this->updateProperty($this->name, 'username', $username);
1087 38
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1088 38
        $this->resetAlias();
1089
1090 38
        return $this;
1091
    }
1092
1093
    /**
1094
     * Alphabetical order function for use in usort (case-insensitive)
1095
     * @return Closure The sort function
1096
     */
1097
    public static function getAlphabeticalSort()
1098
    {
1099 1
        return function (Player $a, Player $b) {
1100 1
            return strcasecmp($a->getUsername(), $b->getUsername());
1101 1
        };
1102
    }
1103
1104
    /**
1105
     * {@inheritdoc}
1106
     * @todo Add a constraint that does this automatically
1107
     */
1108 39
    public function wipe()
1109
    {
1110 39
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1111
1112 39
        parent::wipe();
1113 39
    }
1114
1115
    /**
1116
     * Find whether the player can delete a model
1117
     *
1118
     * @param  PermissionModel $model       The model that will be seen
1119
     * @param  bool         $showDeleted Whether to show deleted models to admins
1120
     * @return bool
1121
     */
1122 1
    public function canSee($model, $showDeleted = false)
1123
    {
1124 1
        return $model->canBeSeenBy($this, $showDeleted);
1125
    }
1126
1127
    /**
1128
     * Find whether the player can delete a model
1129
     *
1130
     * @param  PermissionModel $model The model that will be deleted
1131
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1132
     *                                to soft-delete ones
1133
     * @return bool
1134
     */
1135 1
    public function canDelete($model, $hard = false)
1136
    {
1137 1
        if ($hard) {
1138
            return $model->canBeHardDeletedBy($this);
1139
        } else {
1140 1
            return $model->canBeSoftDeletedBy($this);
1141
        }
1142
    }
1143
1144
    /**
1145
     * Find whether the player can create a model
1146
     *
1147
     * @param  string  $modelName The PHP class identifier of the model type
1148
     * @return bool
1149
     */
1150 1
    public function canCreate($modelName)
1151
    {
1152 1
        return $modelName::canBeCreatedBy($this);
1153
    }
1154
1155
    /**
1156
     * Find whether the player can edit a model
1157
     *
1158
     * @param  PermissionModel $model The model which will be edited
1159
     * @return bool
1160
     */
1161 1
    public function canEdit($model)
1162
    {
1163 1
        return $model->canBeEditedBy($this);
1164
    }
1165
}
1166