Scrutinizer GitHub App not installed

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

Install GitHub App

Failed Conditions
Pull Request — main (#1498)
by Dan
04:47
created

Session::fetchVarInfo()   C

Complexity

Conditions 12
Paths 41

Size

Total Lines 47
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 29
c 0
b 0
f 0
nc 41
nop 0
dl 0
loc 47
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

84
				$this->sessionID = /** @scrutinizer ignore-call */ random_string($idLength);
Loading history...
85
				$dbResult = $this->db->read('SELECT 1 FROM active_session WHERE session_id = ' . $this->db->escapeString($this->sessionID));
86
			} while ($dbResult->hasRecord()); //Make sure we haven't somehow clashed with someone else's session.
87
88
			// This is a minor hack to make sure that setcookie is not called
89
			// for CLI programs and tests (to avoid "headers already sent").
90
			if (headers_sent() === false) {
91
				setcookie('session_id', $this->sessionID);
92
			}
93
		}
94
95
		// Delete any expired sessions
96
		$this->db->write('DELETE FROM active_session WHERE last_accessed < ' . $this->db->escapeNumber(time() - self::TIME_BEFORE_EXPIRY));
97
98
		// try to get current session
99
		$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...
100
		$this->SN = Request::get('sn', '');
101
		$this->fetchVarInfo();
102
103
		if (!$this->ajax && $this->hasCurrentVar()) {
104
			$file = $this->getCurrentVar()::class;
105
			$loadDelay = self::URL_LOAD_DELAY[$file] ?? 0;
106
			$timeBetweenLoads = microtime(true) - $this->lastAccessed;
107
			if ($timeBetweenLoads < $loadDelay) {
108
				$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

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

348
			$sn = /** @scrutinizer ignore-call */ random_alphabetic_string(6);
Loading history...
349
		} while (isset($this->links[$sn]) && $sn === $this->SN);
350
		$this->links[$sn] = $container;
351
		return $sn;
352
	}
353
354
	public function addAjaxReturns(string $element, string $contents): bool {
355
		$this->ajaxReturns[$element] = $contents;
356
		return isset($this->previousAjaxReturns[$element]) && $this->previousAjaxReturns[$element] == $contents;
357
	}
358
359
	public function saveAjaxReturns(): void {
360
		if (empty($this->ajaxReturns)) {
361
			return;
362
		}
363
		$this->db->write('UPDATE active_session SET ajax_returns=' . $this->db->escapeObject($this->ajaxReturns, true) .
364
				' WHERE session_id=' . $this->db->escapeString($this->sessionID));
365
	}
366
367
}
368