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

SmrAccount   F
last analyzed

Complexity

Total Complexity 260

Size/Duplication

Total Lines 1333
Duplicated Lines 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
eloc 707
dl 0
loc 1333
rs 1.893
c 4
b 2
f 0
wmc 260

123 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 63 6
A setDiscordId() 0 6 2
A setCenterGalaxyMapOnPlayer() 0 6 2
A getNeutralColour() 0 2 1
A setUseAJAX() 0 6 2
A getAccountBySocialLogin() 0 12 3
A isDefaultCSSEnabled() 0 2 1
A setEnemyColour() 0 3 1
A setMessageNotifications() 0 6 2
A getFriendlyColour() 0 2 1
A getDateFormat() 0 2 1
A getDiscordId() 0 2 1
A sendValidationEmail() 0 18 1
A getOffset() 0 2 1
A getAccountByIrcNick() 0 10 3
A update() 0 30 1
A setHofName() 0 6 2
A getPasswordReset() 0 2 1
C addPoints() 0 36 12
A isMailBanned() 0 2 1
A getPermissions() 0 9 3
A banAccount() 0 31 3
A getTimeFormat() 0 2 1
A getHofName() 0 2 1
A getDefaultHotkeys() 0 2 1
A getUserScoreCaseStatement() 0 10 2
A addAuthMethod() 0 12 3
A getPoints() 0 22 6
A setColourScheme() 0 9 3
A getCssLink() 0 2 1
A isReceivingMessageNotifications() 0 2 1
A isDisabled() 0 19 4
A changeEmail() 0 14 3
A getAccountByEmail() 0 10 3
A setValidated() 0 6 2
A setTimeFormat() 0 6 2
A setPoints() 0 17 5
A setDefaultCSSEnabled() 0 6 2
A increaseMailBanned() 0 3 1
A setIrcNick() 0 6 2
A isUseAJAX() 0 2 1
A setFontSize() 0 6 2
A getPersonalHofHREF() 0 2 1
A setHotkey() 0 6 2
A setNeutralColour() 0 3 1
A setValidationCode() 0 6 2
A setMailBanned() 0 6 2
A generatePasswordReset() 0 2 1
A getEnemyColour() 0 2 1
A isValidated() 0 2 1
A getAccountByHofName() 0 10 3
A isDisplayShipImages() 0 2 1
A getMessageNotifications() 0 2 1
A unbanAccount() 0 22 4
A getIrcNick() 0 2 1
A removePoints() 0 3 2
A getValidationCode() 0 2 1
A getColourScheme() 0 2 1
A getAccount() 0 5 3
A getTemplate() 0 2 1
A isActive() 0 3 1
A getAccountByDiscordId() 0 10 3
A getDateTimeFormat() 0 2 1
A getAccountByLogin() 0 10 3
A getCssUrl() 0 2 1
A setPasswordReset() 0 6 2
A setPassword() 0 8 2
A setDisplayShipImages() 0 6 2
A clearCache() 0 2 1
A checkPassword() 0 18 4
A setDateFormat() 0 6 2
A increaseMessageNotifications() 0 8 3
A getReferralLink() 0 2 1
A decreaseMessageNotifications() 0 8 3
A setTemplate() 0 9 3
A getHofDisplayName() 0 6 2
A getFontSize() 0 2 1
A getCssColourUrl() 0 2 1
A getHotkeys() 0 5 2
A hasPermission() 0 6 2
A setCssLink() 0 6 2
A isCenterGalaxyMapOnPlayer() 0 2 1
A setFriendlyColour() 0 3 1
A createAccount() 0 19 2
A getDateTimeFormatSplit() 0 3 1
A getMailBanned() 0 2 1
A increaseSmrRewardCredits() 0 8 3
A setSmrRewardCredits() 0 13 4
A setSmrCredits() 0 13 4
A isVeteran() 0 4 2
A getReferrerID() 0 2 1
A getIndividualScores() 0 20 5
A getLogin() 0 2 1
A setEmail() 0 6 2
A setLoggingEnabled() 0 6 2
A getEmail() 0 2 1
A getOldAccountID() 0 2 1
A updateMaxRankAchieved() 0 11 3
A isNPC() 0 6 2
A getSmrRewardCredits() 0 3 1
A getLastLogin() 0 2 1
A isLoggingEnabled() 0 2 1
A getSmrCredits() 0 3 1
A equals() 0 2 1
A getRank() 0 6 2
A getHOF() 0 3 1
A log() 0 8 2
A getAccountID() 0 2 1
A isVeteranForced() 0 2 1
A doMessageSendingToBox() 0 8 1
A decreaseSmrCredits() 0 11 4
A updateLastLogin() 0 7 2
B decreaseTotalSmrCredits() 0 29 7
A getReferrer() 0 2 1
A getHOFData() 0 7 3
A updateIP() 0 33 4
A getTotalSmrCredits() 0 2 1
A increaseSmrCredits() 0 8 3
A sendMessageToBox() 0 3 1
A getSmrCreditsData() 0 9 4
A getScore() 0 9 3
A hasReferrer() 0 2 1
B checkEmail() 0 21 8

How to fix   Complexity   

Complex Class

Complex classes like SmrAccount often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SmrAccount, and based on these observations, apply Extract Interface, too.

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

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

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

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

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

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

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

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

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

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

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

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