Passed
Pull Request — master (#1882)
by Janko
28:33
created

Session::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 5
dl 0
loc 8
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Stu\Lib;
4
5
use DateTime;
6
use Override;
0 ignored issues
show
Bug introduced by
The type Override was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use RuntimeException;
8
use Stu\Component\Game\TimeConstants;
0 ignored issues
show
Bug introduced by
The type Stu\Component\Game\TimeConstants was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use Stu\Exception\SessionInvalidException;
10
use Stu\Module\Control\StuHashInterface;
11
use Stu\Module\Logging\LoggerUtilFactoryInterface;
12
use Stu\Module\Logging\LoggerUtilInterface;
13
use Stu\Module\PlayerSetting\Lib\UserEnum;
0 ignored issues
show
Bug introduced by
The type Stu\Module\PlayerSetting\Lib\UserEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use Stu\Orm\Entity\UserInterface;
15
use Stu\Orm\Entity\UserLockInterface;
16
use Stu\Orm\Repository\SessionStringRepositoryInterface;
17
use Stu\Orm\Repository\UserIpTableRepositoryInterface;
18
use Stu\Orm\Repository\UserRepositoryInterface;
19
20
final class Session implements SessionInterface
21
{
22
    private LoggerUtilInterface $loggerUtil;
23
24
    private ?UserInterface $user = null;
25
26 4
    public function __construct(
27
        private UserIpTableRepositoryInterface $userIpTableRepository,
28
        private SessionStringRepositoryInterface $sessionStringRepository,
29
        private UserRepositoryInterface $userRepository,
30
        private StuHashInterface $stuHash,
31
        LoggerUtilFactoryInterface $loggerUtilFactory
32
    ) {
33 4
        $this->loggerUtil = $loggerUtilFactory->getLoggerUtil();
34
    }
35
36
    #[Override]
37
    public function createSession(bool $session_check = true): void
38
    {
39
        if (!$this->isLoggedIn() && $session_check) {
40
            throw new SessionInvalidException('Session abgelaufen');
41
        }
42
        if ($session_check && (!$_SESSION['uid'] || !$_SESSION['login'])) {
43
            $this->logout();
44
            return;
45
        }
46
        if ($this->isLoggedIn() && $session_check) {
47
            $this->chklogin();
48
        }
49
    }
50
51
    /**
52
     * @api
53
     */
54
    #[Override]
55
    public function checkLoginCookie(): void
56
    {
57
        $sstr = $_COOKIE['sstr'] ?? '';
58
        $uid = (int) ($_SESSION['uid'] ?? 0);
59
        if ($uid > 0) {
60
            $this->performCookieLogin($uid, $sstr);
61
        }
62
    }
63
64
    private function isLoggedIn(): bool
65
    {
66
        return array_key_exists('uid', $_SESSION)
67
            && array_key_exists('login', $_SESSION)
68
            && $_SESSION['login'] == 1;
69
    }
70
71
    /**
72
     * @api
73
     */
74
    #[Override]
75
    public function getUser(): ?UserInterface
76
    {
77
        return $this->user;
78
    }
79
80
    #[Override]
81
    public function login(string $login, string $password): bool
82
    {
83
        $this->destroyLoginCookies();
84
85
        $user = $this->loadUser($login);
86
87
        $this->checkPassword($user, $password);
88
        $this->checkUser($user);
89
        $this->updateUser($user);
90
        $this->setUser($user);
91
92
        $this->sessionStringRepository->truncate($user);
93
94
        if (!$user->isSaveLogin()) {
95
            $cookieString = $this->buildCookieString($user);
96
            $this->loggerUtil->log(sprintf('noSaveLogin, set cookieString: %s', $cookieString));
97
            setcookie('sstr', $cookieString, ['expires' => time() + TimeConstants::TWO_DAYS_IN_SECONDS]);
98
        }
99
100
        // register login
101
        $this->addIpTableEntry($user);
102
103
        return true;
104
    }
105
106
    private function loadUser(string $login): UserInterface
107
    {
108
        $user = $this->userRepository->getByLogin(mb_strtolower($login));
109
        if ($user === null) {
110
            if (is_numeric($login)) {
111
                $user = $this->userRepository->find((int)$login);
112
            }
113
114
            if ($user === null) {
115
                throw new LoginException(_('Login oder Passwort inkorrekt'));
116
            }
117
        }
118
119
        return $user;
120
    }
121
122
    private function checkPassword(UserInterface $user, string $password): void
123
    {
124
        $password_hash = $user->getPassword();
125
126
        if (!password_verify($password, $password_hash)) {
127
            throw new LoginException(_('Login oder Passwort inkorrekt'));
128
        }
129
130
        if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) {
131
            $user->setPassword(password_hash($password, PASSWORD_DEFAULT));
132
133
            $this->userRepository->save($user);
134
        }
135
    }
136
137
    private function checkUser(UserInterface $user): void
138
    {
139
        if ($user->isLocked()) {
140
            /** @var UserLockInterface $userLock */
141
            $userLock = $user->getUserLock();
142
143
            throw new UserLockedException(
144
                _('Dein Spieleraccount wurde gesperrt'),
145
                sprintf(_('Dein Spieleraccount ist noch für %d Ticks gesperrt. Begründung: %s'), $userLock->getRemainingTicks(), $userLock->getReason())
146
            );
147
        }
148
        if ($user->getDeletionMark() === UserEnum::DELETION_CONFIRMED) {
149
            throw new LoginException(_('Dein Spieleraccount ist zur Löschung vorgesehen'));
150
        }
151
    }
152
153
    private function updateUser(UserInterface $user): void
154
    {
155
        if ($user->getState() === UserEnum::USER_STATE_NEW) {
156
            $user->setState(UserEnum::USER_STATE_UNCOLONIZED);
157
158
            $this->userRepository->save($user);
159
        }
160
161
        if ($user->isVacationMode()) {
162
            $user->setVacationMode(false);
163
        }
164
165
        $this->userRepository->save($user);
166
    }
167
168
    private function setUser(UserInterface $user): void
169
    {
170
        $_SESSION['uid'] = $user->getId();
171
        $_SESSION['login'] = 1;
172
173
        $this->user = $user;
174
    }
175
176
    private function addIpTableEntry(UserInterface $user): void
177
    {
178
        $ipTableEntry = $this->userIpTableRepository->prototype();
179
        $ipTableEntry->setUser($user);
180
        $ipTableEntry->setIp((string) getenv('REMOTE_ADDR'));
181
        $ipTableEntry->setSessionId((string) session_id());
182
        $ipTableEntry->setUserAgent((string) getenv('HTTP_USER_AGENT'));
183
        $ipTableEntry->setStartDate(new DateTime());
184
185
        $this->userIpTableRepository->save($ipTableEntry);
186
    }
187
188
    private function buildCookieString(UserInterface $user): string
189
    {
190
        return $this->stuHash->hash(($user->getId() . $user->getEMail() . $user->getCreationDate()));
191
    }
192
193
    private function destroySession(?UserInterface $user = null): void
194
    {
195
        if ($this->user !== null || $user !== null) {
196
            $userToTruncate = $user ?? $this->user;
197
            $this->sessionStringRepository->truncate($userToTruncate);
0 ignored issues
show
Bug introduced by
It seems like $userToTruncate can also be of type null; however, parameter $user of Stu\Orm\Repository\Sessi...ryInterface::truncate() does only seem to accept Stu\Orm\Entity\UserInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

197
            $this->sessionStringRepository->truncate(/** @scrutinizer ignore-type */ $userToTruncate);
Loading history...
198
        }
199
200
        if ($user === null) {
201
            $this->destroyLoginCookies();
202
            setcookie(session_name(), '', ['expires' => time() - 42000]);
203
            if (@session_destroy() === false) {
204
                throw new RuntimeException('The session could not be destroyed');
205
            }
206
207
            $this->user = null;
208
        }
209
    }
210
211
    private function destroyLoginCookies(): void
212
    {
213
        setcookie('sstr', 0);
214
    }
215
216
    #[Override]
217
    public function logout(?UserInterface $user = null): void
218
    {
219
        $this->destroySession($user);
220
    }
221
222
    private function performCookieLogin(int $uid, string $sstr): void
223
    {
224
        if (strlen($sstr) != 40) {
225
            $this->destroySession();
226
            return;
227
        }
228
        $user = $this->userRepository->find($uid);
229
        if ($user === null) {
230
            $this->destroySession();
231
            return;
232
        }
233
        if ($this->buildCookieString($user) !== $sstr) {
234
            $this->destroySession();
235
            return;
236
        }
237
        if ($user->getState() == UserEnum::USER_STATE_NEW) {
238
            throw new SessionInvalidException("Aktivierung");
239
        }
240
        if ($user->isLocked()) {
241
            throw new SessionInvalidException("Gesperrt");
242
        }
243
        if ($user->getDeletionMark() === UserEnum::DELETION_CONFIRMED) {
244
            throw new SessionInvalidException("Löschung");
245
        }
246
        if ($user->isVacationMode() === true) {
247
            $user->setVacationMode(false);
248
        }
249
        $this->userRepository->save($user);
250
251
        $this->setUser($user);
252
253
        $this->sessionStringRepository->truncate($user);
254
255
        //start session if not already active
256
        if (session_id() == '') {
257
            session_start();
258
        }
259
260
        // register login
261
        $this->addIpTableEntry($user);
262
    }
263
264
    private function chklogin(): void
265
    {
266
        if (!$this->isLoggedIn()) {
267
            throw new SessionInvalidException("Not logged in");
268
        }
269
270
        $userId = (int) $_SESSION['uid'];
271
272
        $user = $this->userRepository->find($userId);
273
274
        if ($user === null) {
275
            $this->logout();
276
            return;
277
        }
278
        $user->setLastaction(time());
279
280
        $this->userRepository->save($user);
281
282
283
        $ipTableEntry = $this->userIpTableRepository->findBySessionId(session_id());
284
        if ($ipTableEntry !== null) {
285
            $ipTableEntry->setEndDate(new DateTime());
286
287
            $this->userIpTableRepository->save($ipTableEntry);
288
        }
289
290
        $_SESSION['login'] = 1;
291
292
        $this->user = $user;
293
    }
294
295
    /**
296
     * @api
297
     */
298
    #[Override]
299
    public function storeSessionData($key, $value, bool $isSingleValue = false): void
300
    {
301
        $stored = false;
302
303
        $data = $this->user->getSessionDataUnserialized();
0 ignored issues
show
Bug introduced by
The method getSessionDataUnserialized() does not exist on null. ( Ignorable by Annotation )

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

303
        /** @scrutinizer ignore-call */ 
304
        $data = $this->user->getSessionDataUnserialized();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
304
        if (!array_key_exists($key, $data)) {
305
            if ($isSingleValue) {
306
                $data[$key] = $value;
307
                $stored = true;
308
            } else {
309
                $data[$key] = [];
310
            }
311
        }
312
        if (!$isSingleValue && !array_key_exists($value, $data[$key])) {
313
            $data[$key][$value] = 1;
314
            $stored = true;
315
        }
316
317
        if ($stored) {
318
            $this->user->setSessionData(serialize($data));
319
            $this->userRepository->save($this->user);
0 ignored issues
show
Bug introduced by
It seems like $this->user can also be of type null; however, parameter $post of Stu\Orm\Repository\UserRepositoryInterface::save() does only seem to accept Stu\Orm\Entity\UserInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

319
            $this->userRepository->save(/** @scrutinizer ignore-type */ $this->user);
Loading history...
320
        }
321
    }
322
323
    /**
324
     * @api
325
     */
326
    #[Override]
327
    public function deleteSessionData($key, $value = null): void
328
    {
329
        $data = $this->user->getSessionDataUnserialized();
330
        if (!array_key_exists($key, $data)) {
331
            return;
332
        }
333
        if ($value === null) {
334
            unset($data[$key]);
335
        } else {
336
            if (!array_key_exists($value, $data[$key])) {
337
                return;
338
            }
339
            unset($data[$key][$value]);
340
        }
341
        $this->user->setSessionData(serialize($data));
342
        $this->userRepository->save($this->user);
0 ignored issues
show
Bug introduced by
It seems like $this->user can also be of type null; however, parameter $post of Stu\Orm\Repository\UserRepositoryInterface::save() does only seem to accept Stu\Orm\Entity\UserInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

342
        $this->userRepository->save(/** @scrutinizer ignore-type */ $this->user);
Loading history...
343
    }
344
345
    /**
346
     * @api
347
     */
348
    #[Override]
349
    public function hasSessionValue($key, $value): bool
350
    {
351
        $data = $this->user->getSessionDataUnserialized();
352
        if (!array_key_exists($key, $data)) {
353
            return false;
354
        }
355
        return array_key_exists($value, $data[$key]);
356
    }
357
358
    /**
359
     * @api
360
     */
361
    #[Override]
362
    public function getSessionValue($key)
363
    {
364
        $data = $this->user->getSessionDataUnserialized();
365
        if (!array_key_exists($key, $data)) {
366
            return false;
367
        }
368
        return $data[$key];
369
    }
370
}
371