Scrutinizer GitHub App not installed

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

Install GitHub App

Failed Conditions
Push — main ( 2c9538...02418a )
by Dan
38s queued 18s
created

Session   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 331
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 145
dl 0
loc 331
rs 6.4799
c 2
b 0
f 0
wmc 54

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getRequestVarIntArray() 0 5 1
A getGameID() 0 2 1
A destroy() 0 5 1
A updateGame() 0 7 2
A getAccountID() 0 2 1
A hasCurrentVar() 0 2 1
A getRequestVar() 0 5 1
A hasAccount() 0 2 1
A hasChangedSN() 0 2 1
A getPlayer() 0 2 1
A getRequestVarInt() 0 5 1
A setAccount() 0 2 1
A getSessionID() 0 2 1
A hasGame() 0 2 1
A getLastAccessed() 0 2 1
A getSN() 0 2 1
A getAccount() 0 2 1
A getCurrentVar() 0 2 1
B __construct() 0 44 9
A getInstance() 0 2 1
A clearLinks() 0 2 1
A saveAjaxReturns() 0 6 2
A addLink() 0 13 4
A addAjaxReturns() 0 3 2
A setCurrentVar() 0 2 1
B fetchVarInfo() 0 43 11
A update() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like Session 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 Session, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Smr;
4
5
use AbstractSmrPlayer;
6
use Page;
7
use Smr\Container\DiContainer;
8
use Smr\Exceptions\UserError;
9
use SmrAccount;
10
use SmrPlayer;
11
12
class Session {
13
14
	private const TIME_BEFORE_EXPIRY = 172800; // 2 days
15
16
	private const URL_LOAD_DELAY = [
17
		'configure_hardware.php' => .4,
18
		'forces_drop.php' => .4,
19
		'forces_drop_processing.php' => .5,
20
		'forces_refresh_processing.php' => .4,
21
		'sector_jump_processing.php' => .4,
22
		'sector_move_processing.php' => .4,
23
		'sector_scan.php' => .4,
24
		'shop_goods_processing.php' => .4,
25
		'trader_attack_processing.php' => .75,
26
		'trader_examine.php' => .75,
27
	];
28
29
	protected Database $db;
30
31
	private string $sessionID;
32
	private int $gameID;
33
	/** @var array<string, Page> */
34
	private array $links = [];
35
	private ?Page $currentPage = null;
36
	private bool $generate;
37
	public readonly bool $ajax;
38
	private string $SN;
39
	private string $lastSN;
40
	private int $accountID;
41
	private float $lastAccessed;
42
43
	/** @var ?array<string, string> */
44
	protected ?array $previousAjaxReturns;
45
	/** @var array<string, string> */
46
	protected array $ajaxReturns = [];
47
48
	/**
49
	 * Return the Smr\Session in the DI container.
50
	 * If one does not exist yet, it will be created.
51
	 * This is the intended way to construct this class.
52
	 */
53
	public static function getInstance(): self {
54
		return DiContainer::get(self::class);
55
	}
56
57
	/**
58
	 * Smr\Session constructor.
59
	 * Not intended to be constructed by hand. Use Smr\Session::getInstance().
60
	 */
61
	public function __construct() {
62
		// Initialize the db connector here
63
		$this->db = Database::getInstance();
64
65
		// now try the cookie
66
		$idLength = 32;
67
		if (isset($_COOKIE['session_id']) && strlen($_COOKIE['session_id']) === $idLength) {
68
			$this->sessionID = $_COOKIE['session_id'];
69
		} else {
70
			// create a new session id
71
			do {
72
				$this->sessionID = random_string($idLength);
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

72
				$this->sessionID = /** @scrutinizer ignore-call */ random_string($idLength);
Loading history...
73
				$dbResult = $this->db->read('SELECT 1 FROM active_session WHERE session_id = ' . $this->db->escapeString($this->sessionID));
74
			} while ($dbResult->hasRecord()); //Make sure we haven't somehow clashed with someone else's session.
75
76
			// This is a minor hack to make sure that setcookie is not called
77
			// for CLI programs and tests (to avoid "headers already sent").
78
			if (headers_sent() === false) {
79
				setcookie('session_id', $this->sessionID);
80
			}
81
		}
82
83
		// Delete any expired sessions
84
		$this->db->write('DELETE FROM active_session WHERE last_accessed < ' . $this->db->escapeNumber(time() - self::TIME_BEFORE_EXPIRY));
85
86
		// try to get current session
87
		$this->ajax = Request::getInt('ajax', 0) === 1;
0 ignored issues
show
Bug introduced by
The property ajax is declared read-only in Smr\Session.
Loading history...
88
		$this->SN = Request::get('sn', '');
89
		$this->fetchVarInfo();
90
91
		if (!$this->ajax && $this->hasCurrentVar()) {
92
			$file = $this->getCurrentVar()->file;
93
			$loadDelay = self::URL_LOAD_DELAY[$file] ?? 0;
94
			$timeBetweenLoads = microtime(true) - $this->lastAccessed;
95
			if ($timeBetweenLoads < $loadDelay) {
96
				$sleepTime = IRound(($loadDelay - $timeBetweenLoads) * 1000000);
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

96
				$sleepTime = /** @scrutinizer ignore-call */ IRound(($loadDelay - $timeBetweenLoads) * 1000000);
Loading history...
97
				//echo 'Sleeping for: ' . $sleepTime . 'us';
98
				usleep($sleepTime);
99
			}
100
			if (ENABLE_DEBUG) {
101
				$this->db->insert('debug', [
102
					'debug_type' => $this->db->escapeString('Delay: ' . $file),
103
					'account_id' => $this->db->escapeNumber($this->accountID),
104
					'value' => $this->db->escapeNumber($timeBetweenLoads),
105
				]);
106
			}
107
		}
108
	}
109
110
	public function fetchVarInfo(): void {
111
		$dbResult = $this->db->read('SELECT * FROM active_session WHERE session_id = ' . $this->db->escapeString($this->sessionID));
112
		if ($dbResult->hasRecord()) {
113
			$dbRecord = $dbResult->record();
114
			$this->generate = false;
115
			$this->sessionID = $dbRecord->getString('session_id');
116
			$this->accountID = $dbRecord->getInt('account_id');
117
			$this->gameID = $dbRecord->getInt('game_id');
118
			$this->lastAccessed = $dbRecord->getFloat('last_accessed');
119
			$this->lastSN = $dbRecord->getString('last_sn');
120
			// We may not have ajax_returns if ajax was disabled
121
			$this->previousAjaxReturns = $dbRecord->getObject('ajax_returns', true, true);
122
123
			[$this->links, $lastPage] = $dbRecord->getObject('session_var', true);
124
125
			$ajaxRefresh = $this->ajax && !$this->hasChangedSN();
126
			if ($ajaxRefresh) {
127
				$this->currentPage = $lastPage;
128
			} elseif (isset($this->links[$this->SN])) {
129
				// If the current page is modified during page processing, we need
130
				// to make sure the original link is unchanged. So we clone it here.
131
				$this->currentPage = clone $this->links[$this->SN];
132
133
				// If SN changes during an ajax update, it is an error unless user is
134
				// requesting a page that is allowed to be executed in an ajax call.
135
				$allowAjax = $this->currentPage['AJAX'] ?? false;
136
				if (!$allowAjax && $this->ajax && $this->hasChangedSN()) {
137
					throw new UserError('The previous page failed to auto-refresh properly!');
138
				}
139
			}
140
141
			if (!$ajaxRefresh) { // since form pages don't ajax refresh properly
142
				foreach ($this->links as $sn => $link) {
143
					if (!$link->reusable) {
144
						// This link is no longer valid
145
						unset($this->links[$sn]);
146
					}
147
				}
148
			}
149
		} else {
150
			$this->generate = true;
151
			$this->accountID = 0;
152
			$this->gameID = 0;
153
		}
154
	}
155
156
	public function update(): void {
157
		$sessionVar = [$this->links, $this->currentPage];
158
		if (!$this->generate) {
159
			$this->db->write('UPDATE active_session SET account_id=' . $this->db->escapeNumber($this->accountID) . ',game_id=' . $this->db->escapeNumber($this->gameID) . (!$this->ajax ? ',last_accessed=' . $this->db->escapeNumber(Epoch::microtime()) : '') . ',session_var=' . $this->db->escapeObject($sessionVar, true) .
160
					',last_sn=' . $this->db->escapeString($this->SN) .
161
					' WHERE session_id=' . $this->db->escapeString($this->sessionID) . ($this->ajax ? ' AND last_sn=' . $this->db->escapeString($this->lastSN) : ''));
162
		} else {
163
			$this->db->write('DELETE FROM active_session WHERE account_id = ' . $this->db->escapeNumber($this->accountID) . ' AND game_id = ' . $this->db->escapeNumber($this->gameID));
164
			$this->db->insert('active_session', [
165
				'session_id' => $this->db->escapeString($this->sessionID),
166
				'account_id' => $this->db->escapeNumber($this->accountID),
167
				'game_id' => $this->db->escapeNumber($this->gameID),
168
				'last_accessed' => $this->db->escapeNumber(Epoch::microtime()),
169
				'session_var' => $this->db->escapeObject($sessionVar, true),
170
			]);
171
			$this->generate = false;
172
		}
173
	}
174
175
	/**
176
	 * Uniquely identifies the session in the database.
177
	 */
178
	public function getSessionID(): string {
179
		return $this->sessionID;
180
	}
181
182
	/**
183
	 * Returns the Game ID associated with the session.
184
	 */
185
	public function getGameID(): int {
186
		return $this->gameID;
187
	}
188
189
	/**
190
	 * Returns true if the session is inside a game, false otherwise.
191
	 */
192
	public function hasGame(): bool {
193
		return $this->gameID != 0;
194
	}
195
196
	public function hasAccount(): bool {
197
		return $this->accountID > 0;
198
	}
199
200
	public function getAccountID(): int {
201
		return $this->accountID;
202
	}
203
204
	public function getAccount(): SmrAccount {
205
		return SmrAccount::getAccount($this->accountID);
206
	}
207
208
	public function getPlayer(bool $forceUpdate = false): AbstractSmrPlayer {
209
		return SmrPlayer::getPlayer($this->accountID, $this->gameID, $forceUpdate);
210
	}
211
212
	/**
213
	 * Sets the `accountID` attribute of this session.
214
	 */
215
	public function setAccount(SmrAccount $account): void {
216
		$this->accountID = $account->getAccountID();
217
	}
218
219
	/**
220
	 * Updates the `gameID` attribute of the session and deletes any other
221
	 * active sessions in this game for this account.
222
	 */
223
	public function updateGame(int $gameID): void {
224
		if ($this->gameID == $gameID) {
225
			return;
226
		}
227
		$this->gameID = $gameID;
228
		$this->db->write('DELETE FROM active_session WHERE account_id = ' . $this->db->escapeNumber($this->accountID) . ' AND game_id = ' . $this->gameID);
229
		$this->db->write('UPDATE active_session SET game_id=' . $this->db->escapeNumber($this->gameID) . ' WHERE session_id=' . $this->db->escapeString($this->sessionID));
230
	}
231
232
	/**
233
	 * The SN is the URL parameter that defines the page being requested.
234
	 */
235
	public function getSN(): string {
236
		return $this->SN;
237
	}
238
239
	/**
240
	 * Returns true if the current SN is different than the previous SN.
241
	 */
242
	public function hasChangedSN(): bool {
243
		return $this->SN != $this->lastSN;
244
	}
245
246
	public function destroy(): void {
247
		$this->db->write('DELETE FROM active_session WHERE session_id = ' . $this->db->escapeString($this->sessionID));
248
		unset($this->sessionID);
249
		unset($this->accountID);
250
		unset($this->gameID);
251
	}
252
253
	public function getLastAccessed(): float {
254
		return $this->lastAccessed;
255
	}
256
257
	/**
258
	 * Check if the session has a var associated with the current SN.
259
	 */
260
	public function hasCurrentVar(): bool {
261
		return $this->currentPage !== null;
262
	}
263
264
	/**
265
	 * Returns the session var associated with the current SN.
266
	 */
267
	public function getCurrentVar(): Page {
268
		return $this->currentPage;
269
	}
270
271
	/**
272
	 * Gets a var from $var, $_REQUEST, or $default. Then stores it in the
273
	 * session so that it can still be retrieved when the page auto-refreshes.
274
	 * This is the recommended way to get $_REQUEST data for display pages.
275
	 * For processing pages, see the Request class.
276
	 */
277
	public function getRequestVar(string $varName, string $default = null): string {
278
		$result = Request::getVar($varName, $default);
279
		$var = $this->getCurrentVar();
280
		$var[$varName] = $result;
281
		return $result;
282
	}
283
284
	public function getRequestVarInt(string $varName, int $default = null): int {
285
		$result = Request::getVarInt($varName, $default);
286
		$var = $this->getCurrentVar();
287
		$var[$varName] = $result;
288
		return $result;
289
	}
290
291
	/**
292
	 * @param ?array<int> $default
293
	 * @return array<int>
294
	 */
295
	public function getRequestVarIntArray(string $varName, array $default = null): array {
296
		$result = Request::getVarIntArray($varName, $default);
297
		$var = $this->getCurrentVar();
298
		$var[$varName] = $result;
299
		return $result;
300
	}
301
302
	/**
303
	 * Replace the global $var with the given $container.
304
	 */
305
	public function setCurrentVar(Page $container): void {
306
		$this->currentPage = $container;
307
	}
308
309
	public function clearLinks(): void {
310
		$this->links = [];
311
	}
312
313
	/**
314
	 * Add a page to the session so that it can be used on next page load.
315
	 * It will be associated with an SN that will be used for linking.
316
	 */
317
	public function addLink(Page $container): string {
318
		// If we already had a link to this exact page, use the existing SN for it.
319
		foreach ($this->links as $sn => $link) {
320
			if ($container == $link) { // loose equality to compare contents
321
				return $sn;
322
			}
323
		}
324
		// This page isn't an existing link, so give it a new SN.
325
		do {
326
			$sn = random_alphabetic_string(6);
0 ignored issues
show
Bug introduced by
The function random_alphabetic_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

326
			$sn = /** @scrutinizer ignore-call */ random_alphabetic_string(6);
Loading history...
327
		} while (isset($this->links[$sn]));
328
		$this->links[$sn] = $container;
329
		return $sn;
330
	}
331
332
	public function addAjaxReturns(string $element, string $contents): bool {
333
		$this->ajaxReturns[$element] = $contents;
334
		return isset($this->previousAjaxReturns[$element]) && $this->previousAjaxReturns[$element] == $contents;
335
	}
336
337
	public function saveAjaxReturns(): void {
338
		if (empty($this->ajaxReturns)) {
339
			return;
340
		}
341
		$this->db->write('UPDATE active_session SET ajax_returns=' . $this->db->escapeObject($this->ajaxReturns, true) .
342
				' WHERE session_id=' . $this->db->escapeString($this->sessionID));
343
	}
344
345
}
346