‪TYPO3CMS  10.4
SessionService.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use Doctrine\DBAL\FetchMode;
19 use Symfony\Component\HttpFoundation\Cookie;
35 
41 class SessionService implements SingletonInterface
42 {
43  use BlockSerializationTrait;
44  use CookieHeaderTrait;
45 
51  private $cookieName = 'Typo3InstallTool';
52 
58  private $expireTimeInMinutes = 15;
59 
65  private $regenerateSessionIdTime = 5;
66 
72  public function __construct()
73  {
74  // Register our "save" session handler
75  $sessionHandler = GeneralUtility::makeInstance(
76  FileSessionHandler::class,
77  ‪Environment::getVarPath() . '/session',
78  $this->expireTimeInMinutes
79  );
80  session_set_save_handler($sessionHandler);
81  session_name($this->cookieName);
82  ini_set('session.cookie_secure', GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'On' : 'Off');
83  ini_set('session.cookie_httponly', 'On');
84  if ($this->hasSameSiteCookieSupport()) {
85  ini_set('session.cookie_samesite', Cookie::SAMESITE_STRICT);
86  }
87  ini_set('session.cookie_path', (string)GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
88  // Always call the garbage collector to clean up stale session files
89  ini_set('session.gc_probability', (string)100);
90  ini_set('session.gc_divisor', (string)100);
91  ini_set('session.gc_maxlifetime', (string)($this->expireTimeInMinutes * 2 * 60));
92  if ($this->isSessionAutoStartEnabled()) {
93  $sessionCreationError = 'Error: session.auto-start is enabled.<br />';
94  $sessionCreationError .= 'The PHP option session.auto-start is enabled. Disable this option in php.ini or .htaccess:<br />';
95  $sessionCreationError .= '<pre>php_value session.auto_start Off</pre>';
96  throw new Exception($sessionCreationError, 1294587485);
97  }
98  if (session_status() === PHP_SESSION_ACTIVE) {
99  $sessionCreationError = 'Session already started by session_start().<br />';
100  $sessionCreationError .= 'Make sure no installed extension is starting a session in its ext_localconf.php or ext_tables.php.';
101  throw new Exception($sessionCreationError, 1294587486);
102  }
103  }
104 
105  public function initializeSession()
106  {
107  if (session_status() === PHP_SESSION_ACTIVE) {
108  return;
109  }
110  session_start();
111  if (!$this->hasSameSiteCookieSupport()) {
112  $this->resendCookieHeader();
113  }
114  }
115 
121  public function startSession()
122  {
123  $this->initializeSession();
124  // check if session is already active
125  if ($_SESSION['active'] ?? false) {
126  return session_id();
127  }
128  $_SESSION['active'] = true;
129  // Be sure to use our own session id, so create a new one
130  return $this->renewSession();
131  }
132 
136  public function destroySession()
137  {
138  if ($this->hasSessionCookie()) {
139  $this->initializeSession();
140  $_SESSION = [];
141  $params = session_get_cookie_params();
142  $cookie = Cookie::create(
143  ($sessionName = session_name()) !== false ? $sessionName : $this->cookieName,
144  0,
145  0,
146  $params['path'],
147  $params['domain'],
148  $params['samesite'] === Cookie::SAMESITE_NONE || GeneralUtility::getIndpEnv('TYPO3_SSL'),
149  $params['httponly'],
150  false,
151  $params['samesite']
152  );
153 
154  header('Set-Cookie: ' . $cookie);
155  session_destroy();
156  }
157  }
158 
162  public function resetSession()
163  {
164  $this->initializeSession();
165  $_SESSION = [];
166  $_SESSION['active'] = false;
167  }
168 
174  private function renewSession()
175  {
176  // we do not have parallel ajax requests so we can safely remove the old session data
177  session_regenerate_id(true);
178  if (!$this->hasSameSiteCookieSupport()) {
179  $this->resendCookieHeader([$this->cookieName]);
180  }
181  return session_id();
182  }
183 
189  public function hasSessionCookie(): bool
190  {
191  return isset($_COOKIE[$this->cookieName]);
192  }
193 
200  public function setAuthorized()
201  {
202  $_SESSION['authorized'] = true;
203  $_SESSION['lastSessionId'] = time();
204  $_SESSION['tstamp'] = time();
205  $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
206  // Renew the session id to avoid session fixation
207  $this->renewSession();
208  }
209 
216  public function setAuthorizedBackendSession(BackendUserAuthentication $backendUser)
217  {
218  $nonce = bin2hex(random_bytes(20));
219  $sessionBackend = $this->getBackendUserSessionBackend();
220  // use hash mechanism of session backend, or pass plain value through generic hmac
221  $sessionHmac = $sessionBackend instanceof HashableSessionBackendInterface
222  ? $sessionBackend->hash($backendUser->id)
223  : hash_hmac('sha256', $backendUser->id, $nonce);
224 
225  $_SESSION['authorized'] = true;
226  $_SESSION['lastSessionId'] = time();
227  $_SESSION['tstamp'] = time();
228  $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
229  $_SESSION['isBackendSession'] = true;
230  $_SESSION['backendUserSession'] = [
231  'nonce' => $nonce,
232  'userId' => (int)$backendUser->user['uid'],
233  'hmac' => $sessionHmac,
234  ];
235  // Renew the session id to avoid session fixation
236  $this->renewSession();
237  }
238 
244  public function isAuthorized()
245  {
246  if (!$this->hasSessionCookie()) {
247  return false;
248  }
249  $this->initializeSession();
250  if (empty($_SESSION['authorized'])) {
251  return false;
252  }
253  return !$this->isExpired();
254  }
255 
261  public function isAuthorizedBackendUserSession(): bool
262  {
263  if (!$this->hasSessionCookie()) {
264  return false;
265  }
266  $this->initializeSession();
267  if (empty($_SESSION['authorized']) || empty($_SESSION['isBackendSession'])) {
268  return false;
269  }
270  return !$this->isExpired();
271  }
272 
279  public function hasActiveBackendUserRoleAndSession(): bool
280  {
281  // @see \TYPO3\CMS\Install\Controller\BackendModuleController::setAuthorizedAndRedirect()
282  $backendUserSession = $this->getBackendUserSession();
283  $backendUserRecord = $this->getBackendUserRecord($backendUserSession['userId']);
284  if ($backendUserRecord === null || empty($backendUserRecord['uid'])) {
285  return false;
286  }
287  $isAdmin = (($backendUserRecord['admin'] ?? 0) & 1) === 1;
288  $systemMaintainers = array_map('intval', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
289  // in case no system maintainers are configured, all admin users are considered to be system maintainers
290  $isSystemMaintainer = empty($systemMaintainers) || in_array((int)$backendUserRecord['uid'], $systemMaintainers, true);
291  // in development context, all admin users are considered to be system maintainers
292  $hasDevelopmentContext = ‪Environment::getContext()->‪isDevelopment();
293  // stop here, in case the current admin tool session does not belong to a backend user having admin & maintainer privileges
294  if (!$isAdmin || !$hasDevelopmentContext && !$isSystemMaintainer) {
295  return false;
296  }
297 
298  $sessionBackend = $this->getBackendUserSessionBackend();
299  foreach ($sessionBackend->getAll() as $sessionRecord) {
300  $sessionUserId = (int)($sessionRecord['ses_userid'] ?? 0);
301  // skip, in case backend user id does not match
302  if ($backendUserSession['userId'] !== $sessionUserId) {
303  continue;
304  }
305  $sessionId = (string)($sessionRecord['ses_id'] ?? '');
306  // use persisted hashed `ses_id` directly, or pass through hmac for plain values
307  $sessionHmac = $sessionBackend instanceof HashableSessionBackendInterface
308  ? $sessionId
309  : hash_hmac('sha256', $sessionId, $backendUserSession['nonce']);
310  // skip, in case backend user session id does not match
311  if ($backendUserSession['hmac'] !== $sessionHmac) {
312  continue;
313  }
314  // backend user id and session id matched correctly
315  return true;
316  }
317  return false;
318  }
319 
327  public function isExpired()
328  {
329  if (!$this->hasSessionCookie()) {
330  // Session never existed, means it is not "expired"
331  return false;
332  }
333  $this->initializeSession();
334  if (empty($_SESSION['authorized'])) {
335  // Session never authorized, means it is not "expired"
336  return false;
337  }
338  return $_SESSION['expires'] <= time();
339  }
340 
346  public function refreshSession()
347  {
348  $_SESSION['tstamp'] = time();
349  $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
350  if (time() > $_SESSION['lastSessionId'] + $this->regenerateSessionIdTime * 60) {
351  // Renew our session ID
352  $_SESSION['lastSessionId'] = time();
353  $this->renewSession();
354  }
355  }
356 
362  public function addMessage(FlashMessage $message)
363  {
364  if (!is_array($_SESSION['messages'])) {
365  $_SESSION['messages'] = [];
366  }
367  $_SESSION['messages'][] = $message;
368  }
369 
375  public function getMessagesAndFlush()
376  {
377  $messages = [];
378  if (is_array($_SESSION['messages'])) {
379  $messages = $_SESSION['messages'];
380  }
381  $_SESSION['messages'] = [];
382  return $messages;
383  }
384 
388  public function getBackendUserSession(): array
389  {
390  if (empty($_SESSION['backendUserSession'])) {
391  throw new Exception(
392  'The backend user session is only available if invoked via the backend user interface.',
393  1624879295
394  );
395  }
396  return $_SESSION['backendUserSession'];
397  }
398 
404  protected function isSessionAutoStartEnabled()
405  {
406  return $this->getIniValueBoolean('session.auto_start');
407  }
408 
415  protected function getIniValueBoolean($configOption)
416  {
417  return filter_var(
418  ini_get($configOption),
419  FILTER_VALIDATE_BOOLEAN,
420  [FILTER_REQUIRE_SCALAR, FILTER_NULL_ON_FAILURE]
421  );
422  }
423 
431  protected function getBackendUserRecord(int $uid): ?array
432  {
433  $restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class);
434  $restrictionContainer->add(GeneralUtility::makeInstance(RootLevelRestriction::class, ['be_users']));
435 
436  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
437  $queryBuilder->setRestrictions($restrictionContainer);
438  $queryBuilder->select('uid', 'admin')
439  ->from('be_users')
440  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)));
441 
442  $resetBeUsersTca = false;
443  if (!isset(‪$GLOBALS['TCA']['be_users'])) {
444  // The admin tool intentionally does not load any TCA information at this time.
445  // The database restictions, needs the enablecolumns TCA information
446  // for 'be_users' to load the user correctly.
447  // That is why this part of the TCA ($GLOBALS['TCA']['be_users']['ctrl']['enablecolumns'])
448  // is simulated.
449  // The simulation state will be removed later to avoid unexpected side effects.
450  ‪$GLOBALS['TCA']['be_users']['ctrl']['enablecolumns'] = [
451  'rootLevel' => 1,
452  'deleted' => 'deleted',
453  'disabled' => 'disable',
454  'starttime' => 'starttime',
455  'endtime' => 'endtime',
456  ];
457  $resetBeUsersTca = true;
458  }
459  $result = $queryBuilder->execute()->fetch(FetchMode::ASSOCIATIVE);
460  if ($resetBeUsersTca) {
461  unset(‪$GLOBALS['TCA']['be_users']);
462  }
463 
464  return is_array($result) ? $result : null;
465  }
466 
467  protected function getBackendUserSessionBackend(): SessionBackendInterface
468  {
469  return GeneralUtility::makeInstance(SessionManager::class)->getSessionBackend('BE');
470  }
471 }
‪TYPO3\CMS\Install\Service\Session\FileSessionHandler
Definition: FileSessionHandler.php:29
‪TYPO3\CMS\Core\Session\SessionManager
Definition: SessionManager.php:39
‪TYPO3\CMS\Core\Session\Backend\HashableSessionBackendInterface
Definition: HashableSessionBackendInterface.php:21
‪TYPO3\CMS\Core\Core\Environment\getContext
‪static ApplicationContext getContext()
Definition: Environment.php:133
‪TYPO3\CMS\Core\Security\BlockSerializationTrait
Definition: BlockSerializationTrait.php:28
‪TYPO3\CMS\Install\Exception
Definition: Exception.php:24
‪TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction
Definition: RootLevelRestriction.php:27
‪TYPO3\CMS\Core\Session\Backend\SessionBackendInterface
Definition: SessionBackendInterface.php:28
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Core\ApplicationContext\isDevelopment
‪bool isDevelopment()
Definition: ApplicationContext.php:96
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:24
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:23
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:40
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Install\Service
Definition: ClearCacheService.php:16
‪TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer
Definition: DefaultRestrictionContainer.php:24
‪TYPO3\CMS\Core\Core\Environment\getVarPath
‪static string getVarPath()
Definition: Environment.php:192