‪TYPO3CMS  10.4
RedisSessionBackend.php
Go to the documentation of this file.
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 
19 
20 use Psr\Log\LoggerAwareInterface;
21 use Psr\Log\LoggerAwareTrait;
25 
33 {
34  use LoggerAwareTrait;
35 
39  protected ‪$configuration = [];
40 
46  protected ‪$connected = false;
47 
54  protected ‪$applicationIdentifier = '';
55 
61  protected ‪$redis;
62 
66  protected ‪$identifier;
67 
75  public function ‪initialize(string ‪$identifier, array ‪$configuration)
76  {
77  $this->redis = new \Redis();
78 
79  $this->configuration = ‪$configuration;
80  $this->identifier = ‪$identifier;
81  $this->applicationIdentifier = 'typo3_ses_'
82  . ‪$identifier . '_'
83  . sha1(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']) . '_';
84  }
85 
92  public function ‪validateConfiguration()
93  {
94  if (!extension_loaded('redis')) {
95  throw new \RuntimeException(
96  'The PHP extension "redis" must be installed and loaded in order to use the redis session backend.',
97  1481269826
98  );
99  }
100 
101  if (isset($this->configuration['database'])) {
102  if (!is_int($this->configuration['database'])) {
103  throw new \InvalidArgumentException(
104  'The specified database number is of type "' . gettype($this->configuration['database']) .
105  '" but an integer is expected.',
106  1481270871
107  );
108  }
109 
110  if ($this->configuration['database'] < 0) {
111  throw new \InvalidArgumentException(
112  'The specified database "' . $this->configuration['database'] . '" must be greater or equal than zero.',
113  1481270923
114  );
115  }
116  }
117  }
118 
119  public function ‪hash(string $sessionId): string
120  {
121  // The sha1 hash ensures we have good length for the key.
122  $key = sha1(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . 'core-session-backend');
123  return hash_hmac('sha256', $sessionId, $key);
124  }
125 
133  public function get(string $sessionId): array
134  {
135  $this->‪initializeConnection();
136 
137  $hashedSessionId = $this->‪hash($sessionId);
138  $rawData = $this->redis->get($this->‪getSessionKeyName($hashedSessionId));
139  if ($rawData !== false) {
140  $decodedValue = json_decode($rawData, true);
141  if (is_array($decodedValue)) {
142  return $decodedValue;
143  }
144  }
145  // Fallback to the non-hashed-value, will be removed in TYPO3 v11
146  $rawData = $this->redis->get($this->‪getSessionKeyName($sessionId));
147  if ($rawData !== false) {
148  $decodedValue = json_decode($rawData, true);
149  if (is_array($decodedValue)) {
150  return $decodedValue;
151  }
152  }
153  throw new SessionNotFoundException('Session could not be fetched from redis', 1481885583);
154  }
155 
163  public function remove(string $sessionId): bool
164  {
165  $this->‪initializeConnection();
166  $status = $this->redis->del($this->‪getSessionKeyName($this->‪hash($sessionId))) >= 1;
167  // Checking for non-hashed-identifier, will be removed in TYPO3 v11
168  $statusLegacy = $this->redis->del($this->‪getSessionKeyName($sessionId)) >= 1;
169 
170  return $status || $statusLegacy;
171  }
172 
183  public function set(string $sessionId, array $sessionData): array
184  {
185  $this->‪initializeConnection();
186 
187  $hashedSessionId = $this->‪hash($sessionId);
188  $sessionData['ses_id'] = $hashedSessionId;
189  $sessionData['ses_tstamp'] = ‪$GLOBALS['EXEC_TIME'] ?? time();
190 
191  // nx will not allow overwriting existing keys
192  $jsonString = json_encode($sessionData);
193  $wasSet = is_string($jsonString) && $this->redis->set(
194  $this->‪getSessionKeyName($hashedSessionId),
195  $jsonString,
196  ['nx']
197  );
198 
199  if (!$wasSet) {
200  throw new SessionNotCreatedException('Session could not be written to Redis', 1481895647);
201  }
202 
203  return $sessionData;
204  }
205 
216  public function ‪update(string $sessionId, array $sessionData): array
217  {
218  $hashedSessionId = $this->‪hash($sessionId);
219  try {
220  $sessionData = array_merge($this->get($sessionId), $sessionData);
221  } catch (SessionNotFoundException $e) {
222  throw new SessionNotUpdatedException('Cannot update non-existing record', 1484389971, $e);
223  }
224  $sessionData['ses_id'] = $hashedSessionId;
225  $sessionData['ses_tstamp'] = ‪$GLOBALS['EXEC_TIME'] ?? time();
226 
227  $key = $this->‪getSessionKeyName($hashedSessionId);
228  $jsonString = json_encode($sessionData);
229  $wasSet = is_string($jsonString) && $this->redis->set($key, $jsonString);
230 
231  if (!$wasSet) {
232  throw new SessionNotUpdatedException('Session could not be updated in Redis', 1481896383);
233  }
234 
235  return $sessionData;
236  }
237 
244  public function ‪collectGarbage(int $maximumLifetime, int $maximumAnonymousLifetime = 0)
245  {
246  foreach ($this->‪getAll() as $sessionRecord) {
247  if ($sessionRecord['ses_anonymous']) {
248  if ($maximumAnonymousLifetime > 0 && ($sessionRecord['ses_tstamp'] + $maximumAnonymousLifetime) < ‪$GLOBALS['EXEC_TIME']) {
249  $this->redis->del($this->‪getSessionKeyName($sessionRecord['ses_id']));
250  }
251  } else {
252  if (($sessionRecord['ses_tstamp'] + $maximumLifetime) < ‪$GLOBALS['EXEC_TIME']) {
253  $this->redis->del($this->‪getSessionKeyName($sessionRecord['ses_id']));
254  }
255  }
256  }
257  }
258 
264  protected function ‪initializeConnection()
265  {
266  if ($this->connected) {
267  return;
268  }
269 
270  try {
271  $this->connected = $this->redis->pconnect(
272  $this->configuration['hostname'] ?? '127.0.0.1',
273  $this->configuration['port'] ?? 6379,
274  0.0,
275  $this->identifier
276  );
277  } catch (\RedisException $e) {
278  $this->logger->alert('Could not connect to redis server.', ['exception' => $e]);
279  }
280 
281  if (!$this->connected) {
282  throw new \RuntimeException(
283  'Could not connect to redis server at ' . $this->configuration['hostname'] . ':' . $this->configuration['port'],
284  1482242961
285  );
286  }
287 
288  if (isset($this->configuration['password'])
289  && $this->configuration['password'] !== ''
290  && !$this->redis->auth($this->configuration['password'])
291  ) {
292  throw new \RuntimeException(
293  'The given password was not accepted by the redis server.',
294  1481270961
295  );
296  }
297 
298  if (isset($this->configuration['database'])
299  && $this->configuration['database'] > 0
300  && !$this->redis->select($this->configuration['database'])
301  ) {
302  throw new \RuntimeException(
303  'The given database "' . $this->configuration['database'] . '" could not be selected.',
304  1481270987
305  );
306  }
307  }
308 
314  public function ‪getAll(): array
315  {
316  $this->‪initializeConnection();
317 
318  $keys = [];
319  // Initialize our iterator to null, needed by redis->scan
320  $iterator = null;
321  $this->redis->setOption(\Redis::OPT_SCAN, (string)\Redis::SCAN_RETRY);
322  $pattern = $this->‪getSessionKeyName('*');
323  // retry when we get no keys back, redis->scan returns a chunk (array) of keys per iteration
324  while (($keyChunk = $this->redis->scan($iterator, $pattern)) !== false) {
325  foreach ($keyChunk as $key) {
326  $keys[] = $key;
327  }
328  }
329 
330  $encodedSessions = $this->redis->mGet($keys);
331  if (!is_array($encodedSessions)) {
332  return [];
333  }
334 
335  $sessions = [];
336  foreach ($encodedSessions as $session) {
337  if (is_string($session)) {
338  $decodedSession = json_decode($session, true);
339  if ($decodedSession) {
340  $sessions[] = $decodedSession;
341  }
342  }
343  }
344 
345  return $sessions;
346  }
347 
352  protected function ‪getSessionKeyName(string $sessionId): string
353  {
354  return $this->applicationIdentifier . $sessionId;
355  }
356 
360  protected function ‪getSessionTimeout(): int
361  {
362  return (int)(‪$GLOBALS['TYPO3_CONF_VARS'][‪$this->identifier]['sessionTimeout'] ?? 86400);
363  }
364 }
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\$identifier
‪string $identifier
Definition: RedisSessionBackend.php:61
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\getSessionTimeout
‪int getSessionTimeout()
Definition: RedisSessionBackend.php:355
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\getSessionKeyName
‪string getSessionKeyName(string $sessionId)
Definition: RedisSessionBackend.php:347
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\$configuration
‪array $configuration
Definition: RedisSessionBackend.php:38
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\update
‪array update(string $sessionId, array $sessionData)
Definition: RedisSessionBackend.php:211
‪TYPO3\CMS\Core\Session\Backend\HashableSessionBackendInterface
Definition: HashableSessionBackendInterface.php:21
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\collectGarbage
‪collectGarbage(int $maximumLifetime, int $maximumAnonymousLifetime=0)
Definition: RedisSessionBackend.php:239
‪TYPO3\CMS\Core\Session\Backend\Exception\SessionNotCreatedException
Definition: SessionNotCreatedException.php:24
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\$connected
‪bool $connected
Definition: RedisSessionBackend.php:44
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\initialize
‪initialize(string $identifier, array $configuration)
Definition: RedisSessionBackend.php:70
‪TYPO3\CMS\Core\Session\Backend\SessionBackendInterface
Definition: SessionBackendInterface.php:28
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend
Definition: RedisSessionBackend.php:33
‪TYPO3\CMS\Core\Session\Backend\Exception\SessionNotUpdatedException
Definition: SessionNotUpdatedException.php:24
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\hash
‪hash(string $sessionId)
Definition: RedisSessionBackend.php:114
‪TYPO3\CMS\Core\Session\Backend
Definition: DatabaseSessionBackend.php:18
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\initializeConnection
‪initializeConnection()
Definition: RedisSessionBackend.php:259
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\$redis
‪Redis $redis
Definition: RedisSessionBackend.php:57
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\$applicationIdentifier
‪string $applicationIdentifier
Definition: RedisSessionBackend.php:51
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\getAll
‪array getAll()
Definition: RedisSessionBackend.php:309
‪TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException
Definition: SessionNotFoundException.php:24
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\validateConfiguration
‪validateConfiguration()
Definition: RedisSessionBackend.php:87