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

Completed
Push — live ( 402f10...19837b )
by Dan
21s queued 18s
created

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

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

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

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

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

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

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