Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Failed Conditions
Pull Request — main (#1494)
by Dan
08:06 queued 03:15
created

SmrAccount::getUserRankingHREF()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
1
<?php declare(strict_types=1);
2
3
use Smr\Database;
4
use Smr\Epoch;
5
use Smr\Exceptions\AccountNotFound;
6
use Smr\Exceptions\UserError;
7
use Smr\Pages\Account\HallOfFamePersonal;
8
use Smr\SocialLogin\SocialLogin;
9
use Smr\UserRanking;
10
11
class SmrAccount {
12
13
	protected const USER_RANKINGS_EACH_STAT_POW = .9;
14
	protected const USER_RANKINGS_SCORE = [
15
		// [Stat, a, b]
16
		// Used as: pow(Stat * a, USER_RANKINGS_EACH_STAT_POW) * b
17
		[['Trade', 'Experience', 'Total'], .1, 0.5],
18
		[['Trade', 'Money', 'Profit'], 0.00005, 0.5],
19
		[['Killing', 'Kills'], 1000, 1],
20
	];
21
22
	/** @var array<int, self> */
23
	protected static array $CACHE_ACCOUNTS = [];
24
	protected const DEFAULT_HOTKEYS = [
25
		'MoveUp' => ['w', 'up'],
26
		'ScanUp' => ['shift+w', 'shift+up'],
27
		'MoveLeft' => ['a', 'left'],
28
		'ScanLeft' => ['shift+a', 'shift+left'],
29
		'MoveRight' => ['d', 'right'],
30
		'ScanRight' => ['shift+d', 'shift+right'],
31
		'MoveDown' => ['s', 'down'],
32
		'ScanDown' => ['shift+s', 'shift+down'],
33
		'MoveWarp' => ['e', '0'],
34
		'ScanWarp' => ['shift+e', 'shift+0'],
35
		'ScanCurrent' => ['shift+1'],
36
		'CurrentSector' => ['1'],
37
		'LocalMap' => ['2'],
38
		'PlotCourse' => ['3'],
39
		'CurrentPlayers' => ['4'],
40
		'EnterPort' => ['q'],
41
		'AttackTrader' => ['f'],
42
	];
43
44
	protected Database $db;
45
	protected readonly string $SQL;
46
47
	protected string $login;
48
	protected string $passwordHash;
49
	protected string $email;
50
	protected bool $validated;
51
	protected string $validation_code;
52
	protected int $last_login;
53
	protected string $hofName;
54
	protected ?string $discordId;
55
	protected ?string $ircNick;
56
	protected bool $veteranForced;
57
	protected bool $logging;
58
	protected int $offset;
59
	protected bool $images;
60
	protected int $fontSize;
61
	protected string $passwordReset;
62
	protected int $points;
63
	protected bool $useAJAX;
64
	protected int $mailBanned;
65
	/** @var array<string, float> */
66
	protected array $HOF;
67
	/** @var array<int, array<array<string, mixed>>> */
68
	protected array $individualScores;
69
	protected int $score;
70
	protected ?string $cssLink;
71
	protected bool $defaultCSSEnabled;
72
	/** @var ?array<int, int> */
73
	protected ?array $messageNotifications;
74
	protected bool $centerGalaxyMapOnPlayer;
75
	/** @var array<string, int> */
76
	protected array $oldAccountIDs = [];
77
	protected int $maxRankAchieved;
78
	protected int $referrerID;
79
	protected int $credits; // SMR credits
80
	protected int $rewardCredits; // SMR reward credits
81
	protected string $dateFormat;
82
	protected string $timeFormat;
83
	protected string $template;
84
	protected string $colourScheme;
85
	/** @var array<string, array<string>> */
86
	protected array $hotkeys;
87
	/** @var array<int, bool> */
88
	protected array $permissions;
89
	protected string $friendlyColour;
90
	protected string $neutralColour;
91
	protected string $enemyColour;
92
93
	protected bool $npc;
94
95
	protected bool $hasChanged;
96
97
	/**
98
	 * @return array<string, array<string>>
99
	 */
100
	public static function getDefaultHotkeys(): array {
101
		return self::DEFAULT_HOTKEYS;
102
	}
103
104
	public static function clearCache(): void {
105
		self::$CACHE_ACCOUNTS = [];
106
	}
107
108
	public static function getAccount(int $accountID, bool $forceUpdate = false): self {
109
		if ($forceUpdate || !isset(self::$CACHE_ACCOUNTS[$accountID])) {
110
			self::$CACHE_ACCOUNTS[$accountID] = new self($accountID);
111
		}
112
		return self::$CACHE_ACCOUNTS[$accountID];
113
	}
114
115
	public static function getAccountByLogin(string $login, bool $forceUpdate = false): self {
116
		if (!empty($login)) {
117
			$db = Database::getInstance();
118
			$dbResult = $db->read('SELECT account_id FROM account WHERE login = ' . $db->escapeString($login));
119
			if ($dbResult->hasRecord()) {
120
				$accountID = $dbResult->record()->getInt('account_id');
121
				return self::getAccount($accountID, $forceUpdate);
122
			}
123
		}
124
		throw new AccountNotFound('Account login not found.');
125
	}
126
127
	public static function getAccountByHofName(string $hofName, bool $forceUpdate = false): self {
128
		if (!empty($hofName)) {
129
			$db = Database::getInstance();
130
			$dbResult = $db->read('SELECT account_id FROM account WHERE hof_name = ' . $db->escapeString($hofName));
131
			if ($dbResult->hasRecord()) {
132
				$accountID = $dbResult->record()->getInt('account_id');
133
				return self::getAccount($accountID, $forceUpdate);
134
			}
135
		}
136
		throw new AccountNotFound('Account HoF name not found.');
137
	}
138
139
	public static function getAccountByEmail(?string $email, bool $forceUpdate = false): self {
140
		if (!empty($email)) {
141
			$db = Database::getInstance();
142
			$dbResult = $db->read('SELECT account_id FROM account WHERE email = ' . $db->escapeString($email));
143
			if ($dbResult->hasRecord()) {
144
				$accountID = $dbResult->record()->getInt('account_id');
145
				return self::getAccount($accountID, $forceUpdate);
146
			}
147
		}
148
		throw new AccountNotFound('Account email not found.');
149
	}
150
151
	public static function getAccountByDiscordId(?string $id, bool $forceUpdate = false): self {
152
		if (!empty($id)) {
153
			$db = Database::getInstance();
154
			$dbResult = $db->read('SELECT account_id FROM account where discord_id = ' . $db->escapeString($id));
155
			if ($dbResult->hasRecord()) {
156
				$accountID = $dbResult->record()->getInt('account_id');
157
				return self::getAccount($accountID, $forceUpdate);
158
			}
159
		}
160
		throw new AccountNotFound('Account discord ID not found.');
161
	}
162
163
	public static function getAccountByIrcNick(?string $nick, bool $forceUpdate = false): self {
164
		if (!empty($nick)) {
165
			$db = Database::getInstance();
166
			$dbResult = $db->read('SELECT account_id FROM account WHERE irc_nick = ' . $db->escapeString($nick));
167
			if ($dbResult->hasRecord()) {
168
				$accountID = $dbResult->record()->getInt('account_id');
169
				return self::getAccount($accountID, $forceUpdate);
170
			}
171
		}
172
		throw new AccountNotFound('Account IRC nick not found.');
173
	}
174
175
	public static function getAccountBySocialLogin(SocialLogin $social, bool $forceUpdate = false): self {
176
		if ($social->isValid()) {
177
			$db = Database::getInstance();
178
			$dbResult = $db->read('SELECT account_id FROM account JOIN account_auth USING(account_id)
179
				WHERE login_type = ' . $db->escapeString($social->getLoginType()) . '
180
				AND auth_key = ' . $db->escapeString($social->getUserID()));
181
			if ($dbResult->hasRecord()) {
182
				$accountID = $dbResult->record()->getInt('account_id');
183
				return self::getAccount($accountID, $forceUpdate);
184
			}
185
		}
186
		throw new AccountNotFound('Account social login not found.');
187
	}
188
189
	public static function createAccount(string $login, string $password, string $email, int $timez, int $referral): self {
190
		if ($referral != 0) {
191
			// Will throw if referral account doesn't exist
192
			self::getAccount($referral);
193
		}
194
		$db = Database::getInstance();
195
		$passwordHash = password_hash($password, PASSWORD_DEFAULT);
196
		$db->insert('account', [
197
			'login' => $db->escapeString($login),
198
			'password' => $db->escapeString($passwordHash),
199
			'email' => $db->escapeString($email),
200
			'validation_code' => $db->escapeString(random_string(10)),
0 ignored issues
show
Bug introduced by
The function random_string was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

200
			'validation_code' => $db->escapeString(/** @scrutinizer ignore-call */ random_string(10)),
Loading history...
201
			'last_login' => $db->escapeNumber(Epoch::time()),
202
			'offset' => $db->escapeNumber($timez),
203
			'referral_id' => $db->escapeNumber($referral),
204
			'hof_name' => $db->escapeString($login),
205
			'hotkeys' => $db->escapeObject([]),
206
		]);
207
		return self::getAccountByLogin($login);
208
	}
209
210
	/**
211
	 * @return array<string, string>
212
	 */
213
	public static function getUserScoreCaseStatement(Database $db): array {
214
		$userRankingTypes = [];
215
		$case = 'IFNULL(FLOOR(SUM(CASE type ';
216
		foreach (self::USER_RANKINGS_SCORE as [$stat, $a, $b]) {
217
			$userRankingType = $db->escapeString(implode(':', $stat));
218
			$userRankingTypes[] = $userRankingType;
219
			$case .= ' WHEN ' . $userRankingType . ' THEN POW(amount*' . $a . ',' . self::USER_RANKINGS_EACH_STAT_POW . ')*' . $b;
220
		}
221
		$case .= ' END)), 0)';
222
		return ['CASE' => $case, 'IN' => implode(',', $userRankingTypes)];
223
	}
224
225
	protected function __construct(protected readonly int $accountID) {
226
		$this->db = Database::getInstance();
227
		$this->SQL = 'account_id = ' . $this->db->escapeNumber($accountID);
0 ignored issues
show
Bug introduced by
The property SQL is declared read-only in SmrAccount.
Loading history...
228
		$dbResult = $this->db->read('SELECT * FROM account WHERE ' . $this->SQL);
229
230
		if ($dbResult->hasRecord()) {
231
			$dbRecord = $dbResult->record();
232
233
			$this->login = $dbRecord->getString('login');
234
			$this->passwordHash = $dbRecord->getString('password');
235
			$this->email = $dbRecord->getString('email');
236
			$this->validated = $dbRecord->getBoolean('validated');
237
238
			$this->last_login = $dbRecord->getInt('last_login');
239
			$this->validation_code = $dbRecord->getString('validation_code');
240
			$this->veteranForced = $dbRecord->getBoolean('veteran');
241
			$this->logging = $dbRecord->getBoolean('logging');
242
			$this->offset = $dbRecord->getInt('offset');
243
			$this->images = $dbRecord->getBoolean('images');
244
			$this->fontSize = $dbRecord->getInt('fontsize');
245
246
			$this->passwordReset = $dbRecord->getString('password_reset');
247
			$this->useAJAX = $dbRecord->getBoolean('use_ajax');
248
			$this->mailBanned = $dbRecord->getInt('mail_banned');
249
250
			$this->friendlyColour = $dbRecord->getString('friendly_colour');
251
			$this->neutralColour = $dbRecord->getString('neutral_colour');
252
			$this->enemyColour = $dbRecord->getString('enemy_colour');
253
254
			$this->cssLink = $dbRecord->getNullableString('css_link');
255
			$this->defaultCSSEnabled = $dbRecord->getBoolean('default_css_enabled');
256
			$this->centerGalaxyMapOnPlayer = $dbRecord->getBoolean('center_galaxy_map_on_player');
257
258
			$this->messageNotifications = $dbRecord->getObject('message_notifications', false, true);
259
			$this->hotkeys = $dbRecord->getObject('hotkeys');
260
			foreach (self::DEFAULT_HOTKEYS as $hotkey => $binding) {
261
				if (!isset($this->hotkeys[$hotkey])) {
262
					$this->hotkeys[$hotkey] = $binding;
263
				}
264
			}
265
266
			foreach (Globals::getHistoryDatabases() as $databaseName => $oldColumn) {
267
				$this->oldAccountIDs[$databaseName] = $dbRecord->getInt($oldColumn);
268
			}
269
270
			$this->referrerID = $dbRecord->getInt('referral_id');
271
			$this->maxRankAchieved = $dbRecord->getInt('max_rank_achieved');
272
273
			$this->hofName = $dbRecord->getString('hof_name');
274
			$this->discordId = $dbRecord->getNullableString('discord_id');
275
			$this->ircNick = $dbRecord->getNullableString('irc_nick');
276
277
			$this->dateFormat = $dbRecord->getString('date_format');
278
			$this->timeFormat = $dbRecord->getString('time_format');
279
280
			$this->template = $dbRecord->getString('template');
281
			$this->colourScheme = $dbRecord->getString('colour_scheme');
282
283
			if (empty($this->hofName)) {
284
				$this->hofName = $this->login;
285
			}
286
		} else {
287
			throw new AccountNotFound('Account ID ' . $accountID . ' does not exist!');
288
		}
289
	}
290
291
	/**
292
	 * Check if the account is disabled.
293
	 *
294
	 * @return array<string, mixed>|false
295
	 */
296
	public function isDisabled(): array|false {
297
		$dbResult = $this->db->read('SELECT * FROM account_is_closed JOIN closing_reason USING(reason_id) WHERE ' . $this->SQL);
298
		if (!$dbResult->hasRecord()) {
299
			return false;
300
		}
301
		$dbRecord = $dbResult->record();
302
		// get the expire time
303
		$expireTime = $dbRecord->getInt('expires');
304
305
		// are we over this time?
306
		if ($expireTime > 0 && $expireTime < Epoch::time()) {
307
			// get rid of the expire entry
308
			$this->unbanAccount();
309
			return false;
310
		}
311
		return [
312
			'Time' => $expireTime,
313
			'Reason' => $dbRecord->getString('reason'),
314
			'ReasonID' => $dbRecord->getInt('reason_id'),
315
		];
316
	}
317
318
	public function update(): void {
319
		$this->db->write('UPDATE account SET email = ' . $this->db->escapeString($this->email) .
320
			', validation_code = ' . $this->db->escapeString($this->validation_code) .
321
			', validated = ' . $this->db->escapeBoolean($this->validated) .
322
			', password = ' . $this->db->escapeString($this->passwordHash) .
323
			', images = ' . $this->db->escapeBoolean($this->images) .
324
			', password_reset = ' . $this->db->escapeString($this->passwordReset) .
325
			', use_ajax=' . $this->db->escapeBoolean($this->useAJAX) .
326
			', mail_banned=' . $this->db->escapeNumber($this->mailBanned) .
327
			', max_rank_achieved=' . $this->db->escapeNumber($this->maxRankAchieved) .
328
			', default_css_enabled=' . $this->db->escapeBoolean($this->defaultCSSEnabled) .
329
			', center_galaxy_map_on_player=' . $this->db->escapeBoolean($this->centerGalaxyMapOnPlayer) .
330
			', message_notifications=' . $this->db->escapeObject($this->messageNotifications, false, true) .
331
			', hotkeys=' . $this->db->escapeObject($this->hotkeys) .
332
			', last_login = ' . $this->db->escapeNumber($this->last_login) .
333
			', logging = ' . $this->db->escapeBoolean($this->logging) .
334
			', time_format = ' . $this->db->escapeString($this->timeFormat) .
335
			', date_format = ' . $this->db->escapeString($this->dateFormat) .
336
			', discord_id = ' . $this->db->escapeString($this->discordId, true) .
337
			', irc_nick = ' . $this->db->escapeString($this->ircNick, true) .
338
			', hof_name = ' . $this->db->escapeString($this->hofName) .
339
			', template = ' . $this->db->escapeString($this->template) .
340
			', colour_scheme = ' . $this->db->escapeString($this->colourScheme) .
341
			', fontsize = ' . $this->db->escapeNumber($this->fontSize) .
342
			', css_link = ' . $this->db->escapeString($this->cssLink, true) .
343
			', friendly_colour = ' . $this->db->escapeString($this->friendlyColour, true) .
344
			', neutral_colour = ' . $this->db->escapeString($this->neutralColour, true) .
345
			', enemy_colour = ' . $this->db->escapeString($this->enemyColour, true) .
346
			' WHERE ' . $this->SQL);
347
		$this->hasChanged = false;
348
	}
349
350
	public function updateIP(): void {
351
		$curr_ip = getIpAddress();
0 ignored issues
show
Bug introduced by
The function getIpAddress was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

351
		$curr_ip = /** @scrutinizer ignore-call */ getIpAddress();
Loading history...
352
		$this->log(LOG_TYPE_LOGIN, 'logged in from ' . $curr_ip);
353
354
		// more than 50 elements in it?
355
356
		$dbResult = $this->db->read('SELECT time,ip FROM account_has_ip WHERE ' . $this->SQL . ' ORDER BY time ASC');
357
		if ($dbResult->getNumRecords() > 50) {
358
			$dbRecord = $dbResult->records()->current();
359
			$delete_time = $dbRecord->getInt('time');
360
			$delete_ip = $dbRecord->getString('ip');
361
362
			$this->db->write('DELETE FROM account_has_ip
363
				WHERE ' . $this->SQL . ' AND
364
				time = ' . $this->db->escapeNumber($delete_time) . ' AND
365
				ip = ' . $this->db->escapeString($delete_ip));
366
		}
367
		[$fi, $se, $th, $fo] = preg_split('/[.\s,]/', $curr_ip, 4);
368
		if ($curr_ip != 'unknown' && $curr_ip != 'unknown...' && $curr_ip != 'unknown, unknown') {
369
			$curr_ip = $fi . '.' . $se . '.' . $th . '.' . $fo;
370
			$host = gethostbyaddr($curr_ip);
371
		} else {
372
			$host = 'unknown';
373
		}
374
375
		// save...first make sure there isn't one for these keys (someone could double click and get error)
376
		$this->db->replace('account_has_ip', [
377
			'account_id' => $this->db->escapeNumber($this->accountID),
378
			'time' => $this->db->escapeNumber(Epoch::time()),
379
			'ip' => $this->db->escapeString($curr_ip),
380
			'host' => $this->db->escapeString($host),
381
		]);
382
	}
383
384
	public function updateLastLogin(): void {
385
		if ($this->last_login == Epoch::time()) {
386
			return;
387
		}
388
		$this->last_login = Epoch::time();
389
		$this->hasChanged = true;
390
		$this->update();
391
	}
392
393
	public function getLastLogin(): int {
394
		return $this->last_login;
395
	}
396
397
	public function setLoggingEnabled(bool $bool): void {
398
		if ($this->logging === $bool) {
399
			return;
400
		}
401
		$this->logging = $bool;
402
		$this->hasChanged = true;
403
	}
404
405
	public function isLoggingEnabled(): bool {
406
		return $this->logging;
407
	}
408
409
	public function isVeteranForced(): bool {
410
		return $this->veteranForced;
411
	}
412
413
	public function isVeteran(): bool {
414
		// Use maxRankAchieved to avoid a database call to get user stats.
415
		// This saves a lot of time on the CPL, Rankings, Rosters, etc.
416
		return $this->isVeteranForced() || $this->maxRankAchieved >= FLEDGLING;
417
	}
418
419
	public function isNPC(): bool {
420
		if (!isset($this->npc)) {
421
			$dbResult = $this->db->read('SELECT 1 FROM npc_logins WHERE login = ' . $this->db->escapeString($this->getLogin()));
422
			$this->npc = $dbResult->hasRecord();
423
		}
424
		return $this->npc;
425
	}
426
427
	protected function getHOFData(): void {
428
		if (!isset($this->HOF)) {
429
			//Get Player HOF
430
			$dbResult = $this->db->read('SELECT type,sum(amount) as amount FROM player_hof WHERE ' . $this->SQL . ' AND game_id IN (SELECT game_id FROM game WHERE ignore_stats = \'FALSE\') GROUP BY type');
431
			$this->HOF = [];
432
			foreach ($dbResult->records() as $dbRecord) {
433
				$this->HOF[$dbRecord->getString('type')] = $dbRecord->getFloat('amount');
434
			}
435
		}
436
	}
437
438
	/**
439
	 * @param array<string> $typeList
440
	 */
441
	public function getHOF(array $typeList): float {
442
		$this->getHOFData();
443
		return $this->HOF[implode(':', $typeList)] ?? 0;
444
	}
445
446
	public function getScore(): int {
447
		if (!isset($this->score)) {
448
			$score = 0;
449
			foreach ($this->getIndividualScores() as $each) {
450
				$score += $each['Score'];
451
			}
452
			$this->score = IRound($score);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

452
			$this->score = /** @scrutinizer ignore-call */ IRound($score);
Loading history...
453
		}
454
		return $this->score;
455
	}
456
457
	/**
458
	 * @return array<int, array<array<string, mixed>>>
459
	 */
460
	public function getIndividualScores(SmrPlayer $player = null): array {
461
		$gameID = 0;
462
		if ($player != null) {
463
			$gameID = $player->getGameID();
464
		}
465
		if (!isset($this->individualScores[$gameID])) {
466
			$this->individualScores[$gameID] = [];
467
			foreach (self::USER_RANKINGS_SCORE as [$stat, $a, $b]) {
468
				if ($player == null) {
469
					$amount = $this->getHOF($stat);
470
				} else {
471
					$amount = $player->getHOF($stat);
472
				}
473
				$this->individualScores[$gameID][] = [
474
					'Stat' => $stat,
475
					'Score' => pow($amount * $a, self::USER_RANKINGS_EACH_STAT_POW) * $b,
476
				];
477
			}
478
		}
479
		return $this->individualScores[$gameID];
480
	}
481
482
	public function getRank(): UserRanking {
483
		$rank = UserRanking::getRankFromScore($this->getScore());
484
		if ($rank->value > $this->maxRankAchieved) {
485
			$this->updateMaxRankAchieved($rank->value);
486
		}
487
		return $rank;
488
	}
489
490
	protected function updateMaxRankAchieved(int $rank): void {
491
		if ($rank <= $this->maxRankAchieved) {
492
			throw new Exception('Trying to set max rank achieved to a lower value: ' . $rank);
493
		}
494
		$delta = $rank - $this->maxRankAchieved;
495
		if ($this->hasReferrer()) {
496
			$this->getReferrer()->increaseSmrRewardCredits($delta * CREDITS_PER_DOLLAR);
497
		}
498
		$this->maxRankAchieved += $delta;
499
		$this->hasChanged = true;
500
		$this->update();
501
	}
502
503
	public function getReferrerID(): int {
504
		return $this->referrerID;
505
	}
506
507
	public function hasReferrer(): bool {
508
		return $this->referrerID > 0;
509
	}
510
511
	public function getReferrer(): self {
512
		return self::getAccount($this->getReferrerID());
513
	}
514
515
	public function log(int $log_type_id, string $msg, int $sector_id = 0): void {
516
		if ($this->isLoggingEnabled()) {
517
			$this->db->insert('account_has_logs', [
518
				'account_id' => $this->db->escapeNumber($this->accountID),
519
				'microtime' => $this->db->escapeNumber(Epoch::microtime()),
520
				'log_type_id' => $this->db->escapeNumber($log_type_id),
521
				'message' => $this->db->escapeString($msg),
522
				'sector_id' => $this->db->escapeNumber($sector_id),
523
			]);
524
		}
525
	}
526
527
	protected function getSmrCreditsData(): void {
528
		if (!isset($this->credits) || !isset($this->rewardCredits)) {
529
			$this->credits = 0;
530
			$this->rewardCredits = 0;
531
			$dbResult = $this->db->read('SELECT * FROM account_has_credits WHERE ' . $this->SQL);
532
			if ($dbResult->hasRecord()) {
533
				$dbRecord = $dbResult->record();
534
				$this->credits = $dbRecord->getInt('credits_left');
535
				$this->rewardCredits = $dbRecord->getInt('reward_credits');
536
			}
537
		}
538
	}
539
540
	public function getTotalSmrCredits(): int {
541
		return $this->getSmrCredits() + $this->getSmrRewardCredits();
542
	}
543
544
	public function decreaseTotalSmrCredits(int $totalCredits): void {
545
		if ($totalCredits == 0) {
546
			return;
547
		}
548
		if ($totalCredits < 0) {
549
			throw new Exception('You cannot use negative total credits');
550
		}
551
		if ($totalCredits > $this->getTotalSmrCredits()) {
552
			throw new Exception('You do not have that many credits in total to use');
553
		}
554
555
		$rewardCredits = $this->rewardCredits;
556
		$credits = $this->credits;
557
		$rewardCredits -= $totalCredits;
558
		if ($rewardCredits < 0) {
559
			$credits += $rewardCredits;
560
			$rewardCredits = 0;
561
		}
562
		if ($this->credits == 0 && $this->rewardCredits == 0) {
563
			$this->db->replace('account_has_credits', [
564
				'account_id' => $this->db->escapeNumber($this->getAccountID()),
565
				'credits_left' => $this->db->escapeNumber($credits),
566
				'reward_credits' => $this->db->escapeNumber($rewardCredits),
567
			]);
568
		} else {
569
			$this->db->write('UPDATE account_has_credits SET credits_left=' . $this->db->escapeNumber($credits) . ', reward_credits=' . $this->db->escapeNumber($rewardCredits) . ' WHERE ' . $this->SQL);
570
		}
571
		$this->credits = $credits;
572
		$this->rewardCredits = $rewardCredits;
573
	}
574
575
	public function getSmrCredits(): int {
576
		$this->getSmrCreditsData();
577
		return $this->credits;
578
	}
579
580
	public function getSmrRewardCredits(): int {
581
		$this->getSmrCreditsData();
582
		return $this->rewardCredits;
583
	}
584
585
	public function setSmrCredits(int $credits): void {
586
		if ($this->getSmrCredits() == $credits) {
587
			return;
588
		}
589
		if ($this->credits == 0 && $this->rewardCredits == 0) {
590
			$this->db->replace('account_has_credits', [
591
				'account_id' => $this->db->escapeNumber($this->getAccountID()),
592
				'credits_left' => $this->db->escapeNumber($credits),
593
			]);
594
		} else {
595
			$this->db->write('UPDATE account_has_credits SET credits_left=' . $this->db->escapeNumber($credits) . ' WHERE ' . $this->SQL);
596
		}
597
		$this->credits = $credits;
598
	}
599
600
	public function increaseSmrCredits(int $credits): void {
601
		if ($credits == 0) {
602
			return;
603
		}
604
		if ($credits < 0) {
605
			throw new Exception('You cannot gain negative credits');
606
		}
607
		$this->setSmrCredits($this->getSmrCredits() + $credits);
608
	}
609
610
	public function decreaseSmrCredits(int $credits): void {
611
		if ($credits == 0) {
612
			return;
613
		}
614
		if ($credits < 0) {
615
			throw new Exception('You cannot use negative credits');
616
		}
617
		if ($credits > $this->getSmrCredits()) {
618
			throw new Exception('You cannot use more credits than you have');
619
		}
620
		$this->setSmrCredits($this->getSmrCredits() - $credits);
621
	}
622
623
	public function setSmrRewardCredits(int $credits): void {
624
		if ($this->getSmrRewardCredits() === $credits) {
625
			return;
626
		}
627
		if ($this->credits == 0 && $this->rewardCredits == 0) {
628
			$this->db->replace('account_has_credits', [
629
				'account_id' => $this->db->escapeNumber($this->getAccountID()),
630
				'reward_credits' => $this->db->escapeNumber($credits),
631
			]);
632
		} else {
633
			$this->db->write('UPDATE account_has_credits SET reward_credits=' . $this->db->escapeNumber($credits) . ' WHERE ' . $this->SQL);
634
		}
635
		$this->rewardCredits = $credits;
636
	}
637
638
	public function increaseSmrRewardCredits(int $credits): void {
639
		if ($credits == 0) {
640
			return;
641
		}
642
		if ($credits < 0) {
643
			throw new Exception('You cannot gain negative reward credits');
644
		}
645
		$this->setSmrRewardCredits($this->getSmrRewardCredits() + $credits);
646
	}
647
648
	public function sendMessageToBox(int $boxTypeID, string $message): void {
649
		// send him the message
650
		self::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message);
651
	}
652
653
	public static function doMessageSendingToBox(int $senderID, int $boxTypeID, string $message, int $gameID = 0): void {
654
		$db = Database::getInstance();
655
		$db->insert('message_boxes', [
656
			'box_type_id' => $db->escapeNumber($boxTypeID),
657
			'game_id' => $db->escapeNumber($gameID),
658
			'message_text' => $db->escapeString($message),
659
			'sender_id' => $db->escapeNumber($senderID),
660
			'send_time' => $db->escapeNumber(Epoch::time()),
661
		]);
662
	}
663
664
	public function equals(self $other): bool {
665
		return $this->getAccountID() == $other->getAccountID();
666
	}
667
668
	public function getAccountID(): int {
669
		return $this->accountID;
670
	}
671
672
	/**
673
	 * Return the ID associated with this account in the given history database.
674
	 */
675
	public function getOldAccountID(string $dbName): int {
676
		return $this->oldAccountIDs[$dbName] ?? 0;
677
	}
678
679
	public function getLogin(): string {
680
		return $this->login;
681
	}
682
683
	public function getEmail(): string {
684
		return $this->email;
685
	}
686
687
	protected function setEmail(string $email): void {
688
		if ($this->email === $email) {
689
			return;
690
		}
691
		$this->email = $email;
692
		$this->hasChanged = true;
693
	}
694
695
	/**
696
	 * Perform basic sanity checks on the usability of an email address.
697
	 */
698
	public static function checkEmail(string $email, self $owner = null): void {
699
		if (empty($email)) {
700
			throw new UserError('Email address is missing!');
701
		}
702
703
		if (str_contains($email, ' ')) {
704
			throw new UserError('The email is invalid! It cannot contain any spaces.');
705
		}
706
707
		// get user and host for the provided address
708
		[$user, $host] = explode('@', $email);
709
710
		// check if the host got a MX or at least an A entry
711
		if (!checkdnsrr($host, 'MX') && !checkdnsrr($host, 'A')) {
712
			throw new UserError('This is not a valid email address! The domain ' . $host . ' does not exist.');
713
		}
714
715
		try {
716
			$other = self::getAccountByEmail($email);
717
			if ($owner === null || !$owner->equals($other)) {
718
				throw new UserError('This email address is already registered.');
719
			}
720
		} catch (AccountNotFound) {
721
			// Proceed, this email is not yet in use
722
		}
723
	}
724
725
	/**
726
	 * Change e-mail address, unvalidate the account, and resend validation code
727
	 */
728
	public function changeEmail(string $email): void {
729
		// Throw an error if this email is not usable
730
		self::checkEmail($email, $this);
731
732
		$this->setEmail($email);
733
		$this->setValidationCode(random_string(10));
0 ignored issues
show
Bug introduced by
The function random_string was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

733
		$this->setValidationCode(/** @scrutinizer ignore-call */ random_string(10));
Loading history...
734
		$this->setValidated(false);
735
		$this->sendValidationEmail();
736
737
		// Remove an "Invalid email" ban (may or may not have one)
738
		$disabled = $this->isDisabled();
739
		if ($disabled !== false) {
740
			if ($disabled['Reason'] == CLOSE_ACCOUNT_INVALID_EMAIL_REASON) {
741
				$this->unbanAccount($this);
742
			}
743
		}
744
	}
745
746
	public function sendValidationEmail(): void {
747
		// remember when we sent validation code
748
		$this->db->replace('notification', [
749
			'notification_type' => $this->db->escapeString('validation_code'),
750
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
751
			'time' => $this->db->escapeNumber(Epoch::time()),
752
		]);
753
754
		$emailMessage =
755
			'Your validation code is: ' . $this->getValidationCode() . EOL . EOL .
756
			'The Space Merchant Realms server is on the web at ' . URL;
757
758
		$mail = setupMailer();
0 ignored issues
show
Bug introduced by
The function setupMailer was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

758
		$mail = /** @scrutinizer ignore-call */ setupMailer();
Loading history...
759
		$mail->Subject = 'Space Merchant Realms Validation Code';
760
		$mail->setFrom('[email protected]', 'SMR Support');
761
		$mail->msgHTML(nl2br($emailMessage));
762
		$mail->addAddress($this->getEmail(), $this->getHofName());
763
		$mail->send();
764
	}
765
766
	public function getOffset(): int {
767
		return $this->offset;
768
	}
769
770
	public function getFontSize(): int {
771
		return $this->fontSize;
772
	}
773
774
	public function setFontSize(int $size): void {
775
		if ($this->fontSize === $size) {
776
			return;
777
		}
778
		$this->fontSize = $size;
779
		$this->hasChanged = true;
780
	}
781
782
	// gets the extra CSS file linked in preferences
783
	public function getCssLink(): ?string {
784
		return $this->cssLink;
785
	}
786
787
	// sets the extra CSS file linked in preferences
788
	public function setCssLink(string $link): void {
789
		if ($this->cssLink === $link) {
790
			return;
791
		}
792
		$this->cssLink = $link;
793
		$this->hasChanged = true;
794
	}
795
796
	public function getTemplate(): string {
797
		return $this->template;
798
	}
799
800
	public function setTemplate(string $template): void {
801
		if ($this->template === $template) {
802
			return;
803
		}
804
		if (!in_array($template, Globals::getAvailableTemplates())) {
805
			throw new Exception('Template not allowed: ' . $template);
806
		}
807
		$this->template = $template;
808
		$this->hasChanged = true;
809
	}
810
811
	public function getColourScheme(): string {
812
		return $this->colourScheme;
813
	}
814
815
	public function setColourScheme(string $colourScheme): void {
816
		if ($this->colourScheme === $colourScheme) {
817
			return;
818
		}
819
		if (!in_array($colourScheme, Globals::getAvailableColourSchemes($this->getTemplate()))) {
820
			throw new Exception('Colour scheme not allowed: ' . $colourScheme);
821
		}
822
		$this->colourScheme = $colourScheme;
823
		$this->hasChanged = true;
824
	}
825
826
	// gets the CSS URL based on the template name specified in preferences
827
	public function getCssUrl(): string {
828
		return CSS_URLS[$this->getTemplate()];
829
	}
830
831
	// gets the CSS_COLOUR URL based on the template and color scheme specified in preferences
832
	public function getCssColourUrl(): string {
833
		return CSS_COLOUR_URLS[$this->getTemplate()][$this->getColourScheme()];
834
	}
835
836
	/**
837
	 * The Hall Of Fame name is not html-escaped in the database, so to display
838
	 * it correctly we must escape html entities.
839
	 */
840
	public function getHofDisplayName(bool $linked = false): string {
841
		$hofDisplayName = htmlspecialchars($this->getHofName());
842
		if ($linked) {
843
			return '<a href="' . $this->getPersonalHofHREF() . '">' . $hofDisplayName . '</a>';
844
		}
845
		return $hofDisplayName;
846
	}
847
848
	public function getHofName(): string {
849
		return $this->hofName;
850
	}
851
852
	public function setHofName(string $name): void {
853
		if ($this->hofName === $name) {
854
			return;
855
		}
856
		$this->hofName = $name;
857
		$this->hasChanged = true;
858
	}
859
860
	public function getIrcNick(): ?string {
861
		return $this->ircNick;
862
	}
863
864
	public function setIrcNick(?string $nick): void {
865
		if ($this->ircNick === $nick) {
866
			return;
867
		}
868
		$this->ircNick = $nick;
869
		$this->hasChanged = true;
870
	}
871
872
	public function getDiscordId(): ?string {
873
		return $this->discordId;
874
	}
875
876
	public function setDiscordId(?string $id): void {
877
		if ($this->discordId === $id) {
878
			return;
879
		}
880
		$this->discordId = $id;
881
		$this->hasChanged = true;
882
	}
883
884
	public function getReferralLink(): string {
885
		return URL . '/login_create.php?ref=' . $this->getAccountID();
886
	}
887
888
	/**
889
	 * Get the epoch format string including both date and time.
890
	 */
891
	public function getDateTimeFormat(): string {
892
		return $this->getDateFormat() . ' ' . $this->getTimeFormat();
893
	}
894
895
	/**
896
	 * Get the (HTML-only) epoch format string including both date and time,
897
	 * split across two lines.
898
	 */
899
	public function getDateTimeFormatSplit(): string {
900
		// We need to escape 'r' because it is a format specifier
901
		return $this->getDateFormat() . '\<b\r /\>' . $this->getTimeFormat();
902
	}
903
904
	public function getDateFormat(): string {
905
		return $this->dateFormat;
906
	}
907
908
	public function setDateFormat(string $format): void {
909
		if ($this->dateFormat === $format) {
910
			return;
911
		}
912
		$this->dateFormat = $format;
913
		$this->hasChanged = true;
914
	}
915
916
	public function getTimeFormat(): string {
917
		return $this->timeFormat;
918
	}
919
920
	public function setTimeFormat(string $format): void {
921
		if ($this->timeFormat === $format) {
922
			return;
923
		}
924
		$this->timeFormat = $format;
925
		$this->hasChanged = true;
926
	}
927
928
	public function getValidationCode(): string {
929
		return $this->validation_code;
930
	}
931
932
	protected function setValidationCode(string $code): void {
933
		if ($this->validation_code === $code) {
934
			return;
935
		}
936
		$this->validation_code = $code;
937
		$this->hasChanged = true;
938
	}
939
940
	public function setValidated(bool $bool): void {
941
		if ($this->validated === $bool) {
942
			return;
943
		}
944
		$this->validated = $bool;
945
		$this->hasChanged = true;
946
	}
947
948
	public function isValidated(): bool {
949
		return $this->validated;
950
	}
951
952
	public function isActive(): bool {
953
		$dbResult = $this->db->read('SELECT 1 FROM active_session WHERE account_id = ' . $this->db->escapeNumber($this->getAccountID()) . ' AND last_accessed >= ' . $this->db->escapeNumber(Epoch::time() - TIME_BEFORE_INACTIVE) . ' LIMIT 1');
954
		return $dbResult->hasRecord();
955
	}
956
957
	/**
958
	 * Check if the given (plain-text) password is correct.
959
	 * Updates the password hash if necessary.
960
	 */
961
	public function checkPassword(string $password): bool {
962
		// New (safe) password hashes will start with a $, but accounts logging
963
		// in for the first time since the transition from md5 will still have
964
		// hex-only hashes.
965
		if (str_starts_with($this->passwordHash, '$')) {
966
			$result = password_verify($password, $this->passwordHash);
967
		} else {
968
			$result = $this->passwordHash === md5($password);
969
		}
970
971
		// If password is correct, but hash algorithm has changed, update the hash.
972
		// This will also update any obsolete md5 password hashes.
973
		if ($result && password_needs_rehash($this->passwordHash, PASSWORD_DEFAULT)) {
974
			$this->setPassword($password);
975
			$this->update();
976
		}
977
978
		return $result;
979
	}
980
981
	/**
982
	 * Set the (plain-text) password for this account.
983
	 */
984
	public function setPassword(string $password): void {
985
		$hash = password_hash($password, PASSWORD_DEFAULT);
986
		if ($this->passwordHash === $hash) {
987
			return;
988
		}
989
		$this->passwordHash = $hash;
990
		$this->generatePasswordReset();
991
		$this->hasChanged = true;
992
	}
993
994
	public function addAuthMethod(string $loginType, string $authKey): void {
995
		$dbResult = $this->db->read('SELECT account_id FROM account_auth WHERE login_type=' . $this->db->escapeString($loginType) . ' AND auth_key = ' . $this->db->escapeString($authKey) . ';');
996
		if ($dbResult->hasRecord()) {
997
			if ($dbResult->record()->getInt('account_id') != $this->getAccountID()) {
998
				throw new Exception('Another account already uses this form of auth.');
999
			}
1000
			return;
1001
		}
1002
		$this->db->insert('account_auth', [
1003
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1004
			'login_type' => $this->db->escapeString($loginType),
1005
			'auth_key' => $this->db->escapeString($authKey),
1006
		]);
1007
	}
1008
1009
	public function generatePasswordReset(): void {
1010
		$this->setPasswordReset(random_string(32));
0 ignored issues
show
Bug introduced by
The function random_string was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1010
		$this->setPasswordReset(/** @scrutinizer ignore-call */ random_string(32));
Loading history...
1011
	}
1012
1013
	public function getPasswordReset(): string {
1014
		return $this->passwordReset;
1015
	}
1016
1017
	protected function setPasswordReset(string $passwordReset): void {
1018
		if ($this->passwordReset === $passwordReset) {
1019
			return;
1020
		}
1021
		$this->passwordReset = $passwordReset;
1022
		$this->hasChanged = true;
1023
	}
1024
1025
	public function isDisplayShipImages(): bool {
1026
		return $this->images;
1027
	}
1028
1029
	public function setDisplayShipImages(bool $bool): void {
1030
		if ($this->images === $bool) {
1031
			return;
1032
		}
1033
		$this->images = $bool;
1034
		$this->hasChanged = true;
1035
	}
1036
1037
	public function isUseAJAX(): bool {
1038
		return $this->useAJAX;
1039
	}
1040
1041
	public function setUseAJAX(bool $bool): void {
1042
		if ($this->useAJAX === $bool) {
1043
			return;
1044
		}
1045
		$this->useAJAX = $bool;
1046
		$this->hasChanged = true;
1047
	}
1048
1049
	public function isDefaultCSSEnabled(): bool {
1050
		return $this->defaultCSSEnabled;
1051
	}
1052
1053
	public function setDefaultCSSEnabled(bool $bool): void {
1054
		if ($this->defaultCSSEnabled === $bool) {
1055
			return;
1056
		}
1057
		$this->defaultCSSEnabled = $bool;
1058
		$this->hasChanged = true;
1059
	}
1060
1061
	/**
1062
	 * @return array<string>|array<string, array<string>>
1063
	 */
1064
	public function getHotkeys(string $hotkeyType = null): array {
1065
		if ($hotkeyType !== null) {
1066
			return $this->hotkeys[$hotkeyType] ?? [];
1067
		}
1068
		return $this->hotkeys;
1069
	}
1070
1071
	/**
1072
	 * @param array<string> $bindings
1073
	 */
1074
	public function setHotkey(string $hotkeyType, array $bindings): void {
1075
		if ($this->getHotkeys($hotkeyType) === $bindings) {
1076
			return;
1077
		}
1078
		$this->hotkeys[$hotkeyType] = $bindings;
1079
		$this->hasChanged = true;
1080
	}
1081
1082
	public function isReceivingMessageNotifications(int $messageTypeID): bool {
1083
		return $this->getMessageNotifications($messageTypeID) > 0;
1084
	}
1085
1086
	public function getMessageNotifications(int $messageTypeID): int {
1087
		return $this->messageNotifications[$messageTypeID] ?? 0;
1088
	}
1089
1090
	public function setMessageNotifications(int $messageTypeID, int $num): void {
1091
		if ($this->getMessageNotifications($messageTypeID) == $num) {
1092
			return;
1093
		}
1094
		$this->messageNotifications[$messageTypeID] = $num;
1095
		$this->hasChanged = true;
1096
	}
1097
1098
	public function increaseMessageNotifications(int $messageTypeID, int $num): void {
1099
		if ($num == 0) {
1100
			return;
1101
		}
1102
		if ($num < 0) {
1103
			throw new Exception('You cannot increase by a negative amount');
1104
		}
1105
		$this->setMessageNotifications($messageTypeID, $this->getMessageNotifications($messageTypeID) + $num);
1106
	}
1107
1108
	public function decreaseMessageNotifications(int $messageTypeID, int $num): void {
1109
		if ($num == 0) {
1110
			return;
1111
		}
1112
		if ($num < 0) {
1113
			throw new Exception('You cannot decrease by a negative amount');
1114
		}
1115
		$this->setMessageNotifications($messageTypeID, $this->getMessageNotifications($messageTypeID) - $num);
1116
	}
1117
1118
	public function isCenterGalaxyMapOnPlayer(): bool {
1119
		return $this->centerGalaxyMapOnPlayer;
1120
	}
1121
1122
	public function setCenterGalaxyMapOnPlayer(bool $bool): void {
1123
		if ($this->centerGalaxyMapOnPlayer === $bool) {
1124
			return;
1125
		}
1126
		$this->centerGalaxyMapOnPlayer = $bool;
1127
		$this->hasChanged = true;
1128
	}
1129
1130
	public function getMailBanned(): int {
1131
		return $this->mailBanned;
1132
	}
1133
1134
	public function isMailBanned(): bool {
1135
		return $this->mailBanned > Epoch::time();
1136
	}
1137
1138
	public function setMailBanned(int $time): void {
1139
		if ($this->mailBanned === $time) {
1140
			return;
1141
		}
1142
		$this->mailBanned = $time;
1143
		$this->hasChanged = true;
1144
	}
1145
1146
	public function increaseMailBanned(int $increaseTime): void {
1147
		$time = max(Epoch::time(), $this->getMailBanned());
1148
		$this->setMailBanned($time + $increaseTime);
1149
	}
1150
1151
	/**
1152
	 * @return array<int, bool>
1153
	 */
1154
	public function getPermissions(): array {
1155
		if (!isset($this->permissions)) {
1156
			$this->permissions = [];
1157
			$dbResult = $this->db->read('SELECT permission_id FROM account_has_permission WHERE ' . $this->SQL);
1158
			foreach ($dbResult->records() as $dbRecord) {
1159
				$this->permissions[$dbRecord->getInt('permission_id')] = true;
1160
			}
1161
		}
1162
		return $this->permissions;
1163
	}
1164
1165
	public function hasPermission(int $permissionID = null): bool {
1166
		$permissions = $this->getPermissions();
1167
		if ($permissionID === null) {
1168
			return count($permissions) > 0;
1169
		}
1170
		return $permissions[$permissionID] ?? false;
1171
	}
1172
1173
	public function getPoints(): int {
1174
		if (!isset($this->points)) {
1175
			$this->points = 0;
1176
			$this->db->lockTable('account_has_points');
1177
			$dbResult = $this->db->read('SELECT * FROM account_has_points WHERE ' . $this->SQL);
1178
			if ($dbResult->hasRecord()) {
1179
				$dbRecord = $dbResult->record();
1180
				$this->points = $dbRecord->getInt('points');
1181
				$lastUpdate = $dbRecord->getInt('last_update');
1182
				//we are gonna check for reducing points...
1183
				if ($this->points > 0 && $lastUpdate < Epoch::time() - (7 * 86400)) {
1184
					$removePoints = 0;
1185
					while ($lastUpdate < Epoch::time() - (7 * 86400)) {
1186
						$removePoints++;
1187
						$lastUpdate += (7 * 86400);
1188
					}
1189
					$this->removePoints($removePoints, $lastUpdate);
1190
				}
1191
			}
1192
			$this->db->unlock();
1193
		}
1194
		return $this->points;
1195
	}
1196
1197
	public function setPoints(int $numPoints, ?int $lastUpdate = null): void {
1198
		$numPoints = max($numPoints, 0);
1199
		if ($this->getPoints() == $numPoints) {
1200
			return;
1201
		}
1202
		if ($this->points == 0) {
1203
			$this->db->insert('account_has_points', [
1204
				'account_id' => $this->db->escapeNumber($this->getAccountID()),
1205
				'points' => $this->db->escapeNumber($numPoints),
1206
				'last_update' => $this->db->escapeNumber($lastUpdate ?? Epoch::time()),
1207
			]);
1208
		} elseif ($numPoints <= 0) {
1209
			$this->db->write('DELETE FROM account_has_points WHERE ' . $this->SQL);
1210
		} else {
1211
			$this->db->write('UPDATE account_has_points SET points = ' . $this->db->escapeNumber($numPoints) . (isset($lastUpdate) ? ', last_update = ' . $this->db->escapeNumber(Epoch::time()) : '') . ' WHERE ' . $this->SQL);
1212
		}
1213
		$this->points = $numPoints;
1214
	}
1215
1216
	public function removePoints(int $numPoints, ?int $lastUpdate = null): void {
1217
		if ($numPoints > 0) {
1218
			$this->setPoints($this->getPoints() - $numPoints, $lastUpdate);
1219
		}
1220
	}
1221
1222
	public function addPoints(int $numPoints, self $admin, int $reasonID, string $suspicion): int|false {
1223
		//do we have points
1224
		$this->setPoints($this->getPoints() + $numPoints, Epoch::time());
1225
		$totalPoints = $this->getPoints();
1226
		if ($totalPoints < 10) {
1227
			return false; //leave scripts its only a warning
1228
		} elseif ($totalPoints < 20) {
1229
			$days = 2;
1230
		} elseif ($totalPoints < 30) {
1231
			$days = 4;
1232
		} elseif ($totalPoints < 50) {
1233
			$days = 7;
1234
		} elseif ($totalPoints < 75) {
1235
			$days = 15;
1236
		} elseif ($totalPoints < 100) {
1237
			$days = 30;
1238
		} elseif ($totalPoints < 125) {
1239
			$days = 60;
1240
		} elseif ($totalPoints < 150) {
1241
			$days = 120;
1242
		} elseif ($totalPoints < 175) {
1243
			$days = 240;
1244
		} elseif ($totalPoints < 200) {
1245
			$days = 480;
1246
		} else {
1247
			$days = 0; //Forever/indefinite
1248
		}
1249
1250
		if ($days == 0) {
1251
			$expireTime = 0;
1252
		} else {
1253
			$expireTime = Epoch::time() + $days * 86400;
1254
		}
1255
		$this->banAccount($expireTime, $admin, $reasonID, $suspicion);
1256
1257
		return $days;
1258
	}
1259
1260
	public function getFriendlyColour(): string {
1261
		return $this->friendlyColour;
1262
	}
1263
	public function setFriendlyColour(string $colour): void {
1264
		$this->friendlyColour = $colour;
1265
		$this->hasChanged = true;
1266
	}
1267
	public function getNeutralColour(): string {
1268
		return $this->neutralColour;
1269
	}
1270
	public function setNeutralColour(string $colour): void {
1271
		$this->neutralColour = $colour;
1272
		$this->hasChanged = true;
1273
	}
1274
	public function getEnemyColour(): string {
1275
		return $this->enemyColour;
1276
	}
1277
	public function setEnemyColour(string $colour): void {
1278
		$this->enemyColour = $colour;
1279
		$this->hasChanged = true;
1280
	}
1281
1282
	public function banAccount(int $expireTime, self $admin, int $reasonID, string $suspicion, bool $removeExceptions = false): void {
1283
		$this->db->replace('account_is_closed', [
1284
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1285
			'reason_id' => $this->db->escapeNumber($reasonID),
1286
			'suspicion' => $this->db->escapeString($suspicion),
1287
			'expires' => $this->db->escapeNumber($expireTime),
1288
		]);
1289
		$this->db->write('DELETE FROM active_session WHERE ' . $this->SQL . ' LIMIT 1');
1290
1291
		$this->db->insert('account_has_closing_history', [
1292
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1293
			'time' => $this->db->escapeNumber(Epoch::time()),
1294
			'admin_id' => $this->db->escapeNumber($admin->getAccountID()),
1295
			'action' => $this->db->escapeString('Closed'),
1296
		]);
1297
		$this->db->write('UPDATE player SET newbie_turns = 1
1298
						WHERE ' . $this->SQL . '
1299
						AND newbie_turns = 0
1300
						AND land_on_planet = ' . $this->db->escapeBoolean(false));
1301
1302
		$dbResult = $this->db->read('SELECT game_id FROM game JOIN player USING (game_id)
1303
						WHERE ' . $this->SQL . '
1304
						AND end_time >= ' . $this->db->escapeNumber(Epoch::time()));
1305
		foreach ($dbResult->records() as $dbRecord) {
1306
			$player = SmrPlayer::getPlayer($this->getAccountID(), $dbRecord->getInt('game_id'));
1307
			$player->updateTurns();
1308
			$player->update();
1309
		}
1310
		$this->log(LOG_TYPE_ACCOUNT_CHANGES, 'Account closed by ' . $admin->getLogin() . '.');
1311
		if ($removeExceptions !== false) {
1312
			$this->db->write('DELETE FROM account_exceptions WHERE ' . $this->SQL);
1313
		}
1314
	}
1315
1316
	public function unbanAccount(self $admin = null, string $currException = null): void {
1317
		$adminID = 0;
1318
		if ($admin !== null) {
1319
			$adminID = $admin->getAccountID();
1320
		}
1321
		$this->db->write('DELETE FROM account_is_closed WHERE ' . $this->SQL);
1322
		$this->db->insert('account_has_closing_history', [
1323
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1324
			'time' => $this->db->escapeNumber(Epoch::time()),
1325
			'admin_id' => $this->db->escapeNumber($adminID),
1326
			'action' => $this->db->escapeString('Opened'),
1327
		]);
1328
		$this->db->write('UPDATE player SET last_turn_update = GREATEST(' . $this->db->escapeNumber(Epoch::time()) . ', last_turn_update) WHERE ' . $this->SQL);
1329
		if ($admin !== null) {
1330
			$this->log(LOG_TYPE_ACCOUNT_CHANGES, 'Account reopened by ' . $admin->getLogin() . '.');
1331
		} else {
1332
			$this->log(LOG_TYPE_ACCOUNT_CHANGES, 'Account automatically reopened.');
1333
		}
1334
		if ($currException !== null) {
1335
			$this->db->replace('account_exceptions', [
1336
				'account_id' => $this->db->escapeNumber($this->getAccountID()),
1337
				'reason' => $this->db->escapeString($currException),
1338
			]);
1339
		}
1340
	}
1341
1342
	public function getPersonalHofHREF(): string {
1343
		return (new HallOfFamePersonal($this->getAccountID()))->href();
1344
	}
1345
1346
}
1347