TYPO3 CMS  TYPO3_8-7
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 
22 
30 {
31 
35  protected $configuration = [];
36 
42  protected $connected = false;
43 
50  protected $applicationIdentifier = '';
51 
57  protected $redis;
58 
62  protected $identifier;
63 
71  public function initialize(string $identifier, array $configuration)
72  {
73  $this->redis = new \Redis();
74 
75  $this->configuration = $configuration;
76  $this->identifier = $identifier;
77  $this->applicationIdentifier = 'typo3_ses_'
78  . $identifier . '_'
79  . sha1($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']) . '_';
80  }
81 
88  public function validateConfiguration()
89  {
90  if (!extension_loaded('redis')) {
91  throw new \RuntimeException(
92  'The PHP extension "redis" must be installed and loaded in order to use the redis session backend.',
93  1481269826
94  );
95  }
96 
97  if (isset($this->configuration['database'])) {
98  if (!is_int($this->configuration['database'])) {
99  throw new \InvalidArgumentException(
100  'The specified database number is of type "' . gettype($this->configuration['database']) .
101  '" but an integer is expected.',
102  1481270871
103  );
104  }
105 
106  if ($this->configuration['database'] < 0) {
107  throw new \InvalidArgumentException(
108  'The specified database "' . $this->configuration['database'] . '" must be greater or equal than zero.',
109  1481270923
110  );
111  }
112  }
113  }
114 
122  public function get(string $sessionId): array
123  {
124  $this->initializeConnection();
125 
126  $key = $this->getSessionKeyName($sessionId);
127  $rawData = $this->redis->get($key);
128 
129  if ($rawData !== false) {
130  return json_decode(
131  $rawData,
132  true
133  );
134  }
135  throw new SessionNotFoundException('Session could not be fetched from redis', 1481885583);
136  }
137 
145  public function remove(string $sessionId): bool
146  {
147  $this->initializeConnection();
148 
149  return $this->redis->del($this->getSessionKeyName($sessionId)) >= 1;
150  }
151 
162  public function set(string $sessionId, array $sessionData): array
163  {
164  $this->initializeConnection();
165  $sessionData['ses_id'] = $sessionId;
166  $sessionData['ses_tstamp'] = $GLOBALS['EXEC_TIME'] ?? time();
167 
168  $key = $this->getSessionKeyName($sessionId);
169 
170  // nx will not allow overwriting existing keys
171  $wasSet = $this->redis->set(
172  $key,
173  json_encode($sessionData),
174  ['nx']
175  );
176 
177  if (!$wasSet) {
178  throw new SessionNotCreatedException('Session could not be written to Redis', 1481895647);
179  }
180 
181  return $sessionData;
182  }
183 
194  public function update(string $sessionId, array $sessionData): array
195  {
196  try {
197  $sessionData = array_merge($this->get($sessionId), $sessionData);
198  } catch (SessionNotFoundException $e) {
199  throw new SessionNotUpdatedException('Cannot update non-existing record', 1484389971, $e);
200  }
201  $sessionData['ses_id'] = $sessionId;
202  $sessionData['ses_tstamp'] = $GLOBALS['EXEC_TIME'] ?? time();
203 
204  $key = $this->getSessionKeyName($sessionId);
205  $wasSet = $this->redis->set($key, json_encode($sessionData));
206 
207  if (!$wasSet) {
208  throw new SessionNotUpdatedException('Session could not be updated in Redis', 1481896383);
209  }
210 
211  return $sessionData;
212  }
213 
220  public function collectGarbage(int $maximumLifetime, int $maximumAnonymousLifetime = 0)
221  {
222  foreach ($this->getAll() as $sessionRecord) {
223  if ($sessionRecord['ses_anonymous']) {
224  if ($maximumAnonymousLifetime > 0 && ($sessionRecord['ses_tstamp'] + $maximumAnonymousLifetime) < $GLOBALS['EXEC_TIME']) {
225  $this->redis->del($this->getSessionKeyName($sessionRecord['ses_id']));
226  }
227  } else {
228  if (($sessionRecord['ses_tstamp'] + $maximumLifetime) < $GLOBALS['EXEC_TIME']) {
229  $this->redis->del($this->getSessionKeyName($sessionRecord['ses_id']));
230  }
231  }
232  }
233  }
234 
240  protected function initializeConnection()
241  {
242  if ($this->connected) {
243  return;
244  }
245 
246  try {
247  $this->connected = $this->redis->pconnect(
248  $this->configuration['hostname'] ?? '127.0.0.1',
249  $this->configuration['port'] ?? 6379,
250  0.0,
251  $this->identifier
252  );
253  } catch (\RedisException $e) {
254  GeneralUtility::sysLog(
255  'Could not connect to redis server.',
256  'core',
258  );
259  }
260 
261  if (!$this->connected) {
262  throw new \RuntimeException(
263  'Could not connect to redis server at ' . $this->configuration['hostname'] . ':' . $this->configuration['port'],
264  1482242961
265  );
266  }
267 
268  if (isset($this->configuration['password'])
269  && $this->configuration['password'] !== ''
270  && !$this->redis->auth($this->configuration['password'])
271  ) {
272  throw new \RuntimeException(
273  'The given password was not accepted by the redis server.',
274  1481270961
275  );
276  }
277 
278  if (isset($this->configuration['database'])
279  && $this->configuration['database'] > 0
280  && !$this->redis->select($this->configuration['database'])
281  ) {
282  throw new \RuntimeException(
283  'The given database "' . $this->configuration['database'] . '" could not be selected.',
284  1481270987
285  );
286  }
287  }
288 
294  public function getAll(): array
295  {
296  $this->initializeConnection();
297 
298  $keys = [];
299  // Initialize our iterator to null, needed by redis->scan
300  $iterator = null;
301  $this->redis->setOption(\Redis::OPT_SCAN, (string)\Redis::SCAN_RETRY);
302  $pattern = $this->getSessionKeyName('*');
303  // retry when we get no keys back, redis->scan returns a chunk (array) of keys per iteration
304  while (($keyChunk = $this->redis->scan($iterator, $pattern)) !== false) {
305  foreach ($keyChunk as $key) {
306  $keys[] = $key;
307  }
308  }
309 
310  $encodedSessions = $this->redis->mGet($keys);
311  if (!is_array($encodedSessions)) {
312  return [];
313  }
314 
315  $sessions = [];
316  foreach ($encodedSessions as $session) {
317  if (is_string($session)) {
318  $decodedSession = json_decode($session, true);
319  if ($decodedSession) {
320  $sessions[] = $decodedSession;
321  }
322  }
323  }
324 
325  return $sessions;
326  }
327 
332  protected function getSessionKeyName(string $sessionId): string
333  {
334  return $this->applicationIdentifier . $sessionId;
335  }
336 
340  protected function getSessionTimeout(): int
341  {
342  return (int)($GLOBALS['TYPO3_CONF_VARS'][$this->identifier]['sessionTimeout'] ?? 86400);
343  }
344 }
collectGarbage(int $maximumLifetime, int $maximumAnonymousLifetime=0)
initialize(string $identifier, array $configuration)
update(string $sessionId, array $sessionData)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']