‪TYPO3CMS  11.5
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  throw new SessionNotFoundException('Session could not be fetched from redis', 1481885583);
146  }
147 
155  public function remove(string $sessionId): bool
156  {
157  $this->‪initializeConnection();
158  return $this->redis->del($this->‪getSessionKeyName($this->‪hash($sessionId))) >= 1;
159  }
160 
171  public function set(string $sessionId, array $sessionData): array
172  {
173  $this->‪initializeConnection();
174 
175  $hashedSessionId = $this->‪hash($sessionId);
176  $sessionData['ses_id'] = $hashedSessionId;
177  $sessionData['ses_tstamp'] = ‪$GLOBALS['EXEC_TIME'] ?? time();
178 
179  // nx will not allow overwriting existing keys
180  $jsonString = json_encode($sessionData);
181  $wasSet = is_string($jsonString) && $this->redis->set(
182  $this->‪getSessionKeyName($hashedSessionId),
183  $jsonString,
184  ['nx']
185  );
186 
187  if (!$wasSet) {
188  throw new SessionNotCreatedException('Session could not be written to Redis', 1481895647);
189  }
190 
191  return $sessionData;
192  }
193 
204  public function ‪update(string $sessionId, array $sessionData): array
205  {
206  $hashedSessionId = $this->‪hash($sessionId);
207  try {
208  $sessionData = array_merge($this->get($sessionId), $sessionData);
209  } catch (SessionNotFoundException $e) {
210  throw new SessionNotUpdatedException('Cannot update non-existing record', 1484389971, $e);
211  }
212  $sessionData['ses_id'] = $hashedSessionId;
213  $sessionData['ses_tstamp'] = ‪$GLOBALS['EXEC_TIME'] ?? time();
214 
215  $key = $this->‪getSessionKeyName($hashedSessionId);
216  $jsonString = json_encode($sessionData);
217  $wasSet = is_string($jsonString) && $this->redis->set($key, $jsonString);
218 
219  if (!$wasSet) {
220  throw new SessionNotUpdatedException('Session could not be updated in Redis', 1481896383);
221  }
222 
223  return $sessionData;
224  }
225 
232  public function ‪collectGarbage(int $maximumLifetime, int $maximumAnonymousLifetime = 0)
233  {
234  foreach ($this->‪getAll() as $sessionRecord) {
235  if (!($sessionRecord['ses_userid'] ?? false)) {
236  if ($maximumAnonymousLifetime > 0 && ($sessionRecord['ses_tstamp'] + $maximumAnonymousLifetime) < ‪$GLOBALS['EXEC_TIME']) {
237  $this->redis->del($this->‪getSessionKeyName($sessionRecord['ses_id']));
238  }
239  } else {
240  if (($sessionRecord['ses_tstamp'] + $maximumLifetime) < ‪$GLOBALS['EXEC_TIME']) {
241  $this->redis->del($this->‪getSessionKeyName($sessionRecord['ses_id']));
242  }
243  }
244  }
245  }
246 
252  protected function ‪initializeConnection()
253  {
254  if ($this->connected) {
255  return;
256  }
257 
258  try {
259  $this->connected = $this->redis->pconnect(
260  $this->configuration['hostname'] ?? '127.0.0.1',
261  $this->configuration['port'] ?? 6379,
262  0.0,
263  $this->identifier
264  );
265  } catch (\RedisException $e) {
266  $this->logger->alert('Could not connect to redis server.', ['exception' => $e]);
267  }
268 
269  if (!$this->connected) {
270  throw new \RuntimeException(
271  'Could not connect to redis server at ' . $this->configuration['hostname'] . ':' . $this->configuration['port'],
272  1482242961
273  );
274  }
275 
276  if (isset($this->configuration['password'])
277  && $this->configuration['password'] !== ''
278  && !$this->redis->auth($this->configuration['password'])
279  ) {
280  throw new \RuntimeException(
281  'The given password was not accepted by the redis server.',
282  1481270961
283  );
284  }
285 
286  if (isset($this->configuration['database'])
287  && $this->configuration['database'] > 0
288  && !$this->redis->select($this->configuration['database'])
289  ) {
290  throw new \RuntimeException(
291  'The given database "' . $this->configuration['database'] . '" could not be selected.',
292  1481270987
293  );
294  }
295  }
296 
302  public function ‪getAll(): array
303  {
304  $this->‪initializeConnection();
305 
306  $keys = [];
307  // Initialize our iterator to null, needed by redis->scan
308  $iterator = null;
309  $this->redis->setOption(\Redis::OPT_SCAN, (string)\Redis::SCAN_RETRY);
310  $pattern = $this->‪getSessionKeyName('*');
311  // retry when we get no keys back, redis->scan returns a chunk (array) of keys per iteration
312  while (($keyChunk = $this->redis->scan($iterator, $pattern)) !== false) {
313  foreach ($keyChunk as $key) {
314  $keys[] = $key;
315  }
316  }
317 
318  $encodedSessions = $this->redis->mGet($keys);
319  if (!is_array($encodedSessions)) {
320  return [];
321  }
322 
323  $sessions = [];
324  foreach ($encodedSessions as $session) {
325  if (is_string($session)) {
326  $decodedSession = json_decode($session, true);
327  if ($decodedSession) {
328  $sessions[] = $decodedSession;
329  }
330  }
331  }
332 
333  return $sessions;
334  }
335 
340  protected function ‪getSessionKeyName(string $sessionId): string
341  {
342  return $this->applicationIdentifier . $sessionId;
343  }
344 
348  protected function ‪getSessionTimeout(): int
349  {
350  return (int)(‪$GLOBALS['TYPO3_CONF_VARS'][‪$this->identifier]['sessionTimeout'] ?? 86400);
351  }
352 }
‪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:343
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\getSessionKeyName
‪string getSessionKeyName(string $sessionId)
Definition: RedisSessionBackend.php:335
‪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:199
‪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:227
‪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: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:23
‪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:25
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\initializeConnection
‪initializeConnection()
Definition: RedisSessionBackend.php:247
‪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:297
‪TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException
Definition: SessionNotFoundException.php:23
‪TYPO3\CMS\Core\Session\Backend\RedisSessionBackend\validateConfiguration
‪validateConfiguration()
Definition: RedisSessionBackend.php:87