Completed
Push — v0_9 ( 4c1f3c...66e765 )
by Vladimir
08:25 queued 03:00
created

Player::setLastMatch()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 2
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 1
        }
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 39
        }
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 1
    public function canReceive($type)
310
    {
311 1
        $this->lazyLoad();
312
313 1
        if (!$this->email || !$this->isVerified()) {
314
            // Unverified e-mail means the user will receive nothing
315 1
            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 39
        }
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 the callsign of the player is outdated
471
     *
472
     * Returns true if this player has probably changed their callsign, making
473
     * the current username stored in the database obsolete
474
     *
475
     * @return bool Whether or not the player is disabled
476
     */
477
    public function isOutdated()
478
    {
479
        $this->lazyLoad();
480
481
        return $this->outdated;
482
    }
483
484
    /**
485
     * Check if a player's account has been disabled
486
     *
487
     * @return bool Whether or not the player is disabled
488
     */
489
    public function isDisabled()
490
    {
491
        return $this->status == "disabled";
492
    }
493
494
    /**
495
     * Check if everyone can log in as this user on a test environment
496
     *
497
     * @return bool
498
     */
499 1
    public function isTestUser()
500
    {
501 1
        return $this->status == "test";
502
    }
503
504
    /**
505
     * Check if a player is teamless
506
     *
507
     * @return bool True if the player is teamless
508
     */
509 18
    public function isTeamless()
510
    {
511 18
        return empty($this->team);
512
    }
513
514
    /**
515
     * Mark a player's account as banned
516
     */
517 1
    public function markAsBanned()
518
    {
519 1
        if ($this->status != 'active') {
520
            return $this;
521
        }
522
523 1
        return $this->updateProperty($this->status, "status", "banned");
524
    }
525
526
    /**
527
     * Mark a player's account as unbanned
528
     */
529
    public function markAsUnbanned()
530
    {
531
        if ($this->status != 'banned') {
532
            return $this;
533
        }
534
535
        return $this->updateProperty($this->status, "status", "active");
536
    }
537
538
    /**
539
     * Find out if a player is banned
540
     *
541
     * @return bool
542
     */
543 2
    public function isBanned()
544
    {
545 2
        return Ban::getBan($this->id) !== null;
546
    }
547
548
    /**
549
     * Get the ban of the player
550
     *
551
     * This method performs a load of all the lazy parameters of the Player
552
     *
553
     * @return Ban|null The current ban of the player, or null if the player is
554
     *                  is not banned
555
     */
556
    public function getBan()
557
    {
558
        $this->lazyLoad();
559
560
        return $this->ban;
561
    }
562
563
    /**
564
     * Remove a player from a role
565
     *
566
     * @param int $role_id The role ID to add or remove
567
     *
568
     * @return bool Whether the operation was successful or not
569
     */
570
    public function removeRole($role_id)
571
    {
572
        $status = $this->modifyRole($role_id, "remove");
573
        $this->refresh();
574
575
        return $status;
576
    }
577
578
    /**
579
     * Set the player's email address and reset their verification status
580
     * @param string $email The address
581
     */
582
    public function setEmailAddress($email)
583
    {
584
        $this->lazyLoad();
585
586
        if ($this->email == $email) {
587
            // The e-mail hasn't changed, don't do anything
588
            return;
589
        }
590
591
        $this->setVerified(false);
592
        $this->generateNewConfirmCode();
593
594
        $this->updateProperty($this->email, 'email', $email);
595
    }
596
597
    /**
598
     * Set whether the player has verified their e-mail address
599
     *
600
     * @param  bool $verified Whether the player is verified or not
601
     * @return self
602
     */
603
    public function setVerified($verified)
604
    {
605
        $this->lazyLoad();
606
607
        if ($verified) {
608
            $this->setConfirmCode(null);
609
        }
610
611
        return $this->updateProperty($this->verified, 'verified', $verified);
612
    }
613
614
    /**
615
     * Generate a new random confirmation token for e-mail address verification
616
     *
617
     * @return self
618
     */
619
    public function generateNewConfirmCode()
620
    {
621
        $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...
622
        $random = $generator->nextBytes(16);
623
624
        return $this->setConfirmCode(bin2hex($random));
625
    }
626
627
    /**
628
     * Set the confirmation token for e-mail address verification
629
     *
630
     * @param  string $code The confirmation code
631
     * @return self
632
     */
633
    private function setConfirmCode($code)
634
    {
635
        $this->lazyLoad();
636
637
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
638
    }
639
640
    /**
641
     * Set what kind of events the player should be e-mailed about
642
     *
643
     * @param  string $receives The type of notification
644
     * @return self
645
     */
646
    public function setReceives($receives)
647
    {
648
        $this->lazyLoad();
649
650
        return $this->updateProperty($this->receives, 'receives', $receives);
651
    }
652
653
    /**
654
     * Set whether the callsign of the player is outdated
655
     *
656
     * @param  bool $outdated Whether the callsign is outdated
657
     * @return self
658
     */
659 39
    public function setOutdated($outdated)
660
    {
661 39
        $this->lazyLoad();
662
663 39
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
664
    }
665
666
    /**
667
     * Set the player's description
668
     * @param string $description The description
669
     */
670
    public function setDescription($description)
671
    {
672
        $this->updateProperty($this->description, "description", $description);
673
    }
674
675
    /**
676
     * Set the player's timezone
677
     * @param string $timezone The timezone
678
     */
679
    public function setTimezone($timezone)
680
    {
681
        $this->updateProperty($this->timezone, "timezone", $timezone);
682
    }
683
684
    /**
685
     * Set the player's team
686
     * @param int $team The team's ID
687
     */
688 18
    public function setTeam($team)
689
    {
690 18
        $this->updateProperty($this->team, "team", $team);
691 18
    }
692
693
    /**
694
     * Set the match the player last participated in
695
     *
696
     * @param int $match The match's ID
697
     */
698
    public function setLastMatch($match)
699
    {
700
        $this->updateProperty($this->last_match, 'last_match', $match);
701
    }
702
703
    /**
704
     * Set the player's status
705
     * @param string $status The new status
706
     */
707
    public function setStatus($status)
708
    {
709
        $this->updateProperty($this->status, 'status', $status);
710
    }
711
712
    /**
713
     * Set the player's admin notes
714
     * @param  string $admin_notes The new admin notes
715
     * @return self
716
     */
717
    public function setAdminNotes($admin_notes)
718
    {
719
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
720
    }
721
722
    /**
723
     * Set the player's country
724
     * @param  int   $country The ID of the new country
725
     * @return self
726
     */
727
    public function setCountry($country)
728
    {
729
        return $this->updateProperty($this->country, 'country', $country);
730
    }
731
732
    /**
733
     * Updates this player's last login
734 1
     */
735
    public function updateLastLogin()
736 1
    {
737
        $this->update("last_login", TimeDate::now()->toMysql());
738
    }
739
740
    /**
741
     * Get the player's username
742
     * @return string The username
743 1
     */
744
    public function getUsername()
745 1
    {
746
        return $this->name;
747
    }
748
749
    /**
750
     * Get the player's username, safe for use in your HTML
751
     * @return string The username
752
     */
753
    public function getEscapedUsername()
754
    {
755
        return $this->getEscapedName();
756
    }
757
758
    /**
759
     * Alias for Player::setUsername()
760
     *
761
     * @param  string $username The new username
762
     * @return self
763
     */
764
    public function setName($username)
765
    {
766
        return $this->setUsername($username);
767
    }
768
769
    /**
770
     * Mark all the unread messages of a player as read
771
     *
772
     * @return void
773
     */
774
    public function markMessagesAsRead()
775
    {
776
        $this->db->execute(
777
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
778
            array($this->id)
779
        );
780
    }
781
782
    /**
783
     * Set the roles of a user
784
     *
785
     * @todo   Is it worth making this faster?
786
     * @param  Role[] $roles The new roles of the user
787
     * @return self
788
     */
789
    public function setRoles($roles)
790
    {
791
        $this->lazyLoad();
792
793
        $oldRoles = Role::mapToIds($this->roles);
794
        $this->roles = $roles;
795
        $roleIds = Role::mapToIds($roles);
796
797
        $newRoles     = array_diff($roleIds, $oldRoles);
798
        $removedRoles = array_diff($oldRoles, $roleIds);
799
800
        foreach ($newRoles as $role) {
801
            $this->modifyRole($role, 'add');
802
        }
803
804
        foreach ($removedRoles as $role) {
805
            $this->modifyRole($role, 'remove');
806
        }
807
808
        $this->refresh();
809
810
        return $this;
811 39
    }
812
813 39
    /**
814
     * Give or remove a role to/form a player
815 39
     *
816 39
     * @param int    $role_id The role ID to add or remove
817 39
     * @param string $action  Whether to "add" or "remove" a role for a player
818 39
     *
819
     * @return bool Whether the operation was successful or not
820
     */
821
    private function modifyRole($role_id, $action)
822
    {
823
        $role = Role::get($role_id);
824 39
825
        if ($role->isValid()) {
826
            if ($action == "add") {
827
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
828
            } elseif ($action == "remove") {
829
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
830
            } else {
831
                throw new Exception("Unrecognized role action");
832
            }
833
834
            return true;
835
        }
836 1
837
        return false;
838 1
    }
839
840
    /**
841
     * Given a player's BZID, get a player object
842
     *
843
     * @param  int    $bzid The player's BZID
844
     * @return Player
845
     */
846
    public static function getFromBZID($bzid)
847 1
    {
848
        return self::get(self::fetchIdFrom($bzid, "bzid"));
849 1
    }
850
851 1
    /**
852
     * Get a single player by their username
853
     *
854
     * @param  string $username The username to look for
855
     * @return Player
856
     */
857
    public static function getFromUsername($username)
858
    {
859
        $player = static::get(self::fetchIdFrom($username, 'username'));
860
861
        return $player->inject('name', $username);
862
    }
863
864
    /**
865
     * Get all the players in the database that have an active status
866
     * @return Player[] An array of player BZIDs
867
     */
868
    public static function getPlayers()
869 1
    {
870
        return self::arrayIdToModel(
871 1
            self::fetchIdsFrom("status", array("active", "test"), false)
872
        );
873
    }
874
875
    /**
876
     * Show the number of notifications the user hasn't read yet
877
     * @return int
878
     */
879
    public function countUnreadNotifications()
880
    {
881
        return Notification::countUnreadNotifications($this->id);
882
    }
883
884
    /**
885
     * Count the number of matches a player has participated in
886
     * @return int
887
     */
888
    public function getMatchCount()
889
    {
890
        if ($this->cachedMatchCount === null) {
891
            $this->cachedMatchCount = Match::getQueryBuilder()
892
                ->active()
893
                ->with($this)
894
                ->count();
895
        }
896
897
        return $this->cachedMatchCount;
898
    }
899
900
    /**
901
     * Get the (victory/total matches) ratio of the player
902
     * @return float
903
     */
904
    public function getMatchWinRatio()
905
    {
906
        $count = $this->getMatchCount();
907
908
        if ($count == 0) {
909
            return 0;
910
        }
911
912
        $wins = Match::getQueryBuilder()
913
            ->active()
914
            ->with($this, 'win')
915
            ->count();
916
917
        return $wins / $count;
918
    }
919
920
    /**
921
     * Get the (total caps made by team/total matches) ratio of the player
922
     * @return float
923
     */
924
    public function getMatchAverageCaps()
925
    {
926
        $count = $this->getMatchCount();
927
928
        if ($count == 0) {
929
            return 0;
930
        }
931
932
        // Get the sum of team A points if the player was in team A, team B
933
        // points if the player was in team B, and their average if the player
934
        // was on both teams for some reason
935
        $query = $this->db->query(
936
            "SELECT SUM(
937
                IF(
938
                    FIND_IN_SET(?, team_a_players) AND FIND_IN_SET(?, team_b_players),
939
                    (team_a_points+team_b_points)/2,
940
                    IF(FIND_IN_SET(?, team_a_players), team_a_points, team_b_points)
941
                )
942
            ) AS sum FROM matches WHERE status='entered' AND (FIND_IN_SET(?, team_a_players) OR FIND_IN_SET(?, team_b_players))",
943
            array_fill(0, 5, $this->id)
944
        );
945
946
        return $query[0]['sum'] / $count;
947
    }
948
949
    /**
950
     * Get the match activity in matches per day for a player
951
     *
952
     * @return float
953
     */
954
    public function getMatchActivity()
955
    {
956
        $activity = 0.0;
957
958
        $matches = Match::getQueryBuilder()
959
            ->active()
960
            ->with($this)
961
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
962
            ->getModels($fast = true);
963
964
        foreach ($matches as $match) {
965 1
            $activity += $match->getActivity();
966
        }
967 1
968 1
        return $activity;
969 1
    }
970
971
    /**
972
     * Show the number of messages the user hasn't read yet
973
     * @return int
974
     */
975
    public function countUnreadMessages()
976
    {
977 2
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
978
            $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...
979 2
        );
980 2
    }
981 2
982
    /**
983
     * Get all of the members belonging to a team
984
     * @param  int      $teamID The ID of the team to fetch the members of
985
     * @return Player[] An array of Player objects of the team members
986
     */
987 1
    public static function getTeamMembers($teamID)
988
    {
989 1
        return self::arrayIdToModel(
990
            self::fetchIds("WHERE team = ?", array($teamID))
991
        );
992
    }
993
994
    /**
995 39
     * {@inheritdoc}
996
     */
997 39
    public static function getActiveStatuses()
998
    {
999
        return array('active', 'reported', 'test');
1000
    }
1001
1002
    /**
1003 39
     * {@inheritdoc}
1004
     */
1005 39
    public static function getEagerColumns()
1006
    {
1007
        return 'id,bzid,team,username,alias,status,avatar,country';
1008
    }
1009
1010
    /**
1011
     * {@inheritdoc}
1012
     */
1013
    public static function getLazyColumns()
1014
    {
1015
        return 'email,verified,receives,confirm_code,outdated,description,timezone,joined,last_login,last_match,admin_notes';
1016
    }
1017
1018
    /**
1019
     * Get a query builder for players
1020
     * @return QueryBuilder
1021
     */
1022 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...
1023
    {
1024
        return new QueryBuilder('Player', array(
1025
            'columns' => array(
1026
                'name'     => 'username',
1027
                'team'     => 'team',
1028
                'outdated' => 'outdated',
1029
                'status'   => 'status'
1030
            ),
1031
            'name' => 'name',
1032
        ));
1033
    }
1034
1035
    /**
1036
     * Enter a new player to the database
1037
     * @param  int              $bzid        The player's bzid
1038
     * @param  string           $username    The player's username
1039
     * @param  int              $team        The player's team
1040 39
     * @param  string           $status      The player's status
1041
     * @param  int              $role_id     The player's role when they are first created
1042 39
     * @param  string           $avatar      The player's profile avatar
1043 39
     * @param  string           $description The player's profile description
1044 39
     * @param  int              $country     The player's country
1045
     * @param  string           $timezone    The player's timezone
1046 39
     * @param  string|\TimeDate $joined      The date the player joined
1047 39
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1048 39
     * @return Player           An object representing the player that was just entered
1049 39
     */
1050 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")
1051 39
    {
1052 39
        $joined = TimeDate::from($joined);
1053 39
        $last_login = TimeDate::from($last_login);
1054 39
        $timezone = ($timezone) ?: date_default_timezone_get();
1055 39
1056 39
        $player = self::create(array(
1057 39
            'bzid'        => $bzid,
1058 39
            'team'        => $team,
1059
            'username'    => $username,
1060 39
            'alias'       => self::generateAlias($username),
1061 39
            'status'      => $status,
1062 39
            'avatar'      => $avatar,
1063
            'description' => $description,
1064 39
            'country'     => $country,
1065
            'timezone'    => $timezone,
1066
            'joined'      => $joined->toMysql(),
1067
            'last_login'  => $last_login->toMysql(),
1068
        ));
1069
1070
        $player->addRole($role_id);
1071
        $player->getIdenticon($player->getId());
1072
        $player->setUsername($username);
1073
1074
        return $player;
1075
    }
1076
1077
    /**
1078
     * Determine if a player exists in the database
1079
     * @param  int  $bzid The player's bzid
1080
     * @return bool Whether the player exists in the database
1081
     */
1082
    public static function playerBZIDExists($bzid)
1083
    {
1084 39
        return self::getFromBZID($bzid)->isValid();
1085
    }
1086
1087
    /**
1088 39
     * Change a player's callsign and add it to the database if it does not
1089
     * exist as a past callsign
1090
     *
1091 39
     * @param  string $username The new username of the player
1092
     * @return self
1093 39
     */
1094
    public function setUsername($username)
1095 1
    {
1096
        // The player's username was just fetched from BzDB, it's definitely not
1097
        // outdated
1098
        $this->setOutdated(false);
1099
1100
        // Players who have this player's username are considered outdated
1101
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1102
1103 38
        if ($username === $this->name) {
1104 38
            // The player's username hasn't changed, no need to do anything
1105 38
            return $this;
1106 38
        }
1107 38
1108
        // Players who used to have our player's username are not outdated anymore,
1109 38
        // unless they are more than one.
1110 38
        // Even though we are sure that the old and new usernames are not equal,
1111 38
        // MySQL makes a different type of string equality tests, which is why we
1112
        // also check IDs to make sure not to affect our own player's outdatedness.
1113 38
        $this->db->execute("
1114
            UPDATE {$this->table} SET outdated =
1115
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1116
            WHERE username = ? AND id != ?",
1117
            array($this->name, $this->id, $this->name, $this->id));
1118
1119
        $this->updateProperty($this->name, 'username', $username);
1120
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1121
        $this->resetAlias();
1122 1
1123 1
        return $this;
1124 1
    }
1125
1126
    /**
1127
     * Alphabetical order function for use in usort (case-insensitive)
1128
     * @return Closure The sort function
1129
     */
1130
    public static function getAlphabeticalSort()
1131 39
    {
1132
        return function (Player $a, Player $b) {
1133 39
            return strcasecmp($a->getUsername(), $b->getUsername());
1134
        };
1135 39
    }
1136 39
1137
    /**
1138
     * {@inheritdoc}
1139
     * @todo Add a constraint that does this automatically
1140
     */
1141
    public function wipe()
1142
    {
1143
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1144
1145 1
        parent::wipe();
1146
    }
1147 1
1148
    /**
1149
     * Find whether the player can delete a model
1150
     *
1151
     * @param  PermissionModel $model       The model that will be seen
1152
     * @param  bool         $showDeleted Whether to show deleted models to admins
1153
     * @return bool
1154
     */
1155
    public function canSee($model, $showDeleted = false)
1156
    {
1157
        return $model->canBeSeenBy($this, $showDeleted);
1158 1
    }
1159
1160 1
    /**
1161
     * Find whether the player can delete a model
1162
     *
1163 1
     * @param  PermissionModel $model The model that will be deleted
1164
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1165
     *                                to soft-delete ones
1166
     * @return bool
1167
     */
1168
    public function canDelete($model, $hard = false)
1169
    {
1170
        if ($hard) {
1171
            return $model->canBeHardDeletedBy($this);
1172
        } else {
1173 1
            return $model->canBeSoftDeletedBy($this);
1174
        }
1175 1
    }
1176
1177
    /**
1178
     * Find whether the player can create a model
1179
     *
1180
     * @param  string  $modelName The PHP class identifier of the model type
1181
     * @return bool
1182
     */
1183
    public function canCreate($modelName)
1184 1
    {
1185
        return $modelName::canBeCreatedBy($this);
1186 1
    }
1187
1188
    /**
1189
     * Find whether the player can edit a model
1190
     *
1191
     * @param  PermissionModel $model The model which will be edited
1192
     * @return bool
1193
     */
1194
    public function canEdit($model)
1195
    {
1196
        return $model->canBeEditedBy($this);
1197
    }
1198
}
1199