Completed
Push — feature/pixie-port ( 8213a1...132a2b )
by Vladimir
13:51
created

Player::getAlphabeticalSort()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
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 Carbon\Carbon;
10
use Symfony\Component\Security\Core\Util\SecureRandom;
11
use Symfony\Component\Security\Core\Util\StringUtils;
12
13
/**
14
 * A league player
15
 * @package    BZiON\Models
16
 */
17
class Player extends AvatarModel implements NamedModel, DuplexUrlInterface, EloInterface
18
{
19
    /**
20
     * These are built-in roles that cannot be deleted via the web interface so we will be storing these values as
21
     * constant variables. Hopefully, a user won't be silly enough to delete them manually from the database.
22
     *
23
     * @TODO Deprecate these and use the Role constants
24
     */
25
    const DEVELOPER    = Role::DEVELOPER;
26
    const ADMIN        = Role::ADMINISTRATOR;
27
    const COP          = Role::COP;
28
    const REFEREE      = Role::REFEREE;
29
    const S_ADMIN      = Role::SYSADMIN;
30
    const PLAYER       = Role::PLAYER;
31
    const PLAYER_NO_PM = Role::PLAYER_NO_PM;
32
33
    /**
34
     * The bzid of the player
35
     * @var int
36
     */
37
    protected $bzid;
38
39
    /**
40
     * The id of the player's team
41
     * @var int
42
     */
43
    protected $team;
44
45
    /**
46
     * The player's e-mail address
47
     * @var string
48
     */
49
    protected $email;
50
51
    /**
52
     * Whether the player has verified their e-mail address
53
     * @var bool
54
     */
55
    protected $verified;
56
57
    /**
58
     * What kind of events the player should be e-mailed about
59
     * @var string
60
     */
61
    protected $receives;
62
63
    /**
64
     * A confirmation code for the player's e-mail address verification
65
     * @var string
66
     */
67
    protected $confirmCode;
68
69
    /**
70
     * Whether the callsign of the player is outdated
71
     * @var bool
72
     */
73
    protected $outdated;
74
75
    /**
76
     * The player's profile description
77
     * @var string
78
     */
79
    protected $description;
80
81
    /**
82
     * The id of the player's country
83
     * @var int
84
     */
85
    protected $country;
86
87
    /**
88
     * The site theme this player has chosen
89
     * @var string
90
     */
91
    protected $theme;
92
93
    /**
94
     * Whether or not this player has opted-in for color blindness assistance.
95
     * @var bool
96
     */
97
    protected $color_blind_enabled;
98
99
    /**
100
     * The player's timezone PHP identifier, e.g. "Europe/Paris"
101
     * @var string
102
     */
103
    protected $timezone;
104
105
    /**
106
     * The date the player joined the site
107
     * @var TimeDate
108
     */
109
    protected $joined;
110
111
    /**
112
     * The date of the player's last login
113
     * @var TimeDate
114
     */
115
    protected $last_login;
116
117
    /**
118
     * The date of the player's last match
119
     * @var Match
120
     */
121
    protected $last_match;
122
123
    /**
124
     * The roles a player belongs to
125
     * @var Role[]
126
     */
127
    protected $roles;
128
129
    /**
130
     * The permissions a player has
131
     * @var Permission[]
132
     */
133
    protected $permissions;
134
135
    /**
136
     * A section for admins to write notes about players
137
     * @var string
138
     */
139
    protected $admin_notes;
140
141
    /**
142
     * The ban of the player, or null if the player is not banned
143
     * @var Ban|null
144
     */
145
    protected $ban;
146
147
    /**
148
     * Cached results for match summaries
149
     *
150
     * @var array
151
     */
152
    private $cachedMatchSummary;
153
154
    /**
155
     * The cached match count for a player
156
     *
157
     * @var int
158
     */
159
    private $cachedMatchCount = null;
160
161
    /**
162
     * The Elo for this player that has been explicitly set for this player from a database query. This value will take
163
     * precedence over having to build to an Elo season history.
164
     *
165
     * @var int
166
     */
167
    private $elo;
168
    private $eloSeason;
169
    private $eloSeasonHistory;
170
171
    private $matchActivity;
172
173
    private $is_disabled;
174
175
    const DELETED_COLUMN = 'is_deleted';
176
    const TABLE = "players";
177
178
    /**
179
     * The location where avatars will be stored
180
     */
181
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
182
183
    const EDIT_PERMISSION = Permission::EDIT_USER;
184
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
185
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
186
187
    /**
188
     * {@inheritdoc}
189
     */
190
    protected function assignResult($player)
191
    {
192
        $this->bzid = $player['bzid'];
193
        $this->name = $player['username'];
194
        $this->alias = $player['alias'];
195
        $this->team = $player['team'];
196
        $this->avatar = $player['avatar'];
197
        $this->country = $player['country'];
198
        $this->is_disabled = $player['is_disabled'];
199
        $this->is_deleted = $player['is_deleted'];
200
201
        if (array_key_exists('activity', $player)) {
202
            $this->matchActivity = ($player['activity'] != null) ? $player['activity'] : 0.0;
203
        }
204
205
        if (array_key_exists('elo', $player)) {
206
            $this->elo = $player['elo'];
207
        }
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    protected function assignLazyResult($player)
214
    {
215
        $this->email = $player['email'];
216
        $this->verified = $player['verified'];
217
        $this->receives = $player['receives'];
218
        $this->confirmCode = $player['confirm_code'];
219
        $this->outdated = $player['outdated'];
220
        $this->description = $player['description'];
221
        $this->timezone = $player['timezone'];
222
        $this->joined = TimeDate::fromMysql($player['joined']);
223
        $this->last_login = TimeDate::fromMysql($player['last_login']);
224
        $this->last_match = Match::get($player['last_match']);
225
        $this->admin_notes = $player['admin_notes'];
226
        $this->ban = Ban::getBan($this->id);
227
        $this->color_blind_enabled = $player['color_blind_enabled'];
228
229
        $this->cachedMatchSummary = [];
230
231
        // Theme user options
232
        if (isset($player['theme'])) {
233
            $this->theme = $player['theme'];
234
        } else {
235
            $themes = Service::getSiteThemes();
236
            $this->theme = $themes[0]['slug'];
237
        }
238
239
        $this->updateUserPermissions();
240
    }
241
242
    /**
243
     * Add a player a new role
244
     *
245
     * @param Role|int $role_id The role ID to add a player to
246
     *
247
     * @return bool Whether the operation was successful or not
248
     */
249
    public function addRole($role_id)
250
    {
251
        if ($role_id instanceof Role) {
252
            $role_id = $role_id->getId();
253
        }
254
255
        $this->lazyLoad();
256
257
        // Make sure the player doesn't already have the role
258
        foreach ($this->roles as $playerRole) {
259
            if ($playerRole->getId() == $role_id) {
260
                return false;
261
            }
262
        }
263
264
        $status = $this->modifyRole($role_id, "add");
265
        $this->refresh();
266
267
        return $status;
268
    }
269
270
    /**
271
     * Get the notes admins have left about a player
272
     * @return string The notes
273
     */
274
    public function getAdminNotes()
275
    {
276
        $this->lazyLoad();
277
278
        return $this->admin_notes;
279
    }
280
281
    /**
282
     * Get the player's BZID
283
     * @return int The BZID
284
     */
285
    public function getBZID()
286
    {
287
        return $this->bzid;
288
    }
289
290
    /**
291
     * Get the country a player belongs to
292
     *
293
     * @return Country The country belongs to
294
     */
295
    public function getCountry()
296
    {
297
        return Country::get($this->country);
298
    }
299
300
    /**
301
     * Get the e-mail address of the player
302
     *
303
     * @return string The address
304
     */
305
    public function getEmailAddress()
306
    {
307
        $this->lazyLoad();
308
309
        return $this->email;
310
    }
311
312
    /**
313
     * Build a key that we'll use for caching season Elo data in this model
314
     *
315
     * @param  string|null $season The season to get
316
     * @param  int|null    $year   The year of the season to get
317
     *
318
     * @return string
319
     */
320
    private function buildSeasonKey(&$season, &$year)
321
    {
322
        if ($season === null) {
323
            $season = Season::getCurrentSeason();
324
        }
325
326
        if ($year === null) {
327
            $year = Carbon::now()->year;
328
        }
329
330
        return sprintf('%s-%s', $year, $season);
331
    }
332
333
    /**
334
     * Build a key to use for caching season Elo data in this model from a timestamp
335
     *
336
     * @param DateTime $timestamp
337
     *
338
     * @return string
339
     */
340
    private function buildSeasonKeyFromTimestamp(\DateTime $timestamp)
341
    {
342
        $seasonInfo = Season::getSeason($timestamp);
343
344
        return sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
345
    }
346
347
    /**
348
     * Remove all Elo data for this model for matches occurring after the given match (inclusive)
349
     *
350
     * This function will not remove the Elo data for this match from the database. Ideally, this function should only
351
     * be called during Elo recalculation for this match.
352
     *
353
     * @internal
354
     *
355
     * @param Match $match
356
     *
357
     * @see Match::recalculateElo()
358
     */
359
    public function invalidateMatchFromCache(Match $match)
360
    {
361
        $seasonInfo = Season::getSeason($match->getTimestamp());
362
        $seasonKey = $this->buildSeasonKeyFromTimestamp($match->getTimestamp());
363
        $seasonElo = null;
0 ignored issues
show
Unused Code introduced by
$seasonElo is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
364
365
        $this->getEloSeasonHistory($seasonInfo['season'], $seasonInfo['year']);
366
367
        if (!isset($this->eloSeasonHistory[$seasonKey][$match->getId()])) {
368
            return;
369
        }
370
371
        $eloChangelogIndex = array_search($match->getId(), array_keys($this->eloSeasonHistory[$seasonKey]));
372
        $slicedChangeLog = array_slice($this->eloSeasonHistory[$seasonKey], 0, $eloChangelogIndex, true);
373
374
        $this->eloSeasonHistory[$seasonKey] = $slicedChangeLog;
375
        $this->eloSeason[$seasonKey] = end($slicedChangeLog)['elo'];
376
    }
377
378
    /**
379
     * Get the Elo changes for a player for a given season
380
     *
381
     * @param  string|null $season The season to get
382
     * @param  int|null    $year   The year of the season to get
383
     *
384
     * @return array
385
     */
386
    public function getEloSeasonHistory($season = null, $year = null)
387
    {
388
        $seasonKey = $this->buildSeasonKey($season, $year);
389
390
        // This season's already been cached
391
        if (isset($this->eloSeasonHistory[$seasonKey])) {
392
            return $this->eloSeasonHistory[$seasonKey];
393
        }
394
395
        $result = $this->db->query('
396
          SELECT
397
            elo_new AS elo,
398
            match_id AS `match`,
399
            MONTH(matches.timestamp) AS `month`,
400
            YEAR(matches.timestamp) AS `year`,
401
            DAY(matches.timestamp) AS `day`
402
          FROM
403
            player_elo
404
            LEFT JOIN matches ON player_elo.match_id = matches.id
405
          WHERE
406
            user_id = ? AND season_period = ? AND season_year = ?
407
          ORDER BY
408
            match_id ASC
409
        ', [ $this->getId(), $season, $year ]);
410
411
        $this->eloSeasonHistory[$seasonKey] = [[
412
            'elo' => 1200,
413
            'match' => null,
414
            'month' => Season::getCurrentSeasonRange($season)->getStartOfRange()->month,
415
            'year' => $year,
416
            'day' => 1
417
        ]] + array_combine(array_column($result, 'match'), $result);
418
419
        return $this->eloSeasonHistory[$seasonKey];
420
    }
421
422
    /**
423
     * Get the player's Elo for a season.
424
     *
425
     * With the default arguments, it will fetch the Elo for the current season.
426
     *
427
     * @param string|null $season The season we're looking for: winter, spring, summer, or fall
428
     * @param int|null    $year   The year of the season we're looking for
429
     *
430
     * @return int The player's Elo
431
     */
432
    public function getElo($season = null, $year = null)
433
    {
434
        // The Elo for this player has been forcefully set from a trusted database query, so just return that.
435
        if ($this->elo !== null) {
436
            return $this->elo;
437
        }
438
439
        $this->getEloSeasonHistory($season, $year);
440
        $seasonKey = $this->buildSeasonKey($season, $year);
441
442
        if (isset($this->eloSeason[$seasonKey])) {
443
            return $this->eloSeason[$seasonKey];
444
        }
445
446
        $season = &$this->eloSeasonHistory[$seasonKey];
447
448
        if (!empty($season)) {
449
            $elo = end($season);
450
            $this->eloSeason[$seasonKey] = ($elo !== false) ? $elo['elo'] : 1200;
451
        } else {
452
            $this->eloSeason[$seasonKey] = 1200;
453
        }
454
455
        return $this->eloSeason[$seasonKey];
456
    }
457
458
    /**
459
     * Adjust the Elo of a player for the current season based on a Match
460
     *
461
     * **Warning:** If $match is null, the Elo for the player will be modified but the value will not be persisted to
462
     * the database.
463
     *
464
     * @param int        $adjust The value to be added to the current ELO (negative to subtract)
465
     * @param Match|null $match  The match where this Elo change took place
466
     */
467
    public function adjustElo($adjust, Match $match = null)
468
    {
469
        $timestamp = ($match !== null) ? $match->getTimestamp() : (Carbon::now());
470
        $seasonInfo = Season::getSeason($timestamp);
471
472
        // Get the current Elo for the player, even if it's the default 1200. We need the value for adjusting
473
        $elo = $this->getElo($seasonInfo['season'], $seasonInfo['year']);
474
        $seasonKey = sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
475
476
        $this->eloSeason[$seasonKey] += $adjust;
477
478
        if ($match !== null && $this->isValid()) {
479
            $this->eloSeasonHistory[$seasonKey][$match->getId()] = [
480
                'elo' => $this->eloSeason[$seasonKey],
481
                'match' => $match->getId(),
482
                'month' => $match->getTimestamp()->month,
483
                'year' => $match->getTimestamp()->year,
484
                'day' => null,
485
            ];
486
487
            $this->db->execute('
488
              INSERT INTO player_elo VALUES (?, ?, ?, ?, ?, ?)
489
            ', [ $this->getId(), $match->getId(), $seasonInfo['season'], $seasonInfo['year'], $elo, $this->eloSeason[$seasonKey] ]);
490
        }
491
    }
492
493
    /**
494
     * Returns whether the player has verified their e-mail address
495
     *
496
     * @return bool `true` for verified players
497
     */
498
    public function isVerified()
499
    {
500
        $this->lazyLoad();
501
502
        return $this->verified;
503
    }
504
505
    /**
506
     * Returns the confirmation code for the player's e-mail address verification
507
     *
508
     * @return string The player's confirmation code
509
     */
510
    public function getConfirmCode()
511
    {
512
        $this->lazyLoad();
513
514
        return $this->confirmCode;
515
    }
516
517
    /**
518
     * Returns what kind of events the player should be e-mailed about
519
     *
520
     * @return string The type of notifications
521
     */
522
    public function getReceives()
523
    {
524
        $this->lazyLoad();
525
526
        return $this->receives;
527
    }
528
529
    /**
530
     * Finds out whether the specified player wants and can receive an e-mail
531
     * message
532
     *
533
     * @param  string  $type
534
     * @return bool `true` if the player should be sent an e-mail
535
     */
536
    public function canReceive($type)
537
    {
538
        $this->lazyLoad();
539
540
        if (!$this->email || !$this->isVerified()) {
541
            // Unverified e-mail means the user will receive nothing
542
            return false;
543
        }
544
545
        if ($this->receives == 'everything') {
546
            return true;
547
        }
548
549
        return $this->receives == $type;
550
    }
551
552
    /**
553
     * Find out whether the specified confirmation code is correct
554
     *
555
     * This method protects against timing attacks
556
     *
557
     * @param  string $code The confirmation code to check
558
     * @return bool `true` for a correct e-mail verification code
559
     */
560
    public function isCorrectConfirmCode($code)
561
    {
562
        $this->lazyLoad();
563
564
        if ($this->confirmCode === null) {
565
            return false;
566
        }
567
568
        return StringUtils::equals($code, $this->confirmCode);
569
    }
570
571
    /**
572
     * Get the player's sanitized description
573
     * @return string The description
574
     */
575
    public function getDescription()
576
    {
577
        $this->lazyLoad();
578
579
        return $this->description;
580
    }
581
582
    /**
583
     * Get the joined date of the player
584
     * @return TimeDate The joined date of the player
585
     */
586
    public function getJoinedDate()
587
    {
588
        $this->lazyLoad();
589
590
        return $this->joined->copy();
591
    }
592
593
    /**
594
     * Get all of the known IPs used by the player
595
     *
596
     * @return string[][] An array containing IPs and hosts
597
     */
598
    public function getKnownIPs()
599
    {
600
        return $this->db->query(
601
            'SELECT DISTINCT ip, host FROM visits WHERE player = ? GROUP BY ip, host ORDER BY MAX(timestamp) DESC LIMIT 10',
602
            array($this->getId())
603
        );
604
    }
605
606
    /**
607
     * Get the last login for a player
608
     * @return TimeDate The date of the last login
609
     */
610
    public function getLastLogin()
611
    {
612
        $this->lazyLoad();
613
614
        return $this->last_login->copy();
615
    }
616
617
    /**
618
     * Get the last match
619
     * @return Match
620
     */
621
    public function getLastMatch()
622
    {
623
        $this->lazyLoad();
624
625
        return $this->last_match;
626
    }
627
628
    /**
629
     * Get all of the callsigns a player has used to log in to the website
630
     * @return string[] An array containing all of the past callsigns recorded for a player
631
     */
632
    public function getPastCallsigns()
633
    {
634
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
635
    }
636
637
    /**
638
     * Get the player's team
639
     * @return Team The object representing the team
640
     */
641
    public function getTeam()
642
    {
643
        return Team::get($this->team);
644
    }
645
646
    /**
647
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
648
     * @return string The timezone
649
     */
650
    public function getTimezone()
651
    {
652
        $this->lazyLoad();
653
654
        return ($this->timezone) ?: date_default_timezone_get();
655
    }
656
657
    /**
658
     * Get the roles of the player
659
     * @return Role[]
660
     */
661
    public function getRoles()
662
    {
663
        $this->lazyLoad();
664
665
        return $this->roles;
666
    }
667
668
    /**
669
     * Rebuild the list of permissions a user has been granted
670
     */
671
    private function updateUserPermissions()
672
    {
673
        $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...
674
        $this->permissions = array();
675
676
        foreach ($this->roles as $role) {
677
            $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...
678
        }
679
    }
680
681
    /**
682
     * Check if a player has a specific permission
683
     *
684
     * @param string|null $permission The permission to check for
685
     *
686
     * @return bool Whether or not the player has the permission
687
     */
688
    public function hasPermission($permission)
689
    {
690
        if ($permission === null) {
691
            return false;
692
        }
693
694
        $this->lazyLoad();
695
696
        return isset($this->permissions[$permission]);
697
    }
698
699
    /**
700
     * Check whether or not a player been in a match or has logged on in the specified amount of time to be considered
701
     * active
702
     *
703
     * @return bool True if the player has been active
704
     */
705
    public function hasBeenActive()
706
    {
707
        $this->lazyLoad();
708
709
        $interval  = Service::getParameter('bzion.miscellaneous.active_interval');
710
        $lastLogin = $this->last_login->copy()->modify($interval);
711
712
        $hasBeenActive = (TimeDate::now() <= $lastLogin);
713
714
        if ($this->last_match->isValid()) {
715
            $lastMatch = $this->last_match->getTimestamp()->copy()->modify($interval);
716
            $hasBeenActive = ($hasBeenActive || TimeDate::now() <= $lastMatch);
717
        }
718
719
        return $hasBeenActive;
720
    }
721
722
    /**
723
     * Check whether the callsign of the player is outdated
724
     *
725
     * Returns true if this player has probably changed their callsign, making
726
     * the current username stored in the database obsolete
727
     *
728
     * @return bool Whether or not the player is disabled
729
     */
730
    public function isOutdated()
731
    {
732
        $this->lazyLoad();
733
734
        return $this->outdated;
735
    }
736
737
    /**
738
     * Check if a player's account has been disabled
739
     *
740
     * @return bool Whether or not the player is disabled
741
     */
742
    public function isDisabled()
743
    {
744
        return $this->status == "disabled";
745
    }
746
747
    /**
748
     * Check if everyone can log in as this user on a test environment
749
     *
750
     * @return bool
751
     */
752
    public function isTestUser()
753
    {
754
        return $this->status == "test";
755
    }
756
757
    /**
758
     * Check if a player is teamless
759
     *
760
     * @return bool True if the player is teamless
761
     */
762
    public function isTeamless()
763
    {
764
        return empty($this->team);
765
    }
766
767
    /**
768
     * Mark a player's account as banned
769
     *
770
     * @deprecated The players table shouldn't have any indicators of banned status, the Bans table is the authoritative source
771
     */
772
    public function markAsBanned()
773
    {
774
        return;
0 ignored issues
show
Coding Style introduced by
Empty return statement not required here
Loading history...
775
    }
776
777
    /**
778
     * Mark a player's account as unbanned
779
     *
780
     * @deprecated The players table shouldn't have any indicators of banned status, the Bans table is the authoritative source
781
     */
782
    public function markAsUnbanned()
783
    {
784
        return;
0 ignored issues
show
Coding Style introduced by
Empty return statement not required here
Loading history...
785
    }
786
787
    /**
788
     * Find out if a player is hard banned
789
     *
790
     * @return bool
791
     */
792
    public function isBanned()
793
    {
794
        $ban = Ban::getBan($this->id);
795
796
        return ($ban !== null && !$ban->isSoftBan());
797
    }
798
799
    /**
800
     * Get the ban of the player
801
     *
802
     * This method performs a load of all the lazy parameters of the Player
803
     *
804
     * @return Ban|null The current ban of the player, or null if the player is
805
     *                  is not banned
806
     */
807
    public function getBan()
808
    {
809
        $this->lazyLoad();
810
811
        return $this->ban;
812
    }
813
814
    /**
815
     * Remove a player from a role
816
     *
817
     * @param int $role_id The role ID to add or remove
818
     *
819
     * @return bool Whether the operation was successful or not
820
     */
821
    public function removeRole($role_id)
822
    {
823
        $status = $this->modifyRole($role_id, "remove");
824
        $this->refresh();
825
826
        return $status;
827
    }
828
829
    /**
830
     * Set the player's email address and reset their verification status
831
     * @param string $email The address
832
     */
833
    public function setEmailAddress($email)
834
    {
835
        $this->lazyLoad();
836
837
        if ($this->email == $email) {
838
            // The e-mail hasn't changed, don't do anything
839
            return;
840
        }
841
842
        $this->setVerified(false);
843
        $this->generateNewConfirmCode();
844
845
        $this->updateProperty($this->email, 'email', $email);
846
    }
847
848
    /**
849
     * Set whether the player has verified their e-mail address
850
     *
851
     * @param  bool $verified Whether the player is verified or not
852
     * @return self
853
     */
854
    public function setVerified($verified)
855
    {
856
        $this->lazyLoad();
857
858
        if ($verified) {
859
            $this->setConfirmCode(null);
860
        }
861
862
        return $this->updateProperty($this->verified, 'verified', $verified);
863
    }
864
865
    /**
866
     * Generate a new random confirmation token for e-mail address verification
867
     *
868
     * @return self
869
     */
870
    public function generateNewConfirmCode()
871
    {
872
        $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...
873
        $random = $generator->nextBytes(16);
874
875
        return $this->setConfirmCode(bin2hex($random));
876
    }
877
878
    /**
879
     * Set the confirmation token for e-mail address verification
880
     *
881
     * @param  string $code The confirmation code
882
     * @return self
883
     */
884
    private function setConfirmCode($code)
885
    {
886
        $this->lazyLoad();
887
888
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
889
    }
890
891
    /**
892
     * Set what kind of events the player should be e-mailed about
893
     *
894
     * @param  string $receives The type of notification
895
     * @return self
896
     */
897
    public function setReceives($receives)
898
    {
899
        $this->lazyLoad();
900
901
        return $this->updateProperty($this->receives, 'receives', $receives);
902
    }
903
904
    /**
905
     * Set whether the callsign of the player is outdated
906
     *
907
     * @param  bool $outdated Whether the callsign is outdated
908
     * @return self
909
     */
910
    public function setOutdated($outdated)
911
    {
912
        $this->lazyLoad();
913
914
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
915
    }
916
917
    /**
918
     * Set the player's description
919
     * @param string $description The description
920
     */
921
    public function setDescription($description)
922
    {
923
        $this->updateProperty($this->description, "description", $description);
924
    }
925
926
    /**
927
     * Set the player's timezone
928
     * @param string $timezone The timezone
929
     */
930
    public function setTimezone($timezone)
931
    {
932
        $this->updateProperty($this->timezone, "timezone", $timezone);
933
    }
934
935
    /**
936
     * Set the player's team
937
     * @param int $team The team's ID
938
     */
939
    public function setTeam($team)
940
    {
941
        $this->updateProperty($this->team, "team", $team);
942
    }
943
944
    /**
945
     * Set the match the player last participated in
946
     *
947
     * @param int $match The match's ID
948
     */
949
    public function setLastMatch($match)
950
    {
951
        $this->updateProperty($this->last_match, 'last_match', $match);
952
    }
953
954
    /**
955
     * Set the player's status
956
     * @param string $status The new status
957
     */
958
    public function setStatus($status)
959
    {
960
        $this->updateProperty($this->status, 'status', $status);
961
    }
962
963
    /**
964
     * Set the player's admin notes
965
     * @param  string $admin_notes The new admin notes
966
     * @return self
967
     */
968
    public function setAdminNotes($admin_notes)
969
    {
970
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
971
    }
972
973
    /**
974
     * Set the player's country
975
     * @param  int   $country The ID of the new country
976
     * @return self
977
     */
978
    public function setCountry($country)
979
    {
980
        return $this->updateProperty($this->country, 'country', $country);
981
    }
982
983
    /**
984
     * Get the player's chosen theme preference
985
     *
986
     * @return string
987
     */
988
    public function getTheme()
989
    {
990
        $this->lazyLoad();
991
992
        return $this->theme;
993
    }
994
995
    /**
996
     * Set the site theme for the player
997
     *
998
     * If the chosen site theme is invalid, it'll be defaulted to the site default (the first theme defined)
999
     *
1000
     * @param string $theme
1001
     */
1002
    public function setTheme($theme)
1003
    {
1004
        $themes = array_column(Service::getSiteThemes(), 'slug');
1005
1006
        if (!in_array($theme, $themes)) {
1007
            $theme = Service::getDefaultSiteTheme();
1008
        }
1009
1010
        return $this->updateProperty($this->theme, 'theme', $theme);
1011
    }
1012
1013
    /**
1014
     * Whether or not this player has color blind assistance enabled.
1015
     *
1016
     * @return bool
1017
     */
1018
    public function hasColorBlindAssist()
1019
    {
1020
        $this->lazyLoad();
1021
1022
        return (bool)$this->color_blind_enabled;
1023
    }
1024
1025
    /**
1026
     * Set a player's setting for color blind assistance.
1027
     *
1028
     * @param bool $enabled
1029
     *
1030
     * @return self
1031
     */
1032
    public function setColorBlindAssist($enabled)
1033
    {
1034
        return $this->updateProperty($this->color_blind_enabled, 'color_blind_enabled', $enabled);
1035
    }
1036
1037
    /**
1038
     * Updates this player's last login
1039
     */
1040
    public function updateLastLogin()
1041
    {
1042
        $this->update("last_login", TimeDate::now()->toMysql());
1043
    }
1044
1045
    /**
1046
     * Get the player's username
1047
     * @return string The username
1048
     */
1049
    public function getUsername()
1050
    {
1051
        return $this->name;
1052
    }
1053
1054
    /**
1055
     * Get the player's username, safe for use in your HTML
1056
     * @return string The username
1057
     */
1058
    public function getEscapedUsername()
1059
    {
1060
        return $this->getEscapedName();
1061
    }
1062
1063
    /**
1064
     * Alias for Player::setUsername()
1065
     *
1066
     * @param  string $username The new username
1067
     * @return self
1068
     */
1069
    public function setName($username)
1070
    {
1071
        return $this->setUsername($username);
1072
    }
1073
1074
    /**
1075
     * Mark all the unread messages of a player as read
1076
     *
1077
     * @return void
1078
     */
1079
    public function markMessagesAsRead()
1080
    {
1081
        $this->db->execute(
1082
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
1083
            array($this->id)
1084
        );
1085
    }
1086
1087
    /**
1088
     * Set the roles of a user
1089
     *
1090
     * @todo   Is it worth making this faster?
1091
     * @param  Role[] $roles The new roles of the user
1092
     * @return self
1093
     */
1094
    public function setRoles($roles)
1095
    {
1096
        $this->lazyLoad();
1097
1098
        $oldRoles = Role::mapToIds($this->roles);
1099
        $this->roles = $roles;
1100
        $roleIds = Role::mapToIds($roles);
1101
1102
        $newRoles     = array_diff($roleIds, $oldRoles);
1103
        $removedRoles = array_diff($oldRoles, $roleIds);
1104
1105
        foreach ($newRoles as $role) {
1106
            $this->modifyRole($role, 'add');
1107
        }
1108
1109
        foreach ($removedRoles as $role) {
1110
            $this->modifyRole($role, 'remove');
1111
        }
1112
1113
        $this->refresh();
1114
1115
        return $this;
1116
    }
1117
1118
    /**
1119
     * Give or remove a role to/form a player
1120
     *
1121
     * @param int    $role_id The role ID to add or remove
1122
     * @param string $action  Whether to "add" or "remove" a role for a player
1123
     *
1124
     * @return bool Whether the operation was successful or not
1125
     */
1126
    private function modifyRole($role_id, $action)
1127
    {
1128
        $role = Role::get($role_id);
1129
1130
        if ($role->isValid()) {
1131
            if ($action == "add") {
1132
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
1133
            } elseif ($action == "remove") {
1134
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
1135
            } else {
1136
                throw new Exception("Unrecognized role action");
1137
            }
1138
1139
            return true;
1140
        }
1141
1142
        return false;
1143
    }
1144
1145
    /**
1146
     * Given a player's BZID, get a player object
1147
     *
1148
     * @param  int    $bzid The player's BZID
1149
     * @return Player
1150
     */
1151
    public static function getFromBZID($bzid)
1152
    {
1153
        return self::get(self::fetchIdFrom($bzid, "bzid"));
1154
    }
1155
1156
    /**
1157
     * Get a single player by their username
1158
     *
1159
     * @param  string $username The username to look for
1160
     * @return Player
1161
     */
1162
    public static function getFromUsername($username)
1163
    {
1164
        $player = static::get(self::fetchIdFrom($username, 'username'));
1165
1166
        return $player->inject('name', $username);
1167
    }
1168
1169
    /**
1170
     * Get all the players in the database that have an active status
1171
     * @return Player[] An array of player BZIDs
1172
     */
1173
    public static function getPlayers()
1174
    {
1175
        return self::arrayIdToModel(
1176
            self::fetchIdsFrom("status", array("active", "test"), false)
1177
        );
1178
    }
1179
1180
    /**
1181
     * Show the number of notifications the user hasn't read yet
1182
     * @return int
1183
     */
1184
    public function countUnreadNotifications()
1185
    {
1186
        return Notification::countUnreadNotifications($this->id);
1187
    }
1188
1189
    /**
1190
     * Count the number of matches a player has participated in
1191
     * @return int
1192
     */
1193
    public function getMatchCount()
1194
    {
1195
        if ($this->cachedMatchCount === null) {
1196
            $this->cachedMatchCount = Match::getQueryBuilder()
1197
                ->active()
1198
                ->with($this)
1199
                ->count();
1200
        }
1201
1202
        return $this->cachedMatchCount;
1203
    }
1204
1205
    /**
1206
     * Get the (victory/total matches) ratio of the player
1207
     * @return float
1208
     */
1209
    public function getMatchWinRatio()
1210
    {
1211
        $count = $this->getMatchCount();
1212
1213
        if ($count == 0) {
1214
            return 0;
1215
        }
1216
1217
        $wins = Match::getQueryBuilder()
1218
            ->active()
1219
            ->with($this, 'win')
1220
            ->count();
1221
1222
        return $wins / $count;
1223
    }
1224
1225
    /**
1226
     * Get the (total caps made by team/total matches) ratio of the player
1227
     * @return float
1228
     */
1229
    public function getMatchAverageCaps()
1230
    {
1231
        $count = $this->getMatchCount();
1232
1233
        if ($count == 0) {
1234
            return 0;
1235
        }
1236
1237
        // Get the sum of team A points if the player was in team A, team B points if the player was in team B
1238
        $query = $this->db->query("
1239
            SELECT
1240
              SUM(
1241
                IF(mp.team_loyalty = 0, team_a_points, team_b_points)
1242
              ) AS sum
1243
            FROM
1244
              matches
1245
            INNER JOIN
1246
              match_participation mp ON mp.match_id = matches.id
1247
            WHERE
1248
              status = 'entered' AND mp.user_id = ?
1249
        ", [$this->id]);
1250
1251
        return $query[0]['sum'] / $count;
1252
    }
1253
1254
    /**
1255
     * Get the match activity in matches per day for a player
1256
     *
1257
     * @return float
1258
     */
1259
    public function getMatchActivity()
1260
    {
1261
        if ($this->matchActivity !== null) {
1262
            return $this->matchActivity;
1263
        }
1264
1265
        $activity = 0.0;
1266
1267
        $matches = Match::getQueryBuilder()
1268
            ->active()
1269
            ->with($this)
1270
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
1271
            ->getModels($fast = true);
1272
1273
        foreach ($matches as $match) {
1274
            $activity += $match->getActivity();
1275
        }
1276
1277
        return $activity;
1278
    }
1279
1280
    /**
1281
     * Return an array of matches this player participated in per month.
1282
     *
1283
     * ```
1284
     * ['yyyy-mm'] = <number of matches>
1285
     * ```
1286
     *
1287
     * @param TimeDate|string $timePeriod
1288
     *
1289
     * @return int[]
1290
     */
1291
    public function getMatchSummary($timePeriod = '1 year ago')
1292
    {
1293
        $since = ($timePeriod instanceof TimeDate) ? $timePeriod : TimeDate::from($timePeriod);
1294
1295
        if (!isset($this->cachedMatchSummary[(string)$timePeriod])) {
1296
            $this->cachedMatchSummary[(string)$timePeriod] = Match::getQueryBuilder()
1297
                ->active()
1298
                ->with($this)
1299
                ->where('time')->isAfter($since)
1300
                ->getSummary($since)
1301
            ;
1302
        }
1303
1304
        return $this->cachedMatchSummary[(string)$timePeriod];
1305
    }
1306
1307
    /**
1308
     * Show the number of messages the user hasn't read yet
1309
     * @return int
1310
     */
1311
    public function countUnreadMessages()
1312
    {
1313
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
1314
            $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...
1315
        );
1316
    }
1317
1318
    /**
1319
     * Get all of the members belonging to a team
1320
     * @param  int      $teamID The ID of the team to fetch the members of
1321
     * @return Player[] An array of Player objects of the team members
1322
     */
1323
    public static function getTeamMembers($teamID)
1324
    {
1325
        return self::arrayIdToModel(
1326
            self::fetchIds("WHERE team = ?", array($teamID))
1327
        );
1328
    }
1329
1330
    /**
1331
     * {@inheritdoc}
1332
     */
1333
    public static function getActiveStatuses()
1334
    {
1335
        return array('active', 'reported', 'test');
1336
    }
1337
1338
    /**
1339
     * {@inheritdoc}
1340
     */
1341
    public static function getEagerColumns($prefix = null)
1342
    {
1343
        $columns = [
1344
            'id',
1345
            'bzid',
1346
            'team',
1347
            'username',
1348
            'alias',
1349
            'status',
1350
            'avatar',
1351
            'country',
1352
        ];
1353
1354
        return self::formatColumns($prefix, $columns);
0 ignored issues
show
Deprecated Code introduced by
The method BaseModel::formatColumns() has been deprecated with message: 0.10.2 This function has been removed and is no longer required with the new query builder

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1355
    }
1356
1357
    /**
1358
     * {@inheritdoc}
1359
     */
1360
    public static function getLazyColumns($prefix = null)
1361
    {
1362
        $columns = [
1363
            'email',
1364
            'verified',
1365
            'receives',
1366
            'confirm_code',
1367
            'outdated',
1368
            'description',
1369
            'theme',
1370
            'color_blind_enabled',
1371
            'timezone',
1372
            'joined',
1373
            'last_login',
1374
            'last_match',
1375
            'admin_notes',
1376
        ];
1377
1378
        return self::formatColumns($prefix, $columns);
0 ignored issues
show
Deprecated Code introduced by
The method BaseModel::formatColumns() has been deprecated with message: 0.10.2 This function has been removed and is no longer required with the new query builder

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1379
    }
1380
1381
    /**
1382
     * {@inheritdoc}
1383
     */
1384
    public static function getEagerColumnsList()
1385
    {
1386
        return [
1387
            'id',
1388
            'bzid',
1389
            'team',
1390
            'username',
1391
            'alias',
1392
            'avatar',
1393
            'country',
1394
            'is_disabled',
1395
            'is_deleted',
1396
        ];
1397
    }
1398
1399
    /**
1400
     * {@inheritdoc}
1401
     */
1402
    public static function getLazyColumnsList()
1403
    {
1404
        return [
1405
            'email',
1406
            'verified',
1407
            'receives',
1408
            'confirm_code',
1409
            'outdated',
1410
            'description',
1411
            'theme',
1412
            'color_blind_enabled',
1413
            'timezone',
1414
            'joined',
1415
            'last_login',
1416
            'last_match',
1417
            'admin_notes',
1418
        ];
1419
    }
1420
1421
    /**
1422
     * Get a query builder for players
1423
     *
1424
     * @throws Exception
1425
     *
1426
     * @return PlayerQueryBuilder
1427
     */
1428
    public static function getQueryBuilder()
1429
    {
1430
        return PlayerQueryBuilder::createForModel(Player::class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1431
            ->setNameColumn('username')
1432
        ;
1433
    }
1434
1435
    /**
1436
     * Enter a new player to the database
1437
     * @param  int              $bzid        The player's bzid
1438
     * @param  string           $username    The player's username
1439
     * @param  int              $team        The player's team
1440
     * @param  string           $status      The player's status
1441
     * @param  int              $role_id     The player's role when they are first created
1442
     * @param  string           $avatar      The player's profile avatar
1443
     * @param  string           $description The player's profile description
1444
     * @param  int              $country     The player's country
1445
     * @param  string           $timezone    The player's timezone
1446
     * @param  string|\TimeDate $joined      The date the player joined
1447
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1448
     * @return Player           An object representing the player that was just entered
1449
     */
1450
    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")
1451
    {
1452
        $joined = TimeDate::from($joined);
1453
        $last_login = TimeDate::from($last_login);
1454
        $timezone = ($timezone) ?: date_default_timezone_get();
1455
1456
        $player = self::create(array(
1457
            'bzid'        => $bzid,
1458
            'team'        => $team,
1459
            'username'    => $username,
1460
            'alias'       => self::generateAlias($username),
1461
            'status'      => $status,
1462
            'avatar'      => $avatar,
1463
            'description' => $description,
1464
            'country'     => $country,
1465
            'timezone'    => $timezone,
1466
            'joined'      => $joined->toMysql(),
1467
            'last_login'  => $last_login->toMysql(),
1468
        ));
1469
1470
        $player->addRole($role_id);
1471
        $player->getIdenticon($player->getId());
1472
        $player->setUsername($username);
1473
1474
        return $player;
1475
    }
1476
1477
    /**
1478
     * Determine if a player exists in the database
1479
     * @param  int  $bzid The player's bzid
1480
     * @return bool Whether the player exists in the database
1481
     */
1482
    public static function playerBZIDExists($bzid)
1483
    {
1484
        return self::getFromBZID($bzid)->isValid();
1485
    }
1486
1487
    /**
1488
     * Change a player's callsign and add it to the database if it does not
1489
     * exist as a past callsign
1490
     *
1491
     * @param  string $username The new username of the player
1492
     * @return self
1493
     */
1494
    public function setUsername($username)
1495
    {
1496
        // The player's username was just fetched from BzDB, it's definitely not
1497
        // outdated
1498
        $this->setOutdated(false);
1499
1500
        // Players who have this player's username are considered outdated
1501
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1502
1503
        if ($username === $this->name) {
1504
            // The player's username hasn't changed, no need to do anything
1505
            return $this;
1506
        }
1507
1508
        // Players who used to have our player's username are not outdated anymore,
1509
        // unless they are more than one.
1510
        // Even though we are sure that the old and new usernames are not equal,
1511
        // MySQL makes a different type of string equality tests, which is why we
1512
        // also check IDs to make sure not to affect our own player's outdatedness.
1513
        $this->db->execute("
1514
            UPDATE {$this->table} SET outdated =
1515
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1516
            WHERE username = ? AND id != ?",
1517
            array($this->name, $this->id, $this->name, $this->id));
1518
1519
        $this->updateProperty($this->name, 'username', $username);
1520
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1521
        $this->resetAlias();
1522
1523
        return $this;
1524
    }
1525
1526
    /**
1527
     * Alphabetical order function for use in usort (case-insensitive)
1528
     * @return Closure The sort function
1529
     */
1530
    public static function getAlphabeticalSort()
1531
    {
1532
        return function (Player $a, Player $b) {
1533
            return strcasecmp($a->getUsername(), $b->getUsername());
1534
        };
1535
    }
1536
1537
    /**
1538
     * {@inheritdoc}
1539
     * @todo Add a constraint that does this automatically
1540
     */
1541
    public function wipe()
1542
    {
1543
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1544
1545
        parent::wipe();
1546
    }
1547
1548
    /**
1549
     * Find whether the player can delete a model
1550
     *
1551
     * @param  PermissionModel $model       The model that will be seen
1552
     * @param  bool         $showDeleted Whether to show deleted models to admins
1553
     * @return bool
1554
     */
1555
    public function canSee($model, $showDeleted = false)
1556
    {
1557
        return $model->canBeSeenBy($this, $showDeleted);
1558
    }
1559
1560
    /**
1561
     * Find whether the player can delete a model
1562
     *
1563
     * @param  PermissionModel $model The model that will be deleted
1564
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1565
     *                                to soft-delete ones
1566
     * @return bool
1567
     */
1568
    public function canDelete($model, $hard = false)
1569
    {
1570
        if ($hard) {
1571
            return $model->canBeHardDeletedBy($this);
1572
        } else {
1573
            return $model->canBeSoftDeletedBy($this);
1574
        }
1575
    }
1576
1577
    /**
1578
     * Find whether the player can create a model
1579
     *
1580
     * @param  string  $modelName The PHP class identifier of the model type
1581
     * @return bool
1582
     */
1583
    public function canCreate($modelName)
1584
    {
1585
        return $modelName::canBeCreatedBy($this);
1586
    }
1587
1588
    /**
1589
     * Find whether the player can edit a model
1590
     *
1591
     * @param  PermissionModel $model The model which will be edited
1592
     * @return bool
1593
     */
1594
    public function canEdit($model)
1595
    {
1596
        return $model->canBeEditedBy($this);
1597
    }
1598
}
1599