‪TYPO3CMS  ‪main
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 
74  public function ‪initialize(string ‪$identifier, array ‪$configuration)
75  {
76  $this->redis = new \Redis();
77 
78  $this->configuration = ‪$configuration;
79  $this->identifier = ‪$identifier;
80  $this->applicationIdentifier = 'typo3_ses_'
81  . ‪$identifier . '_'
82  . sha1(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']) . '_';
83  }
84 
91  public function ‪validateConfiguration()
92  {
93  if (!extension_loaded('redis')) {
94  throw new \RuntimeException(
95  'The PHP extension "redis" must be installed and loaded in order to use the redis session backend.',
96  1481269826
97  );
98  }
99 
100  if (isset($this->configuration['database'])) {
101  if (!is_int($this->configuration['database'])) {
102  throw new \InvalidArgumentException(
103  'The specified database number is of type "' . gettype($this->configuration['database']) .
104  '" but an integer is expected.',
105  1481270871
106  );
107  }
108 
109  if ($this->configuration['database'] < 0) {
110  throw new \InvalidArgumentException(
111  'The specified database "' . $this->configuration['database'] . '" must be greater or equal than zero.',
112  1481270923
113  );
114  }
115  }
116  }
117 
118  public function ‪hash(string $sessionId): string
119  {
120  // The sha1 hash ensures we have good length for the key.
121  $key = sha1(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . 'core-session-backend');
122  return hash_hmac('sha256', $sessionId, $key);
123  }
124 
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  throw new SessionNotFoundException('Session could not be fetched from redis', 1481885583);
144  }
145 
149  public function remove(string $sessionId): bool
150  {
151  $this->‪initializeConnection();
152 
153  $deleteResult = $this->redis->del($this->‪getSessionKeyName($this->‪hash($sessionId)));
154 
155  // Redis delete result is either `int`, `false` or a `\Redis` multi mode object, where delete state cannot get
156  // determined. Multi mode is not even supported by this session backend at all, therefore we handle this case as
157  // "not successful".
158  return is_int($deleteResult) && $deleteResult >= 1;
159  }
160 
169  public function set(string $sessionId, array $sessionData): array
170  {
171  $this->‪initializeConnection();
172 
173  $hashedSessionId = $this->‪hash($sessionId);
174  $sessionData['ses_id'] = $hashedSessionId;
175  $sessionData['ses_tstamp'] = ‪$GLOBALS['EXEC_TIME'] ?? time();
176 
177  // nx will not allow overwriting existing keys
178  $jsonString = json_encode($sessionData);
179  $wasSet = is_string($jsonString) && $this->redis->set(
180  $this->‪getSessionKeyName($hashedSessionId),
181  $jsonString,
182  ['nx']
183  );
184 
185  if (!$wasSet) {
186  throw new SessionNotCreatedException('Session could not be written to Redis', 1481895647);
187  }
188 
189  return $sessionData;
190  }
191 
201  public function ‪update(string $sessionId, array $sessionData): array
202  {
203  $hashedSessionId = $this->‪hash($sessionId);
204  try {
205  $sessionData = array_merge($this->get($sessionId), $sessionData);
206  } catch (SessionNotFoundException $e) {
207  throw new SessionNotUpdatedException('Cannot update non-existing record', 1484389971, $e);
208  }
209  $sessionData['ses_id'] = $hashedSessionId;
210  $sessionData['ses_tstamp'] = ‪$GLOBALS['EXEC_TIME'] ?? time();
211 
212  $key = $this->‪getSessionKeyName($hashedSessionId);
213  $jsonString = json_encode($sessionData);
214  $wasSet = is_string($jsonString) && $this->redis->set($key, $jsonString);
215 
216  if (!$wasSet) {
217  throw new SessionNotUpdatedException('Session could not be updated in Redis', 1481896383);
218  }
219 
220  return $sessionData;
221  }
222 
229  public function ‪collectGarbage(int $maximumLifetime, int $maximumAnonymousLifetime = 0)
230  {
231  foreach ($this->‪getAll() as $sessionRecord) {
232  if (!($sessionRecord['ses_userid'] ?? false)) {
233  if ($maximumAnonymousLifetime > 0 && ($sessionRecord['ses_tstamp'] + $maximumAnonymousLifetime) < ‪$GLOBALS['EXEC_TIME']) {
234  $this->redis->del($this->‪getSessionKeyName($sessionRecord['ses_id']));
235  }
236  } else {
237  if (($sessionRecord['ses_tstamp'] + $maximumLifetime) < ‪$GLOBALS['EXEC_TIME']) {
238  $this->redis->del($this->‪getSessionKeyName($sessionRecord['ses_id']));
239  }
240  }
241  }
242  }
243 
249  protected function ‪initializeConnection()
250  {
251  if ($this->connected) {
252  return;
253  }
254 
255  try {
256  $this->connected = $this->redis->pconnect(
257  $this->configuration['hostname'] ?? '127.0.0.1',
258  $this->configuration['port'] ?? 6379,
259  0.0,
260  $this->identifier
261  );
262  } catch (\RedisException $e) {
263  $this->logger->alert('Could not connect to redis server.', ['exception' => $e]);
264  }
265 
266  if (!$this->connected) {
267  throw new \RuntimeException(
268  'Could not connect to redis server at ' . $this->configuration['hostname'] . ':' . $this->configuration['port'],
269  1482242961
270  );
271  }
272 
273  if (isset($this->configuration['password'])
274  && $this->configuration['password'] !== ''
275  && !$this->redis->auth($this->configuration['password'])
276  ) {
277  throw new \RuntimeException(
278  'The given password was not accepted by the redis server.',
279  1481270961
280  );
281  }
282 
283  if (isset($this->configuration['database'])
284  && $this->configuration['database'] > 0
285  && !$this->redis->select($this->configuration['database'])
286  ) {
287  throw new \RuntimeException(
288  'The given database "' . $this->configuration['database'] . '" could not be selected.',
289  1481270987
290  );
291  }
292  }
293 
299  public function ‪getAll(): array
300  {
301  $this->‪initializeConnection();
302 
303  $keys = [];
304  // Initialize our iterator to null, needed by redis->scan
305  $iterator = null;
306  $this->redis->setOption(\Redis::OPT_SCAN, (string)\Redis::SCAN_RETRY);
307  $pattern = $this->‪getSessionKeyName('*');
308  // retry when we get no keys back, redis->scan returns a chunk (array) of keys per iteration
309  while (($keyChunk = $this->redis->scan($iterator, $pattern)) !== false) {
310  foreach ($keyChunk as $key) {
311  $keys[] = $key;
312  }
313  }
314 
315  $encodedSessions = $this->redis->mGet($keys);
316  if (!is_array($encodedSessions)) {
317  return [];
318  }
319 
320  $sessions = [];
321  foreach ($encodedSessions as $session) {
322  if (is_string($session)) {
323  $decodedSession = json_decode($session, true);
324  if ($decodedSession) {
325  $sessions[] = $decodedSession;
326  }
327  }
328  }
329 
330  return $sessions;
331  }
332 
333  protected function ‪getSessionKeyName(string $sessionId): string
334  {
335  return $this->applicationIdentifier . $sessionId;
336  }
337 
338  protected function ‪getSessionTimeout(): int
339  {
340  return (int)(‪$GLOBALS['TYPO3_CONF_VARS'][‪$this->identifier]['sessionTimeout'] ?? 86400);
341  }
342 }
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\$identifier
‪string $identifier
Definition: RedisSessionBackend.php:61
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\getSessionKeyName
‪getSessionKeyName(string $sessionId)
Definition: RedisSessionBackend.php:328
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\getSessionTimeout
‪getSessionTimeout()
Definition: RedisSessionBackend.php:333
‪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:196
‪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:224
‪TYPO3\CMS\Core\Session\Backend\Exception\SessionNotCreatedException
Definition: SessionNotCreatedException.php:23
‪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:69
‪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:23
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\hash
‪hash(string $sessionId)
Definition: RedisSessionBackend.php:113
‪TYPO3\CMS\Core\Session\Backend
Definition: DatabaseSessionBackend.php:18
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\initializeConnection
‪initializeConnection()
Definition: RedisSessionBackend.php:244
‪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:294
‪TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException
Definition: SessionNotFoundException.php:23
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\validateConfiguration
‪validateConfiguration()
Definition: RedisSessionBackend.php:86