Passed
Push — master ( f16b47...733353 )
by
unknown
13:48
created

UserSessionManager::removeSession()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Core\Session;
19
20
use Psr\Http\Message\ServerRequestInterface;
21
use Psr\Log\LoggerAwareInterface;
22
use Psr\Log\LoggerAwareTrait;
23
use TYPO3\CMS\Core\Authentication\IpLocker;
24
use TYPO3\CMS\Core\Crypto\Random;
25
use TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException;
26
use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28
29
/**
30
 * The purpose of the UserSessionManager is to create new user session objects (acting as a factory),
31
 * depending on the need / request, and to fetch sessions from the Session Backend, effectively
32
 * encapsulating all calls to the SessionManager
33
 */
34
class UserSessionManager implements LoggerAwareInterface
35
{
36
    use LoggerAwareTrait;
37
38
    protected const SESSION_ID_LENGTH = 32;
39
    protected const GARBAGE_COLLECTION_LIFETIME = 86400;
40
    protected const LIFETIME_OF_ANONYMOUS_SESSION_DATA = 86400;
41
42
    /**
43
     * Session timeout (on the storage-side, used to know until a session (timestamp) is valid
44
     *
45
     * If >0: session-timeout in seconds.
46
     * If =0: Instant logout after login.
47
     *
48
     * @var int
49
     */
50
    protected int $sessionLifetime;
51
52
    protected int $garbageCollectionForAnonymousSessions = self::LIFETIME_OF_ANONYMOUS_SESSION_DATA;
53
    protected SessionBackendInterface $sessionBackend;
54
    protected IpLocker $ipLocker;
55
56
    /**
57
     * Constructor. Marked as internal, as it is recommended to use the factory method "create"
58
     *
59
     * @param SessionBackendInterface $sessionBackend
60
     * @param int $sessionLifetime
61
     * @param IpLocker $ipLocker
62
     * @internal
63
     */
64
    public function __construct(SessionBackendInterface $sessionBackend, int $sessionLifetime, IpLocker $ipLocker)
65
    {
66
        $this->sessionBackend = $sessionBackend;
67
        $this->sessionLifetime = $sessionLifetime;
68
        $this->ipLocker = $ipLocker;
69
    }
70
71
    protected function setGarbageCollectionTimeoutForAnonymousSessions(int $garbageCollectionForAnonymousSessions = 0): void
72
    {
73
        if ($garbageCollectionForAnonymousSessions > 0) {
74
            $this->garbageCollectionForAnonymousSessions = $garbageCollectionForAnonymousSessions;
75
        }
76
    }
77
78
    /**
79
     * sessionId is set to ses_id if a cookie is present in the request.
80
     * Otherwise a new anonymous session will start.
81
     *
82
     * @param ServerRequestInterface $request
83
     * @param string $cookieName
84
     * @return UserSession
85
     */
86
    public function createFromRequestOrAnonymous(ServerRequestInterface $request, string $cookieName): UserSession
87
    {
88
        $sessionId = (string)($request->getCookieParams()[$cookieName] ?? '');
89
        return $this->getSessionFromSessionId($sessionId) ?? $this->createAnonymousSession();
90
    }
91
92
    /**
93
     * sessionId is set to ses_id if a cookie is present in $_COOKIE.
94
     * Otherwise a new anonymous session will start.
95
     *
96
     * @param string $cookieName
97
     * @return UserSession
98
     */
99
    public function createFromGlobalCookieOrAnonymous(string $cookieName): UserSession
100
    {
101
        $sessionId = isset($_COOKIE[$cookieName]) ? stripslashes((string)$_COOKIE[$cookieName]) : '';
102
        return $this->getSessionFromSessionId($sessionId) ?? $this->createAnonymousSession();
103
    }
104
105
    /**
106
     * Creates a non-fixated session without a user logged in
107
     *
108
     * @return UserSession
109
     */
110
    public function createAnonymousSession(): UserSession
111
    {
112
        $randomSessionId = $this->createSessionId();
113
        return UserSession::createNonFixated($randomSessionId);
114
    }
115
116
    /**
117
     * Creates a new user session object from an existing session data.
118
     *
119
     * @param string $sessionId The session id to be looked up in the session backend
120
     * @return UserSession The created user session object
121
     * @internal this is only used as a bridge for existing methods, might be removed or renamed without further notice
122
     */
123
    public function createSessionFromStorage(string $sessionId): UserSession
124
    {
125
        $this->logger->debug('Fetch session with identifier ' . $sessionId);
126
        $sessionRecord = $this->sessionBackend->get($sessionId);
127
        return UserSession::createFromRecord($sessionId, $sessionRecord);
128
    }
129
130
    /**
131
     * Check if a session has expired. This is the case if sessionLifetime is 0,
132
     * or current time greater than sessionLifetime plus last update time of the session.
133
     *
134
     * @param UserSession $session
135
     * @return bool
136
     */
137
    public function hasExpired(UserSession $session): bool
138
    {
139
        return $this->sessionLifetime === 0 || $GLOBALS['EXEC_TIME'] > $session->getLastUpdated() + $this->sessionLifetime;
140
    }
141
142
    /**
143
     * Check if a session will expire within the given grace period.
144
     *
145
     * @param UserSession $session
146
     * @param int $gracePeriod
147
     * @return bool
148
     */
149
    public function willExpire(UserSession $session, int $gracePeriod): bool
150
    {
151
        return $GLOBALS['EXEC_TIME'] >= ($session->getLastUpdated() + $this->sessionLifetime) - $gracePeriod;
152
    }
153
154
    /**
155
     * Persists an anonymous session without a user logged in,
156
     * in order to store session data between requests.
157
     *
158
     * @param UserSession $session The user session to fixate
159
     * @param bool $isPermanent If TRUE, the session will get the is_permanent flag
160
     *
161
     * @throws Backend\Exception\SessionNotCreatedException
162
     */
163
    public function fixateAnonymousSession(UserSession $session, bool $isPermanent = false): void
164
    {
165
        $sessionIpLock = $this->ipLocker->getSessionIpLock((string)GeneralUtility::getIndpEnv('REMOTE_ADDR'));
166
167
        $sessionRecord = $session->toArray();
168
        $sessionRecord['ses_iplock'] = $sessionIpLock;
169
        $sessionRecord['ses_userid'] = 0;
170
        if ($isPermanent) {
171
            $sessionRecord['ses_permanent'] = $isPermanent;
172
        }
173
        $this->sessionBackend->set($session->getIdentifier(), $sessionRecord);
174
    }
175
176
    /**
177
     * Removes existing entries, creates and returns a new user session record
178
     *
179
     * @param UserSession $session The user session to recreate
180
     * @param int $userId The user id the session belongs to
181
     * @param bool $isPermanent If TRUE, the session will get the is_permanent flag
182
     * @return UserSession The newly created user session object
183
     *
184
     * @throws Backend\Exception\SessionNotCreatedException
185
     */
186
    public function elevateToFixatedUserSession(UserSession $session, int $userId, bool $isPermanent = false): UserSession
187
    {
188
        $sessionId = $session->getIdentifier();
189
        $this->logger->debug('Create session ses_id = ' . $sessionId);
190
        // Delete any session entry first
191
        $this->sessionBackend->remove($sessionId);
192
        // Re-create session entry
193
        $sessionIpLock = $this->ipLocker->getSessionIpLock((string)GeneralUtility::getIndpEnv('REMOTE_ADDR'));
194
        $sessionRecord = [
195
            'ses_iplock' => $sessionIpLock,
196
            'ses_userid' => $userId,
197
            'ses_tstamp' => $GLOBALS['EXEC_TIME'],
198
            'ses_data' => '',
199
        ];
200
        if ($isPermanent) {
201
            $sessionRecord['is_permanent'] = 1;
202
        }
203
        $sessionRecord = $this->sessionBackend->set($sessionId, $sessionRecord);
204
        return UserSession::createFromRecord($sessionId, $sessionRecord, true);
205
    }
206
207
    /**
208
     * Regenerate the session ID and transfer the session to new ID
209
     * Call this method whenever a user proceeds to a higher authorization level
210
     * e.g. when an anonymous session is now authenticated.
211
     *
212
     * @param string $sessionId The session id
213
     * @param array $existingSessionRecord If given, this session record will be used instead of fetching again
214
     * @param bool $anonymous If true session will be regenerated as anonymous session
215
     * @return UserSession
216
     *
217
     * @throws Backend\Exception\SessionNotCreatedException
218
     * @throws SessionNotFoundException
219
     */
220
    public function regenerateSession(
221
        string $sessionId,
222
        array $existingSessionRecord = [],
223
        bool $anonymous = false
224
    ): UserSession {
225
        if (empty($existingSessionRecord)) {
226
            $existingSessionRecord = $this->sessionBackend->get($sessionId);
227
        }
228
        if ($anonymous) {
229
            $existingSessionRecord['ses_userid'] = 0;
230
        }
231
        // Update session record with new ID
232
        $newSessionId = $this->createSessionId();
233
        $this->sessionBackend->set($newSessionId, $existingSessionRecord);
234
        $this->sessionBackend->remove($sessionId);
235
        return UserSession::createFromRecord($newSessionId, $existingSessionRecord, true);
236
    }
237
238
    /**
239
     * Updates the session timestamp for the given user session if
240
     * the session is marked as "needs update" (which means the current
241
     * timestamp is greater than "last updated + a specified gracetime-value".
242
     *
243
     * @param UserSession $session
244
     *
245
     * @throws Backend\Exception\SessionNotUpdatedException
246
     */
247
    public function updateSessionTimestamp(UserSession $session): void
248
    {
249
        if ($session->needsUpdate()) {
250
            // Update the session timestamp by writing a dummy update. (Backend will update the timestamp)
251
            $this->sessionBackend->update($session->getIdentifier(), []);
252
        }
253
    }
254
255
    public function isSessionPersisted(UserSession $session): bool
256
    {
257
        return $this->getSessionFromSessionId($session->getIdentifier()) !== null;
258
    }
259
260
    public function removeSession(UserSession $session): void
261
    {
262
        $this->sessionBackend->remove($session->getIdentifier());
263
    }
264
265
    public function updateSession(UserSession $session): void
266
    {
267
        $this->sessionBackend->update($session->getIdentifier(), $session->toArray());
268
    }
269
270
    public function collectGarbage(int $garbageCollectionProbability = 1): void
271
    {
272
        // If we're lucky we'll get to clean up old sessions
273
        if (random_int(0, mt_getrandmax()) % 100 <= $garbageCollectionProbability) {
274
            $this->sessionBackend->collectGarbage(
275
                $this->sessionLifetime > 0 ? $this->sessionLifetime : self::GARBAGE_COLLECTION_LIFETIME,
276
                $this->garbageCollectionForAnonymousSessions
277
            );
278
        }
279
    }
280
281
    /**
282
     * Creates a new session ID using a random with SESSION_ID_LENGTH as length
283
     *
284
     * @return string
285
     */
286
    protected function createSessionId(): string
287
    {
288
        return GeneralUtility::makeInstance(Random::class)->generateRandomHexString(self::SESSION_ID_LENGTH);
289
    }
290
291
    /**
292
     * Tries to fetch a user session form the session backend.
293
     * If non is given, an anonymous session will be created.
294
     *
295
     * @param string $id
296
     * @return UserSession|null The created user session object or null
297
     */
298
    protected function getSessionFromSessionId(string $id): ?UserSession
299
    {
300
        if ($id === '') {
301
            return null;
302
        }
303
        try {
304
            $sessionRecord = $this->sessionBackend->get($id);
305
            if ($sessionRecord === []) {
306
                return null;
307
            }
308
            // If the session does not match the current IP lock, it should be treated as invalid
309
            // and a new session should be created.
310
            if ($this->ipLocker->validateRemoteAddressAgainstSessionIpLock(
311
                (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'),
312
                $sessionRecord['ses_iplock']
313
            )) {
314
                return UserSession::createFromRecord($id, $sessionRecord);
315
            }
316
        } catch (SessionNotFoundException $e) {
317
            return null;
318
        }
319
320
        return null;
321
    }
322
323
    /**
324
     * Create a UserSessionManager instance for the given login type. Has several optional arguments used for testing purposes
325
     * to inject dummy objects if needed.
326
     *
327
     * Ideally, this factory encapsulates all "TYPO3_CONF_VARS" options, so the actual object does not need to consider any
328
     * global state.
329
     *
330
     * @param string $loginType
331
     * @param int|null $sessionLifetime
332
     * @param SessionManager|null $sessionManager
333
     * @param IpLocker|null $ipLocker
334
     * @return static
335
     */
336
    public static function create(string $loginType, int $sessionLifetime = null, SessionManager $sessionManager = null, IpLocker $ipLocker = null): self
337
    {
338
        $sessionManager = $sessionManager ?? GeneralUtility::makeInstance(SessionManager::class);
339
        $ipLocker = $ipLocker ?? GeneralUtility::makeInstance(
340
            IpLocker::class,
341
            $GLOBALS['TYPO3_CONF_VARS'][$loginType]['lockIP'],
342
            $GLOBALS['TYPO3_CONF_VARS'][$loginType]['lockIPv6']
343
        );
344
        $lifetime = (int)($GLOBALS['TYPO3_CONF_VARS'][$loginType]['lifetime'] ?? 0);
345
        $sessionLifetime = $sessionLifetime ?? (int)$GLOBALS['TYPO3_CONF_VARS'][$loginType]['sessionTimeout'];
346
        if ($sessionLifetime > 0 && $sessionLifetime < $lifetime && $lifetime > 0) {
347
            // If server session timeout is non-zero but less than client session timeout: Copy this value instead.
348
            $sessionLifetime = $lifetime;
349
        }
350
        $object = GeneralUtility::makeInstance(
351
            self::class,
352
            $sessionManager->getSessionBackend($loginType),
353
            $sessionLifetime,
354
            $ipLocker
355
        );
356
        if ($loginType === 'FE') {
357
            $object->setGarbageCollectionTimeoutForAnonymousSessions((int)($GLOBALS['TYPO3_CONF_VARS']['FE']['sessionDataLifetime'] ?? 0));
358
        }
359
        return $object;
360
    }
361
}
362