Completed
Push — master ( 80fdf3...351fbc )
by Vladimir
02:43
created

Player::invalidateMatchFromCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 11
nc 2
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 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
    /**
174
     * The name of the database table used for queries
175
     */
176
    const TABLE = "players";
177 76
178
    /**
179 76
     * The location where avatars will be stored
180 76
     */
181 76
    const AVATAR_LOCATION = "/web/assets/imgs/avatars/players/";
182 76
183 76
    const EDIT_PERMISSION = Permission::EDIT_USER;
184 76
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_USER;
185 76
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_USER;
186
187 76
    /**
188
     * {@inheritdoc}
189
     */
190 76
    protected function assignResult($player)
191
    {
192
        $this->bzid = $player['bzid'];
193
        $this->name = $player['username'];
194
        $this->alias = $player['alias'];
195 76
        $this->team = $player['team'];
196
        $this->status = $player['status'];
197 76
        $this->avatar = $player['avatar'];
198 76
        $this->country = $player['country'];
199 76
200 76
        if (array_key_exists('activity', $player)) {
201 76
            $this->matchActivity = ($player['activity'] != null) ? $player['activity'] : 0.0;
202 76
        }
203 76
204 76
        if (array_key_exists('elo', $player)) {
205 76
            $this->elo = $player['elo'];
206 76
        }
207 76
    }
208 76
209
    /**
210 76
     * {@inheritdoc}
211
     */
212 76
    protected function assignLazyResult($player)
213 76
    {
214
        $this->email = $player['email'];
215
        $this->verified = $player['verified'];
216
        $this->receives = $player['receives'];
217
        $this->confirmCode = $player['confirm_code'];
218
        $this->outdated = $player['outdated'];
219
        $this->description = $player['description'];
220
        $this->timezone = $player['timezone'];
221
        $this->joined = TimeDate::fromMysql($player['joined']);
222 76
        $this->last_login = TimeDate::fromMysql($player['last_login']);
223
        $this->last_match = Match::get($player['last_match']);
224 76
        $this->admin_notes = $player['admin_notes'];
225 1
        $this->ban = Ban::getBan($this->id);
226
        $this->color_blind_enabled = $player['color_blind_enabled'];
227
228 76
        $this->cachedMatchSummary = [];
229
230
        // Theme user options
231 76
        if (isset($player['theme'])) {
232 14
            $this->theme = $player['theme'];
233 14
        } else {
234
            $themes = Service::getSiteThemes();
235
            $this->theme = $themes[0]['slug'];
236
        }
237 76
238 76
        $this->updateUserPermissions();
239
    }
240 76
241
    /**
242
     * Add a player a new role
243
     *
244
     * @param Role|int $role_id The role ID to add a player to
245
     *
246
     * @return bool Whether the operation was successful or not
247
     */
248
    public function addRole($role_id)
249
    {
250
        if ($role_id instanceof Role) {
251
            $role_id = $role_id->getId();
252
        }
253
254
        $this->lazyLoad();
255
256
        // Make sure the player doesn't already have the role
257
        foreach ($this->roles as $playerRole) {
258 22
            if ($playerRole->getId() == $role_id) {
259
                return false;
260 22
            }
261
        }
262
263
        $status = $this->modifyRole($role_id, "add");
264
        $this->refresh();
265
266
        return $status;
267
    }
268 1
269
    /**
270 1
     * Get the notes admins have left about a player
271
     * @return string The notes
272
     */
273
    public function getAdminNotes()
274
    {
275
        $this->lazyLoad();
276
277
        return $this->admin_notes;
278
    }
279
280
    /**
281
     * Get the player's BZID
282
     * @return int The BZID
283
     */
284
    public function getBZID()
285
    {
286
        return $this->bzid;
287
    }
288
289
    /**
290
     * Get the country a player belongs to
291
     *
292
     * @return Country The country belongs to
293 30
     */
294
    public function getCountry()
295 30
    {
296 29
        return Country::get($this->country);
297
    }
298
299 30
    /**
300 29
     * Get the e-mail address of the player
301
     *
302
     * @return string The address
303 30
     */
304
    public function getEmailAddress()
305
    {
306
        $this->lazyLoad();
307
308
        return $this->email;
309
    }
310
311
    /**
312
     * Build a key that we'll use for caching season Elo data in this model
313 3
     *
314
     * @param  string|null $season The season to get
315 3
     * @param  int|null    $year   The year of the season to get
316
     *
317 3
     * @return string
318
     */
319
    private function buildSeasonKey(&$season, &$year)
320
    {
321
        if ($season === null) {
322
            $season = Season::getCurrentSeason();
323
        }
324
325
        if ($year === null) {
326
            $year = Carbon::now()->year;
327
        }
328
329
        return sprintf('%s-%s', $year, $season);
330
    }
331
332 3
    /**
333
     * Build a key to use for caching season Elo data in this model from a timestamp
334 3
     *
335 3
     * @param DateTime $timestamp
336
     *
337
     * @return string
338
     */
339 3
    private function buildSeasonKeyFromTimestamp(\DateTime $timestamp)
340 2
    {
341
        $seasonInfo = Season::getSeason($timestamp);
342
343
        return sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
344
    }
345 3
346
    /**
347 3
     * Remove all Elo data for this model for matches occurring after the given match (inclusive)
348 1
     *
349
     * This function will not remove the Elo data for this match from the database. Ideally, this function should only
350
     * be called during Elo recalculation for this match.
351
     *
352
     * @internal
353 2
     *
354 2
     * @param Match $match
355
     *
356 2
     * @see Match::recalculateElo()
357
     */
358
    public function invalidateMatchFromCache(Match $match)
359 2
    {
360
        $seasonInfo = Season::getSeason($match->getTimestamp());
361
        $seasonKey = $this->buildSeasonKeyFromTimestamp($match->getTimestamp());
362
        $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...
363
364
        $this->getEloSeasonHistory($seasonInfo['season'], $seasonInfo['year']);
365
366
        if (!isset($this->eloSeasonHistory[$seasonKey][$match->getId()])) {
367
            return;
368
        }
369 30
370
        $eloChangelogIndex = array_search($match->getId(), array_keys($this->eloSeasonHistory[$seasonKey]));
371 30
        $slicedChangeLog = array_slice($this->eloSeasonHistory[$seasonKey], 0, $eloChangelogIndex, true);
372
373
        $this->eloSeasonHistory[$seasonKey] = $slicedChangeLog;
374 30
        $this->eloSeason[$seasonKey] = end($slicedChangeLog)['elo'];
375 29
    }
376
377
    /**
378 30
     * Get the Elo changes for a player for a given season
379
     *
380
     * @param  string|null $season The season to get
381
     * @param  int|null    $year   The year of the season to get
382
     *
383
     * @return array
384
     */
385
    public function getEloSeasonHistory($season = null, $year = null)
386
    {
387
        $seasonKey = $this->buildSeasonKey($season, $year);
388
389
        // This season's already been cached
390
        if (isset($this->eloSeasonHistory[$seasonKey])) {
391
            return $this->eloSeasonHistory[$seasonKey];
392 30
        }
393
394 30
        $result = $this->db->query('
395 30
          SELECT
396
            elo_new AS elo,
397 30
            match_id AS `match`,
398 30
            MONTH(matches.timestamp) AS `month`,
399 30
            YEAR(matches.timestamp) AS `year`,
400
            DAY(matches.timestamp) AS `day`
401
          FROM
402 30
            player_elo
403
            LEFT JOIN matches ON player_elo.match_id = matches.id
404
          WHERE
405
            user_id = ? AND season_period = ? AND season_year = ?
406
          ORDER BY
407
            match_id ASC
408
        ', [ $this->getId(), $season, $year ]);
409
410
        $this->eloSeasonHistory[$seasonKey] = [[
411
            'elo' => 1200,
412
            'match' => null,
413
            'month' => Season::getCurrentSeasonRange($season)->getStartOfRange()->month,
414
            'year' => $year,
415 30
            'day' => 1
416
        ]] + array_combine(array_column($result, 'match'), $result);
417 30
418 30
        return $this->eloSeasonHistory[$seasonKey];
419
    }
420 30
421 29
    /**
422
     * Get the player's Elo for a season.
423
     *
424 30
     * With the default arguments, it will fetch the Elo for the current season.
425
     *
426 30
     * @param string|null $season The season we're looking for: winter, spring, summer, or fall
427 30
     * @param int|null    $year   The year of the season we're looking for
428 30
     *
429
     * @return int The player's Elo
430
     */
431
    public function getElo($season = null, $year = null)
432
    {
433 30
        // The Elo for this player has been forcefully set from a trusted database query, so just return that.
434
        if ($this->elo !== null) {
435
            return $this->elo;
436
        }
437
438
        $this->getEloSeasonHistory($season, $year);
439
        $seasonKey = $this->buildSeasonKey($season, $year);
440
441
        if (isset($this->eloSeason[$seasonKey])) {
442
            return $this->eloSeason[$seasonKey];
443
        }
444
445 30
        $season = &$this->eloSeasonHistory[$seasonKey];
446
447 30
        if (!empty($season)) {
448 30
            $elo = end($season);
449
            $this->eloSeason[$seasonKey] = ($elo !== false) ? $elo['elo'] : 1200;
450
        } else {
451 30
            $this->eloSeason[$seasonKey] = 1200;
452 30
        }
453
454 30
        return $this->eloSeason[$seasonKey];
455
    }
456 30
457 29
    /**
458
     * Adjust the Elo of a player for the current season based on a Match
459 29
     *
460
     * **Warning:** If $match is null, the Elo for the player will be modified but the value will not be persisted to
461 30
     * the database.
462
     *
463
     * @param int        $adjust The value to be added to the current ELO (negative to subtract)
464
     * @param Match|null $match  The match where this Elo change took place
465
     */
466
    public function adjustElo($adjust, Match $match = null)
467
    {
468
        $timestamp = ($match !== null) ? $match->getTimestamp() : (Carbon::now());
469
        $seasonInfo = Season::getSeason($timestamp);
470
471
        // Get the current Elo for the player, even if it's the default 1200. We need the value for adjusting
472
        $elo = $this->getElo($seasonInfo['season'], $seasonInfo['year']);
473
        $seasonKey = sprintf('%s-%s', $seasonInfo['year'], $seasonInfo['season']);
474
475
        $this->eloSeason[$seasonKey] += $adjust;
476
477
        if ($match !== null && $this->isValid()) {
478
            $this->eloSeasonHistory[$seasonKey][$match->getId()] = [
479
                'elo' => $this->eloSeason[$seasonKey],
480
                'match' => $match->getId(),
481
                'month' => $match->getTimestamp()->month,
482
                'year' => $match->getTimestamp()->year,
483
                'day' => null,
484
            ];
485
486
            $this->db->execute('
487
              INSERT INTO player_elo VALUES (?, ?, ?, ?, ?, ?)
488
            ', [ $this->getId(), $match->getId(), $seasonInfo['season'], $seasonInfo['year'], $elo, $this->eloSeason[$seasonKey] ]);
489
        }
490
    }
491
492
    /**
493
     * Returns whether the player has verified their e-mail address
494
     *
495
     * @return bool `true` for verified players
496
     */
497
    public function isVerified()
498
    {
499
        $this->lazyLoad();
500
501
        return $this->verified;
502
    }
503
504
    /**
505
     * Returns the confirmation code for the player's e-mail address verification
506 1
     *
507
     * @return string The player's confirmation code
508 1
     */
509
    public function getConfirmCode()
510 1
    {
511
        $this->lazyLoad();
512 1
513
        return $this->confirmCode;
514
    }
515
516
    /**
517
     * Returns what kind of events the player should be e-mailed about
518
     *
519
     * @return string The type of notifications
520
     */
521
    public function getReceives()
522
    {
523
        $this->lazyLoad();
524
525
        return $this->receives;
526
    }
527
528
    /**
529
     * Finds out whether the specified player wants and can receive an e-mail
530
     * message
531
     *
532
     * @param  string  $type
533
     * @return bool `true` if the player should be sent an e-mail
534
     */
535
    public function canReceive($type)
536
    {
537
        $this->lazyLoad();
538
539
        if (!$this->email || !$this->isVerified()) {
540
            // Unverified e-mail means the user will receive nothing
541
            return false;
542
        }
543
544
        if ($this->receives == 'everything') {
545
            return true;
546
        }
547
548
        return $this->receives == $type;
549
    }
550
551
    /**
552
     * Find out whether the specified confirmation code is correct
553
     *
554
     * This method protects against timing attacks
555
     *
556
     * @param  string $code The confirmation code to check
557
     * @return bool `true` for a correct e-mail verification code
558
     */
559
    public function isCorrectConfirmCode($code)
560
    {
561
        $this->lazyLoad();
562
563
        if ($this->confirmCode === null) {
564
            return false;
565
        }
566
567
        return StringUtils::equals($code, $this->confirmCode);
568
    }
569
570
    /**
571
     * Get the player's sanitized description
572
     * @return string The description
573
     */
574
    public function getDescription()
575
    {
576
        $this->lazyLoad();
577
578
        return $this->description;
579
    }
580
581
    /**
582
     * Get the joined date of the player
583
     * @return TimeDate The joined date of the player
584
     */
585
    public function getJoinedDate()
586
    {
587
        $this->lazyLoad();
588
589
        return $this->joined->copy();
590
    }
591
592
    /**
593
     * Get all of the known IPs used by the player
594
     *
595
     * @return string[][] An array containing IPs and hosts
596
     */
597
    public function getKnownIPs()
598
    {
599
        return $this->db->query(
600
            'SELECT DISTINCT ip, host FROM visits WHERE player = ? GROUP BY ip, host ORDER BY MAX(timestamp) DESC LIMIT 10',
601
            array($this->getId())
602
        );
603
    }
604
605
    /**
606
     * Get the last login for a player
607
     * @return TimeDate The date of the last login
608
     */
609
    public function getLastLogin()
610
    {
611 23
        $this->lazyLoad();
612
613 23
        return $this->last_login->copy();
614
    }
615
616
    /**
617
     * Get the last match
618
     * @return Match
619
     */
620 1
    public function getLastMatch()
621
    {
622 1
        $this->lazyLoad();
623
624 1
        return $this->last_match;
625
    }
626
627
    /**
628
     * Get all of the callsigns a player has used to log in to the website
629
     * @return string[] An array containing all of the past callsigns recorded for a player
630
     */
631
    public function getPastCallsigns()
632
    {
633
        return self::fetchIds("WHERE player = ?", array($this->id), "past_callsigns", "username");
634
    }
635
636
    /**
637
     * Get the player's team
638
     * @return Team The object representing the team
639
     */
640
    public function getTeam()
641 76
    {
642
        return Team::get($this->team);
643 76
    }
644 76
645
    /**
646 76
     * Get the player's timezone PHP identifier (example: "Europe/Paris")
647 76
     * @return string The timezone
648
     */
649 76
    public function getTimezone()
650
    {
651
        $this->lazyLoad();
652
653
        return ($this->timezone) ?: date_default_timezone_get();
654
    }
655
656
    /**
657
     * Get the roles of the player
658 2
     * @return Role[]
659
     */
660 2
    public function getRoles()
661 1
    {
662
        $this->lazyLoad();
663
664 2
        return $this->roles;
665
    }
666 2
667
    /**
668
     * Rebuild the list of permissions a user has been granted
669
     */
670
    private function updateUserPermissions()
671
    {
672
        $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...
673
        $this->permissions = array();
674
675
        foreach ($this->roles as $role) {
676
            $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...
677
        }
678
    }
679
680
    /**
681
     * Check if a player has a specific permission
682
     *
683
     * @param string|null $permission The permission to check for
684
     *
685
     * @return bool Whether or not the player has the permission
686
     */
687
    public function hasPermission($permission)
688
    {
689
        if ($permission === null) {
690
            return false;
691
        }
692
693
        $this->lazyLoad();
694
695
        return isset($this->permissions[$permission]);
696
    }
697
698
    /**
699
     * Check whether or not a player been in a match or has logged on in the specified amount of time to be considered
700
     * active
701
     *
702
     * @return bool True if the player has been active
703
     */
704
    public function hasBeenActive()
705
    {
706
        $this->lazyLoad();
707
708
        $interval  = Service::getParameter('bzion.miscellaneous.active_interval');
709
        $lastLogin = $this->last_login->copy()->modify($interval);
710
711
        $hasBeenActive = (TimeDate::now() <= $lastLogin);
712
713
        if ($this->last_match->isValid()) {
714
            $lastMatch = $this->last_match->getTimestamp()->copy()->modify($interval);
715
            $hasBeenActive = ($hasBeenActive || TimeDate::now() <= $lastMatch);
716
        }
717
718
        return $hasBeenActive;
719
    }
720
721
    /**
722 1
     * Check whether the callsign of the player is outdated
723
     *
724 1
     * Returns true if this player has probably changed their callsign, making
725
     * the current username stored in the database obsolete
726
     *
727
     * @return bool Whether or not the player is disabled
728
     */
729
    public function isOutdated()
730
    {
731
        $this->lazyLoad();
732 55
733
        return $this->outdated;
734 55
    }
735
736
    /**
737
     * Check if a player's account has been disabled
738
     *
739
     * @return bool Whether or not the player is disabled
740 1
     */
741
    public function isDisabled()
742 1
    {
743
        return $this->status == "disabled";
744
    }
745
746 1
    /**
747
     * Check if everyone can log in as this user on a test environment
748
     *
749
     * @return bool
750
     */
751
    public function isTestUser()
752
    {
753
        return $this->status == "test";
754
    }
755
756
    /**
757
     * Check if a player is teamless
758
     *
759
     * @return bool True if the player is teamless
760
     */
761
    public function isTeamless()
762
    {
763
        return empty($this->team);
764
    }
765
766 2
    /**
767
     * Mark a player's account as banned
768 2
     *
769
     * @deprecated The players table shouldn't have any indicators of banned status, the Bans table is the authoritative source
770
     */
771
    public function markAsBanned()
772
    {
773
        return;
0 ignored issues
show
Coding Style introduced by
Empty return statement not required here
Loading history...
774
    }
775
776
    /**
777
     * Mark a player's account as unbanned
778
     *
779
     * @deprecated The players table shouldn't have any indicators of banned status, the Bans table is the authoritative source
780
     */
781
    public function markAsUnbanned()
782
    {
783
        return;
0 ignored issues
show
Coding Style introduced by
Empty return statement not required here
Loading history...
784
    }
785
786
    /**
787
     * Find out if a player is hard banned
788
     *
789
     * @return bool
790
     */
791
    public function isBanned()
792
    {
793
        $ban = Ban::getBan($this->id);
794
795
        return ($ban !== null && !$ban->isSoftBan());
796
    }
797
798
    /**
799
     * Get the ban of the player
800
     *
801
     * This method performs a load of all the lazy parameters of the Player
802
     *
803
     * @return Ban|null The current ban of the player, or null if the player is
804
     *                  is not banned
805
     */
806
    public function getBan()
807
    {
808
        $this->lazyLoad();
809
810
        return $this->ban;
811
    }
812
813
    /**
814
     * Remove a player from a role
815
     *
816
     * @param int $role_id The role ID to add or remove
817
     *
818
     * @return bool Whether the operation was successful or not
819
     */
820
    public function removeRole($role_id)
821
    {
822
        $status = $this->modifyRole($role_id, "remove");
823
        $this->refresh();
824
825
        return $status;
826
    }
827
828
    /**
829
     * Set the player's email address and reset their verification status
830
     * @param string $email The address
831
     */
832
    public function setEmailAddress($email)
833
    {
834
        $this->lazyLoad();
835
836
        if ($this->email == $email) {
837
            // The e-mail hasn't changed, don't do anything
838
            return;
839
        }
840
841
        $this->setVerified(false);
842
        $this->generateNewConfirmCode();
843
844
        $this->updateProperty($this->email, 'email', $email);
845
    }
846
847
    /**
848
     * Set whether the player has verified their e-mail address
849
     *
850
     * @param  bool $verified Whether the player is verified or not
851
     * @return self
852
     */
853
    public function setVerified($verified)
854
    {
855
        $this->lazyLoad();
856
857
        if ($verified) {
858
            $this->setConfirmCode(null);
859
        }
860
861
        return $this->updateProperty($this->verified, 'verified', $verified);
862
    }
863
864
    /**
865
     * Generate a new random confirmation token for e-mail address verification
866
     *
867
     * @return self
868
     */
869
    public function generateNewConfirmCode()
870
    {
871
        $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...
872
        $random = $generator->nextBytes(16);
873
874
        return $this->setConfirmCode(bin2hex($random));
875
    }
876
877
    /**
878
     * Set the confirmation token for e-mail address verification
879
     *
880
     * @param  string $code The confirmation code
881
     * @return self
882 76
     */
883
    private function setConfirmCode($code)
884 76
    {
885
        $this->lazyLoad();
886 76
887
        return $this->updateProperty($this->confirmCode, 'confirm_code', $code);
888
    }
889
890
    /**
891
     * Set what kind of events the player should be e-mailed about
892
     *
893
     * @param  string $receives The type of notification
894
     * @return self
895
     */
896
    public function setReceives($receives)
897
    {
898
        $this->lazyLoad();
899
900
        return $this->updateProperty($this->receives, 'receives', $receives);
901
    }
902
903
    /**
904
     * Set whether the callsign of the player is outdated
905
     *
906
     * @param  bool $outdated Whether the callsign is outdated
907
     * @return self
908
     */
909
    public function setOutdated($outdated)
910
    {
911 55
        $this->lazyLoad();
912
913 55
        return $this->updateProperty($this->outdated, 'outdated', $outdated);
914 55
    }
915
916
    /**
917
     * Set the player's description
918
     * @param string $description The description
919
     */
920
    public function setDescription($description)
921 29
    {
922
        $this->updateProperty($this->description, "description", $description);
923 29
    }
924 29
925
    /**
926
     * Set the player's timezone
927
     * @param string $timezone The timezone
928
     */
929
    public function setTimezone($timezone)
930
    {
931
        $this->updateProperty($this->timezone, "timezone", $timezone);
932
    }
933
934
    /**
935
     * Set the player's team
936
     * @param int $team The team's ID
937
     */
938
    public function setTeam($team)
939
    {
940
        $this->updateProperty($this->team, "team", $team);
941
    }
942
943
    /**
944
     * Set the match the player last participated in
945
     *
946
     * @param int $match The match's ID
947
     */
948
    public function setLastMatch($match)
949
    {
950
        $this->updateProperty($this->last_match, 'last_match', $match);
951
    }
952
953
    /**
954
     * Set the player's status
955
     * @param string $status The new status
956
     */
957
    public function setStatus($status)
958
    {
959
        $this->updateProperty($this->status, 'status', $status);
960
    }
961
962
    /**
963
     * Set the player's admin notes
964
     * @param  string $admin_notes The new admin notes
965
     * @return self
966
     */
967 1
    public function setAdminNotes($admin_notes)
968
    {
969 1
        return $this->updateProperty($this->admin_notes, 'admin_notes', $admin_notes);
970
    }
971
972
    /**
973
     * Set the player's country
974
     * @param  int   $country The ID of the new country
975
     * @return self
976 1
     */
977
    public function setCountry($country)
978 1
    {
979
        return $this->updateProperty($this->country, 'country', $country);
980
    }
981
982
    /**
983
     * Get the player's chosen theme preference
984
     *
985
     * @return string
986
     */
987
    public function getTheme()
988
    {
989
        $this->lazyLoad();
990
991
        return $this->theme;
992
    }
993
994
    /**
995
     * Set the site theme for the player
996
     *
997
     * If the chosen site theme is invalid, it'll be defaulted to the site default (the first theme defined)
998
     *
999
     * @param string $theme
1000
     */
1001
    public function setTheme($theme)
1002
    {
1003
        $themes = array_column(Service::getSiteThemes(), 'slug');
1004
1005
        if (!in_array($theme, $themes)) {
1006
            $theme = Service::getDefaultSiteTheme();
1007
        }
1008
1009
        return $this->updateProperty($this->theme, 'theme', $theme);
1010
    }
1011
1012
    /**
1013
     * Whether or not this player has color blind assistance enabled.
1014
     *
1015
     * @return bool
1016
     */
1017
    public function hasColorBlindAssist()
1018
    {
1019
        $this->lazyLoad();
1020
1021
        return (bool)$this->color_blind_enabled;
1022
    }
1023
1024
    /**
1025
     * Set a player's setting for color blind assistance.
1026
     *
1027
     * @param bool $enabled
1028
     *
1029
     * @return self
1030
     */
1031
    public function setColorBlindAssist($enabled)
1032
    {
1033
        return $this->updateProperty($this->color_blind_enabled, 'color_blind_enabled', $enabled);
1034
    }
1035
1036
    /**
1037
     * Updates this player's last login
1038
     */
1039
    public function updateLastLogin()
1040
    {
1041
        $this->update("last_login", TimeDate::now()->toMysql());
1042
    }
1043
1044 76
    /**
1045
     * Get the player's username
1046 76
     * @return string The username
1047
     */
1048 76
    public function getUsername()
1049 76
    {
1050 76
        return $this->name;
1051
    }
1052
1053
    /**
1054
     * Get the player's username, safe for use in your HTML
1055
     * @return string The username
1056
     */
1057 76
    public function getEscapedUsername()
1058
    {
1059
        return $this->getEscapedName();
1060
    }
1061
1062
    /**
1063
     * Alias for Player::setUsername()
1064
     *
1065
     * @param  string $username The new username
1066
     * @return self
1067
     */
1068
    public function setName($username)
1069 23
    {
1070
        return $this->setUsername($username);
1071 23
    }
1072
1073
    /**
1074
     * Mark all the unread messages of a player as read
1075
     *
1076
     * @return void
1077
     */
1078
    public function markMessagesAsRead()
1079
    {
1080 1
        $this->db->execute(
1081
            "UPDATE `player_conversations` SET `read` = 1 WHERE `player` = ? AND `read` = 0",
1082 1
            array($this->id)
1083
        );
1084 1
    }
1085
1086
    /**
1087
     * Set the roles of a user
1088
     *
1089
     * @todo   Is it worth making this faster?
1090
     * @param  Role[] $roles The new roles of the user
1091
     * @return self
1092
     */
1093
    public function setRoles($roles)
1094
    {
1095
        $this->lazyLoad();
1096
1097
        $oldRoles = Role::mapToIds($this->roles);
1098
        $this->roles = $roles;
1099
        $roleIds = Role::mapToIds($roles);
1100
1101
        $newRoles     = array_diff($roleIds, $oldRoles);
1102 1
        $removedRoles = array_diff($oldRoles, $roleIds);
1103
1104 1
        foreach ($newRoles as $role) {
1105
            $this->modifyRole($role, 'add');
1106
        }
1107
1108
        foreach ($removedRoles as $role) {
1109
            $this->modifyRole($role, 'remove');
1110
        }
1111
1112
        $this->refresh();
1113
1114
        return $this;
1115
    }
1116
1117
    /**
1118
     * Give or remove a role to/form a player
1119
     *
1120
     * @param int    $role_id The role ID to add or remove
1121
     * @param string $action  Whether to "add" or "remove" a role for a player
1122
     *
1123
     * @return bool Whether the operation was successful or not
1124
     */
1125
    private function modifyRole($role_id, $action)
1126
    {
1127
        $role = Role::get($role_id);
1128
1129
        if ($role->isValid()) {
1130
            if ($action == "add") {
1131
                $this->db->execute("INSERT INTO player_roles (user_id, role_id) VALUES (?, ?)", array($this->getId(), $role_id));
1132
            } elseif ($action == "remove") {
1133
                $this->db->execute("DELETE FROM player_roles WHERE user_id = ? AND role_id = ?", array($this->getId(), $role_id));
1134
            } else {
1135
                throw new Exception("Unrecognized role action");
1136
            }
1137
1138
            return true;
1139
        }
1140
1141
        return false;
1142
    }
1143
1144
    /**
1145
     * Given a player's BZID, get a player object
1146
     *
1147
     * @param  int    $bzid The player's BZID
1148
     * @return Player
1149
     */
1150
    public static function getFromBZID($bzid)
1151
    {
1152
        return self::get(self::fetchIdFrom($bzid, "bzid"));
1153
    }
1154
1155
    /**
1156
     * Get a single player by their username
1157
     *
1158
     * @param  string $username The username to look for
1159
     * @return Player
1160
     */
1161
    public static function getFromUsername($username)
1162
    {
1163
        $player = static::get(self::fetchIdFrom($username, 'username'));
1164
1165
        return $player->inject('name', $username);
1166
    }
1167
1168
    /**
1169
     * Get all the players in the database that have an active status
1170
     * @return Player[] An array of player BZIDs
1171
     */
1172
    public static function getPlayers()
1173
    {
1174
        return self::arrayIdToModel(
1175
            self::fetchIdsFrom("status", array("active", "test"), false)
1176
        );
1177
    }
1178
1179
    /**
1180
     * Show the number of notifications the user hasn't read yet
1181
     * @return int
1182
     */
1183
    public function countUnreadNotifications()
1184
    {
1185
        return Notification::countUnreadNotifications($this->id);
1186
    }
1187
1188
    /**
1189
     * Count the number of matches a player has participated in
1190
     * @return int
1191
     */
1192
    public function getMatchCount()
1193
    {
1194
        if ($this->cachedMatchCount === null) {
1195
            $this->cachedMatchCount = Match::getQueryBuilder()
1196
                ->active()
1197
                ->with($this)
1198
                ->count();
1199
        }
1200
1201
        return $this->cachedMatchCount;
1202
    }
1203
1204
    /**
1205
     * Get the (victory/total matches) ratio of the player
1206
     * @return float
1207
     */
1208
    public function getMatchWinRatio()
1209
    {
1210
        $count = $this->getMatchCount();
1211
1212
        if ($count == 0) {
1213
            return 0;
1214
        }
1215
1216
        $wins = Match::getQueryBuilder()
1217
            ->active()
1218
            ->with($this, 'win')
1219
            ->count();
1220
1221
        return $wins / $count;
1222
    }
1223
1224
    /**
1225
     * Get the (total caps made by team/total matches) ratio of the player
1226
     * @return float
1227
     */
1228
    public function getMatchAverageCaps()
1229 1
    {
1230
        $count = $this->getMatchCount();
1231 1
1232 1
        if ($count == 0) {
1233
            return 0;
1234
        }
1235
1236
        // Get the sum of team A points if the player was in team A, team B points if the player was in team B
1237
        $query = $this->db->query("
1238
            SELECT
1239
              SUM(
1240
                IF(mp.team_loyalty = 0, team_a_points, team_b_points)
1241 2
              ) AS sum
1242
            FROM
1243 2
              matches
1244 2
            INNER JOIN
1245
              match_participation mp ON mp.match_id = matches.id
1246
            WHERE
1247
              status = 'entered' AND mp.user_id = ?
1248
        ", [$this->id]);
1249
1250
        return $query[0]['sum'] / $count;
1251 1
    }
1252
1253 1
    /**
1254
     * Get the match activity in matches per day for a player
1255
     *
1256
     * @return float
1257
     */
1258
    public function getMatchActivity()
1259 76
    {
1260
        if ($this->matchActivity !== null) {
1261
            return $this->matchActivity;
1262 76
        }
1263
1264
        $activity = 0.0;
1265
1266
        $matches = Match::getQueryBuilder()
1267
            ->active()
1268
            ->with($this)
1269
            ->where('time')->isAfter(TimeDate::from('45 days ago'))
1270
            ->getModels($fast = true);
1271
1272 76
        foreach ($matches as $match) {
1273
            $activity += $match->getActivity();
1274
        }
1275
1276
        return $activity;
1277
    }
1278 76
1279
    /**
1280
     * Return an array of matches this player participated in per month.
1281 76
     *
1282
     * ```
1283
     * ['yyyy-mm'] = <number of matches>
1284
     * ```
1285
     *
1286
     * @param TimeDate|string $timePeriod
1287
     *
1288
     * @return int[]
1289
     */
1290
    public function getMatchSummary($timePeriod = '1 year ago')
1291
    {
1292
        $since = ($timePeriod instanceof TimeDate) ? $timePeriod : TimeDate::from($timePeriod);
1293
1294 76
        if (!isset($this->cachedMatchSummary[(string)$timePeriod])) {
1295
            $this->cachedMatchSummary[(string)$timePeriod] = Match::getQueryBuilder()
1296
                ->active()
1297
                ->with($this)
1298
                ->where('time')->isAfter($since)
1299
                ->getSummary($since)
1300
            ;
1301
        }
1302
1303
        return $this->cachedMatchSummary[(string)$timePeriod];
1304
    }
1305
1306
    /**
1307
     * Show the number of messages the user hasn't read yet
1308
     * @return int
1309
     */
1310
    public function countUnreadMessages()
1311
    {
1312
        return $this->fetchCount("WHERE `player` = ? AND `read` = 0",
1313
            $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...
1314
        );
1315
    }
1316
1317
    /**
1318
     * Get all of the members belonging to a team
1319
     * @param  int      $teamID The ID of the team to fetch the members of
1320
     * @return Player[] An array of Player objects of the team members
1321
     */
1322
    public static function getTeamMembers($teamID)
1323
    {
1324
        return self::arrayIdToModel(
1325
            self::fetchIds("WHERE team = ?", array($teamID))
1326
        );
1327
    }
1328
1329 76
    /**
1330
     * {@inheritdoc}
1331 76
     */
1332 76
    public static function getActiveStatuses()
1333 76
    {
1334
        return array('active', 'reported', 'test');
1335 76
    }
1336 76
1337 76
    /**
1338 76
     * {@inheritdoc}
1339 76
     */
1340 76
    public static function getEagerColumns($prefix = null)
1341 76
    {
1342 76
        $columns = [
1343 76
            'id',
1344 76
            'bzid',
1345 76
            'team',
1346 76
            'username',
1347
            'alias',
1348
            'status',
1349 76
            'avatar',
1350 76
            'country',
1351 76
        ];
1352
1353 76
        return self::formatColumns($prefix, $columns);
1354
    }
1355
1356
    /**
1357
     * {@inheritdoc}
1358
     */
1359
    public static function getLazyColumns($prefix = null)
1360
    {
1361
        $columns = [
1362
            'email',
1363
            'verified',
1364
            'receives',
1365
            'confirm_code',
1366
            'outdated',
1367
            'description',
1368
            'theme',
1369
            'color_blind_enabled',
1370
            'timezone',
1371
            'joined',
1372
            'last_login',
1373 76
            'last_match',
1374
            'admin_notes',
1375
        ];
1376
1377 76
        return self::formatColumns($prefix, $columns);
1378
    }
1379
1380 76
    /**
1381
     * Get a query builder for players
1382 76
     * @return PlayerQueryBuilder
1383
     */
1384 76
    public static function getQueryBuilder()
1385
    {
1386
        return new PlayerQueryBuilder('Player', array(
1387
            'columns' => array(
1388
                'name'     => 'username',
1389
                'team'     => 'team',
1390
                'outdated' => 'outdated',
1391
                'status'   => 'status',
1392
            ),
1393
            'name' => 'name',
1394
        ));
1395
    }
1396
1397
    /**
1398
     * Enter a new player to the database
1399
     * @param  int              $bzid        The player's bzid
1400
     * @param  string           $username    The player's username
1401
     * @param  int              $team        The player's team
1402
     * @param  string           $status      The player's status
1403
     * @param  int              $role_id     The player's role when they are first created
1404
     * @param  string           $avatar      The player's profile avatar
1405
     * @param  string           $description The player's profile description
1406
     * @param  int              $country     The player's country
1407
     * @param  string           $timezone    The player's timezone
1408
     * @param  string|\TimeDate $joined      The date the player joined
1409
     * @param  string|\TimeDate $last_login  The timestamp of the player's last login
1410
     * @return Player           An object representing the player that was just entered
1411 1
     */
1412 1
    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")
1413 1
    {
1414
        $joined = TimeDate::from($joined);
1415
        $last_login = TimeDate::from($last_login);
1416
        $timezone = ($timezone) ?: date_default_timezone_get();
1417
1418
        $player = self::create(array(
1419
            'bzid'        => $bzid,
1420 76
            'team'        => $team,
1421
            'username'    => $username,
1422 76
            'alias'       => self::generateAlias($username),
1423
            'status'      => $status,
1424 76
            'avatar'      => $avatar,
1425 76
            'description' => $description,
1426
            'country'     => $country,
1427
            'timezone'    => $timezone,
1428
            'joined'      => $joined->toMysql(),
1429
            'last_login'  => $last_login->toMysql(),
1430
        ));
1431
1432
        $player->addRole($role_id);
1433
        $player->getIdenticon($player->getId());
1434 1
        $player->setUsername($username);
1435
1436 1
        return $player;
1437
    }
1438
1439
    /**
1440
     * Determine if a player exists in the database
1441
     * @param  int  $bzid The player's bzid
1442
     * @return bool Whether the player exists in the database
1443
     */
1444
    public static function playerBZIDExists($bzid)
1445
    {
1446
        return self::getFromBZID($bzid)->isValid();
1447 1
    }
1448
1449 1
    /**
1450
     * Change a player's callsign and add it to the database if it does not
1451
     * exist as a past callsign
1452 1
     *
1453
     * @param  string $username The new username of the player
1454
     * @return self
1455
     */
1456
    public function setUsername($username)
1457
    {
1458
        // The player's username was just fetched from BzDB, it's definitely not
1459
        // outdated
1460
        $this->setOutdated(false);
1461
1462 1
        // Players who have this player's username are considered outdated
1463
        $this->db->execute("UPDATE {$this->table} SET outdated = 1 WHERE username = ? AND id != ?", array($username, $this->id));
1464 1
1465
        if ($username === $this->name) {
1466
            // The player's username hasn't changed, no need to do anything
1467
            return $this;
1468
        }
1469
1470
        // Players who used to have our player's username are not outdated anymore,
1471
        // unless they are more than one.
1472
        // Even though we are sure that the old and new usernames are not equal,
1473 1
        // MySQL makes a different type of string equality tests, which is why we
1474
        // also check IDs to make sure not to affect our own player's outdatedness.
1475 1
        $this->db->execute("
1476
            UPDATE {$this->table} SET outdated =
1477
                (SELECT (COUNT(*)>1) FROM (SELECT 1 FROM {$this->table} WHERE username = ? AND id != ?) t)
1478
            WHERE username = ? AND id != ?",
1479
            array($this->name, $this->id, $this->name, $this->id));
1480
1481
        $this->updateProperty($this->name, 'username', $username);
1482
        $this->db->execute("INSERT IGNORE INTO past_callsigns (player, username) VALUES (?, ?)", array($this->id, $username));
1483
        $this->resetAlias();
1484
1485
        return $this;
1486
    }
1487
1488
    /**
1489
     * Alphabetical order function for use in usort (case-insensitive)
1490
     * @return Closure The sort function
1491
     */
1492
    public static function getAlphabeticalSort()
1493
    {
1494
        return function (Player $a, Player $b) {
1495
            return strcasecmp($a->getUsername(), $b->getUsername());
1496
        };
1497
    }
1498
1499
    /**
1500
     * {@inheritdoc}
1501
     * @todo Add a constraint that does this automatically
1502
     */
1503
    public function wipe()
1504
    {
1505
        $this->db->execute("DELETE FROM past_callsigns WHERE player = ?", $this->id);
1506
1507
        parent::wipe();
1508
    }
1509
1510
    /**
1511
     * Find whether the player can delete a model
1512
     *
1513
     * @param  PermissionModel $model       The model that will be seen
1514
     * @param  bool         $showDeleted Whether to show deleted models to admins
1515
     * @return bool
1516
     */
1517
    public function canSee($model, $showDeleted = false)
1518
    {
1519
        return $model->canBeSeenBy($this, $showDeleted);
1520
    }
1521
1522
    /**
1523
     * Find whether the player can delete a model
1524
     *
1525
     * @param  PermissionModel $model The model that will be deleted
1526
     * @param  bool         $hard  Whether to check for hard-delete perms, as opposed
1527
     *                                to soft-delete ones
1528
     * @return bool
1529
     */
1530
    public function canDelete($model, $hard = false)
1531
    {
1532
        if ($hard) {
1533
            return $model->canBeHardDeletedBy($this);
1534
        } else {
1535
            return $model->canBeSoftDeletedBy($this);
1536
        }
1537
    }
1538
1539
    /**
1540
     * Find whether the player can create a model
1541
     *
1542
     * @param  string  $modelName The PHP class identifier of the model type
1543
     * @return bool
1544
     */
1545
    public function canCreate($modelName)
1546
    {
1547
        return $modelName::canBeCreatedBy($this);
1548
    }
1549
1550
    /**
1551
     * Find whether the player can edit a model
1552
     *
1553
     * @param  PermissionModel $model The model which will be edited
1554
     * @return bool
1555
     */
1556
    public function canEdit($model)
1557
    {
1558
        return $model->canBeEditedBy($this);
1559
    }
1560
}
1561