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
Push — live ( 05ca5f...631a24 )
by Dan
05:49
created

SmrAccount::getToggleAJAXHREF()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

732
		$this->setValidationCode(/** @scrutinizer ignore-call */ random_string(10));
Loading history...
733
		$this->setValidated(false);
734
		$this->sendValidationEmail();
735
736
		// Remove an "Invalid email" ban (may or may not have one)
737
		$disabled = $this->isDisabled();
738
		if ($disabled !== false) {
739
			if ($disabled['Reason'] == CLOSE_ACCOUNT_INVALID_EMAIL_REASON) {
740
				$this->unbanAccount($this);
741
			}
742
		}
743
	}
744
745
	public function sendValidationEmail(): void {
746
		// remember when we sent validation code
747
		$this->db->replace('notification', [
748
			'notification_type' => $this->db->escapeString('validation_code'),
749
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
750
			'time' => $this->db->escapeNumber(Epoch::time()),
751
		]);
752
753
		$emailMessage =
754
			'Your validation code is: ' . $this->getValidationCode() . EOL . EOL .
755
			'The Space Merchant Realms server is on the web at ' . URL;
756
757
		$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

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

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