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