redis = new \Redis(); $this->configuration = $configuration; $this->identifier = $identifier; $this->applicationIdentifier = ($configuration['keyPrefix'] ?? '') . 'typo3_ses_' . $identifier . '_' . sha1($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']) . '_'; } /** * Checks if the configuration is valid * * @throws \InvalidArgumentException * @internal To be used only by SessionManager */ public function validateConfiguration(): void { if (!extension_loaded('redis')) { throw new \RuntimeException( 'The PHP extension "redis" must be installed and loaded in order to use the redis session backend.', 1481269826 ); } if (isset($this->configuration['database'])) { if (!is_int($this->configuration['database'])) { throw new \InvalidArgumentException( 'The specified database number is of type "' . gettype($this->configuration['database']) . '" but an integer is expected.', 1481270871 ); } if ($this->configuration['database'] < 0) { throw new \InvalidArgumentException( 'The specified database "' . $this->configuration['database'] . '" must be greater or equal than zero.', 1481270923 ); } } if (!is_string($this->configuration['password'] ?? '')) { throw new \InvalidArgumentException( 'The specified password must be a string. To authenticate with a username and password' . ' tuple, use the separate "username" and "password" options.', 1780850765 ); } } public function hash(string $sessionId): string { return GeneralUtility::makeInstance(HashService::class) ->hmac($sessionId, 'core-session-backend', HashAlgo::SHA3_256); } /** * Read session data * * @return array Returns the session data * @throws SessionNotFoundException */ public function get(string $sessionId): array { $this->initializeConnection(); $hashedSessionId = $this->hash($sessionId); $rawData = $this->redis->get($this->getSessionKeyName($hashedSessionId)); if ($rawData !== false) { $decodedValue = json_decode($rawData, true); if (is_array($decodedValue)) { return $decodedValue; } } throw new SessionNotFoundException('Session could not be fetched from redis', 1481885583); } /** * Delete a session record */ public function remove(string $sessionId): bool { $this->initializeConnection(); $deleteResult = $this->redis->del($this->getSessionKeyName($this->hash($sessionId))); // Redis delete result is either `int`, `false` or a `\Redis` multi mode object, where delete state cannot get // determined. Multi mode is not even supported by this session backend at all, therefore we handle this case as // "not successful". return is_int($deleteResult) && $deleteResult >= 1; } /** * Write session data. This method prevents overriding existing session data. * ses_id will always be set to $sessionId and overwritten if existing in $sessionData * This method updates ses_tstamp automatically * * @return array The newly created session record. * @throws SessionNotCreatedException */ public function set(string $sessionId, array $sessionData): array { $this->initializeConnection(); $hashedSessionId = $this->hash($sessionId); $sessionData['ses_id'] = $hashedSessionId; $sessionData['ses_tstamp'] = $GLOBALS['EXEC_TIME'] ?? time(); // nx will not allow overwriting existing keys $jsonString = json_encode($sessionData); $wasSet = is_string($jsonString) && $this->redis->set( $this->getSessionKeyName($hashedSessionId), $jsonString, ['nx'] ); if (!$wasSet) { throw new SessionNotCreatedException('Session could not be written to Redis', 1481895647); } return $sessionData; } /** * Updates the session data. * ses_id will always be set to $sessionId and overwritten if existing in $sessionData * This method updates ses_tstamp automatically * * @param array $sessionData The session data to update. Data may be partial. * @return array $sessionData The newly updated session record. * @throws SessionNotUpdatedException */ public function update(string $sessionId, array $sessionData): array { $hashedSessionId = $this->hash($sessionId); try { $sessionData = array_merge($this->get($sessionId), $sessionData); } catch (SessionNotFoundException $e) { throw new SessionNotUpdatedException('Cannot update non-existing record', 1484389971, $e); } $sessionData['ses_id'] = $hashedSessionId; $sessionData['ses_tstamp'] = $GLOBALS['EXEC_TIME'] ?? time(); $key = $this->getSessionKeyName($hashedSessionId); $jsonString = json_encode($sessionData); $wasSet = is_string($jsonString) && $this->redis->set($key, $jsonString); if (!$wasSet) { throw new SessionNotUpdatedException('Session could not be updated in Redis', 1481896383); } return $sessionData; } /** * Garbage Collection * * @param int $maximumLifetime maximum lifetime of authenticated user sessions, in seconds. * @param int $maximumAnonymousLifetime maximum lifetime of non-authenticated user sessions, in seconds. If set to 0, nothing is collected. */ public function collectGarbage(int $maximumLifetime, int $maximumAnonymousLifetime = 0): void { foreach ($this->getAll() as $sessionRecord) { if (!($sessionRecord['ses_userid'] ?? false)) { if ($maximumAnonymousLifetime > 0 && ($sessionRecord['ses_tstamp'] + $maximumAnonymousLifetime) < $GLOBALS['EXEC_TIME']) { $this->redis->del($this->getSessionKeyName($sessionRecord['ses_id'])); } } elseif (($sessionRecord['ses_tstamp'] + $maximumLifetime) < $GLOBALS['EXEC_TIME']) { $this->redis->del($this->getSessionKeyName($sessionRecord['ses_id'])); } } } /** * Initializes the redis backend * * @throws \RuntimeException if access to redis with password is denied or if database selection fails */ protected function initializeConnection(): void { if ($this->connected) { return; } try { $this->connected = $this->redis->pconnect( $this->configuration['hostname'] ?? '127.0.0.1', $this->configuration['port'] ?? 6379, 0.0, $this->identifier ); } catch (\RedisException $e) { $this->logger->alert('Could not connect to redis server.', ['exception' => $e]); } if (!$this->connected) { throw new \RuntimeException( 'Could not connect to redis server at ' . $this->configuration['hostname'] . ':' . $this->configuration['port'], 1482242961 ); } if ($this->getAuthentication() !== null && !$this->redis->auth($this->getAuthentication()) ) { throw new \RuntimeException( 'Authentication to Redis failed”.', 1481270961 ); } if (isset($this->configuration['database']) && $this->configuration['database'] >= 0 && !$this->redis->select($this->configuration['database']) ) { throw new \RuntimeException( 'The given database "' . $this->configuration['database'] . '" could not be selected.', 1481270987 ); } } protected function getAuthentication(): array|string|null { $username = $this->configuration['username'] ?? null; $password = $this->configuration['password'] ?? null; return match (true) { // Username and password configured for authentication, build associative array // out of possible and supported array variants by `php-redis::auth()`. ($username !== null && $password !== null) => [ 'user' => $username, 'pass' => $password, ], // Password-only authentication configured. ($username === null && $password !== null) => $password, // No authentication configured. default => null, }; } /** * List all sessions * * @return array Return a list of all user sessions. The list may be empty. */ public function getAll(): array { $this->initializeConnection(); $keys = []; // Initialize our iterator to null, needed by redis->scan $iterator = null; $this->redis->setOption(\Redis::OPT_SCAN, (string)\Redis::SCAN_RETRY); $pattern = $this->getSessionKeyName('*'); // retry when we get no keys back, redis->scan returns a chunk (array) of keys per iteration while (($keyChunk = $this->redis->scan($iterator, $pattern)) !== false) { foreach ($keyChunk as $key) { $keys[] = $key; } } $encodedSessions = $this->redis->mGet($keys); if (!is_array($encodedSessions)) { return []; } $sessions = []; foreach ($encodedSessions as $session) { if (is_string($session)) { $decodedSession = json_decode($session, true); if ($decodedSession) { $sessions[] = $decodedSession; } } } return $sessions; } protected function getSessionKeyName(string $sessionId): string { return $this->applicationIdentifier . $sessionId; } }