[TASK] Deprecate PhpOptionsUtility
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Service / SessionService.php
1 <?php
2 namespace TYPO3\CMS\Install\Service;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Core\Environment;
18 use TYPO3\CMS\Core\Messaging\FlashMessage;
19 use TYPO3\CMS\Core\SingletonInterface;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21
22 /**
23 * Secure session handling for the install tool.
24 */
25 class SessionService implements SingletonInterface
26 {
27 /**
28 * The path to our var/ folder (where we can write our sessions). Set in the
29 * constructor.
30 *
31 * @var string
32 */
33 private $basePath;
34
35 /**
36 * Path where to store our session files in var/session/. %s will be
37 * non-guessable.
38 *
39 * @var string
40 */
41 private $sessionPath = 'session/%s';
42
43 /**
44 * the cookie to store the session ID of the install tool
45 *
46 * @var string
47 */
48 private $cookieName = 'Typo3InstallTool';
49
50 /**
51 * time (minutes) to expire an unused session
52 *
53 * @var int
54 */
55 private $expireTimeInMinutes = 60;
56
57 /**
58 * time (minutes) to generate a new session id for our current session
59 *
60 * @var int
61 */
62 private $regenerateSessionIdTime = 5;
63
64 /**
65 * Constructor. Starts PHP session handling in our own private store
66 *
67 * Side-effect: might set a cookie, so must be called before any other output.
68 */
69 public function __construct()
70 {
71 $this->basePath = Environment::getVarPath() . '/';
72 // Start our PHP session early so that hasSession() works
73 $sessionSavePath = $this->getSessionSavePath();
74 // Register our "save" session handler
75 session_set_save_handler([$this, 'open'], [$this, 'close'], [$this, 'read'], [$this, 'write'], [$this, 'destroy'], [$this, 'gc']);
76 session_save_path($sessionSavePath);
77 session_name($this->cookieName);
78 ini_set('session.cookie_path', (string)GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
79 // Always call the garbage collector to clean up stale session files
80 ini_set('session.gc_probability', (string)100);
81 ini_set('session.gc_divisor', (string)100);
82 ini_set('session.gc_maxlifetime', (string)$this->expireTimeInMinutes * 2 * 60);
83 if ($this->isSessionAutoStartEnabled()) {
84 $sessionCreationError = 'Error: session.auto-start is enabled.<br />';
85 $sessionCreationError .= 'The PHP option session.auto-start is enabled. Disable this option in php.ini or .htaccess:<br />';
86 $sessionCreationError .= '<pre>php_value session.auto_start Off</pre>';
87 throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587485);
88 }
89 if (defined('SID')) {
90 $sessionCreationError = 'Session already started by session_start().<br />';
91 $sessionCreationError .= 'Make sure no installed extension is starting a session in its ext_localconf.php or ext_tables.php.';
92 throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587486);
93 }
94 session_start();
95 }
96
97 /**
98 * Returns the path where to store our session files
99 *
100 * @throws \TYPO3\CMS\Install\Exception
101 * @return string Session save path
102 */
103 private function getSessionSavePath()
104 {
105 if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
106 throw new \TYPO3\CMS\Install\Exception(
107 'No encryption key set to secure session',
108 1371243449
109 );
110 }
111 $sessionSavePath = sprintf(
112 $this->basePath . $this->sessionPath,
113 GeneralUtility::hmac('session:' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])
114 );
115 $this->ensureSessionSavePathExists($sessionSavePath);
116 return $sessionSavePath;
117 }
118
119 /**
120 * Create directories for the session save path
121 * and throw an exception if that fails.
122 *
123 * @param string $sessionSavePath The absolute path to the session files
124 * @throws \TYPO3\CMS\Install\Exception
125 */
126 private function ensureSessionSavePathExists($sessionSavePath)
127 {
128 if (!is_dir($sessionSavePath)) {
129 try {
130 GeneralUtility::mkdir_deep($sessionSavePath);
131 } catch (\RuntimeException $exception) {
132 throw new \TYPO3\CMS\Install\Exception(
133 'Could not create session folder in ' . Environment::getVarPath() . '. Make sure it is writeable!',
134 1294587484
135 );
136 }
137 $htaccessContent = '
138 # Apache < 2.3
139 <IfModule !mod_authz_core.c>
140 Order allow,deny
141 Deny from all
142 Satisfy All
143 </IfModule>
144
145 # Apache ≥ 2.3
146 <IfModule mod_authz_core.c>
147 Require all denied
148 </IfModule>
149 ';
150 GeneralUtility::writeFile($sessionSavePath . '/.htaccess', $htaccessContent);
151 $indexContent = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">';
152 $indexContent .= '<HTML><HEAD<TITLE></TITLE><META http-equiv=Refresh Content="0; Url=../../">';
153 $indexContent .= '</HEAD></HTML>';
154 GeneralUtility::writeFile($sessionSavePath . '/index.html', $indexContent);
155 }
156 }
157
158 /**
159 * Starts a new session
160 *
161 * @return string The session ID
162 */
163 public function startSession()
164 {
165 $_SESSION['active'] = true;
166 // Be sure to use our own session id, so create a new one
167 return $this->renewSession();
168 }
169
170 /**
171 * Destroys a session
172 */
173 public function destroySession()
174 {
175 session_destroy();
176 }
177
178 /**
179 * Reset session. Sets _SESSION to empty array.
180 */
181 public function resetSession()
182 {
183 $_SESSION = [];
184 $_SESSION['active'] = false;
185 }
186
187 /**
188 * Generates a new session ID and sends it to the client.
189 *
190 * @return string the new session ID
191 */
192 private function renewSession()
193 {
194 session_regenerate_id();
195 return session_id();
196 }
197
198 /**
199 * Checks whether we already have an active session.
200 *
201 * @return bool TRUE if there is an active session, FALSE otherwise
202 */
203 public function hasSession()
204 {
205 return $_SESSION['active'] === true;
206 }
207
208 /**
209 * Returns the session ID of the running session.
210 *
211 * @return string the session ID
212 */
213 public function getSessionId()
214 {
215 return session_id();
216 }
217
218 /**
219 * Returns a session hash, which can only be calculated by the server.
220 * Used to store our session files without exposing the session ID.
221 *
222 * @param string $sessionId An alternative session ID. Defaults to our current session ID
223 * @throws \TYPO3\CMS\Install\Exception
224 * @return string the session hash
225 */
226 private function getSessionHash($sessionId = '')
227 {
228 if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
229 throw new \TYPO3\CMS\Install\Exception(
230 'No encryption key set to secure session',
231 1371243450
232 );
233 }
234 if (!$sessionId) {
235 $sessionId = $this->getSessionId();
236 }
237 return md5($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . '|' . $sessionId);
238 }
239
240 /**
241 * Marks this session as an "authorized" one (login successful).
242 * Should only be called if:
243 * a) we have a valid session running
244 * b) the "password" or some other authorization mechanism really matched
245 */
246 public function setAuthorized()
247 {
248 $_SESSION['authorized'] = true;
249 $_SESSION['lastSessionId'] = time();
250 $_SESSION['tstamp'] = time();
251 $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
252 // Renew the session id to avoid session fixation
253 $this->renewSession();
254 }
255
256 /**
257 * Marks this session as an "authorized by backend user" one.
258 * This is called by BackendModuleController from backend context.
259 */
260 public function setAuthorizedBackendSession()
261 {
262 $_SESSION['authorized'] = true;
263 $_SESSION['lastSessionId'] = time();
264 $_SESSION['tstamp'] = time();
265 $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
266 $_SESSION['isBackendSession'] = true;
267 // Renew the session id to avoid session fixation
268 $this->renewSession();
269 }
270
271 /**
272 * Check if we have an already authorized session
273 *
274 * @return bool TRUE if this session has been authorized before (by a correct password)
275 */
276 public function isAuthorized()
277 {
278 if (!$_SESSION['authorized']) {
279 return false;
280 }
281 if ($_SESSION['expires'] < time()) {
282 // This session has already expired
283 return false;
284 }
285 return true;
286 }
287
288 /**
289 * Check if we have an authorized session from a system maintainer
290 *
291 * @return bool TRUE if this session has been authorized before and initialized by a backend system maintainer
292 */
293 public function isAuthorizedBackendUserSession()
294 {
295 if (!$_SESSION['authorized'] || !$_SESSION['isBackendSession']) {
296 return false;
297 }
298 if ($_SESSION['expires'] < time()) {
299 // This session has already expired
300 return false;
301 }
302 return true;
303 }
304
305 /**
306 * Check if our session is expired.
307 * Useful only right after a FALSE "isAuthorized" to see if this is the
308 * reason for not being authorized anymore.
309 *
310 * @return bool TRUE if an authorized session exists, but is expired
311 */
312 public function isExpired()
313 {
314 if (!$_SESSION['authorized']) {
315 // Session never existed, means it is not "expired"
316 return false;
317 }
318 if ($_SESSION['expires'] < time()) {
319 // This session was authorized before, but has expired
320 return true;
321 }
322 return false;
323 }
324
325 /**
326 * Refreshes our session information, rising the expire time.
327 * Also generates a new session ID every 5 minutes to minimize the risk of
328 * session hijacking.
329 */
330 public function refreshSession()
331 {
332 $_SESSION['tstamp'] = time();
333 $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
334 if (time() > $_SESSION['lastSessionId'] + $this->regenerateSessionIdTime * 60) {
335 // Renew our session ID
336 $_SESSION['lastSessionId'] = time();
337 $this->renewSession();
338 }
339 }
340
341 /**
342 * Add a message to "Flash" message storage.
343 *
344 * @param FlashMessage $message A message to add
345 */
346 public function addMessage(FlashMessage $message)
347 {
348 if (!is_array($_SESSION['messages'])) {
349 $_SESSION['messages'] = [];
350 }
351 $_SESSION['messages'][] = $message;
352 }
353
354 /**
355 * Return stored session messages and flush.
356 *
357 * @return FlashMessage[] Messages
358 */
359 public function getMessagesAndFlush()
360 {
361 $messages = [];
362 if (is_array($_SESSION['messages'])) {
363 $messages = $_SESSION['messages'];
364 }
365 $_SESSION['messages'] = [];
366 return $messages;
367 }
368
369 /*************************
370 *
371 * PHP session handling with "secure" session files (hashed session id)
372 * see http://www.php.net/manual/en/function.session-set-save-handler.php
373 *
374 *************************/
375 /**
376 * Returns the file where to store our session data
377 *
378 * @param string $id
379 * @return string A filename
380 */
381 private function getSessionFile($id)
382 {
383 $sessionSavePath = $this->getSessionSavePath();
384 return $sessionSavePath . '/hash_' . $this->getSessionHash($id);
385 }
386
387 /**
388 * Open function. See @session_set_save_handler
389 *
390 * @param string $savePath
391 * @param string $sessionName
392 * @return bool
393 */
394 public function open($savePath, $sessionName)
395 {
396 return true;
397 }
398
399 /**
400 * Close function. See @session_set_save_handler
401 *
402 * @return bool
403 */
404 public function close()
405 {
406 return true;
407 }
408
409 /**
410 * Read session data. See @session_set_save_handler
411 *
412 * @param string $id The session id
413 * @return string
414 */
415 public function read($id)
416 {
417 $sessionFile = $this->getSessionFile($id);
418 $content = '';
419 if (file_exists($sessionFile)) {
420 if ($fd = fopen($sessionFile, 'rb')) {
421 $lockres = flock($fd, LOCK_SH);
422 if ($lockres) {
423 $length = filesize($sessionFile);
424 if ($length > 0) {
425 $content = fread($fd, $length);
426 }
427 flock($fd, LOCK_UN);
428 }
429 fclose($fd);
430 }
431 }
432 // Do a "test write" of the session file after opening it. The real session data is written in
433 // __destruct() and we can not create a sane error message there anymore, so this test should fail
434 // before if final session file can not be written due to permission problems.
435 $this->write($id, $content);
436 return $content;
437 }
438
439 /**
440 * Write session data. See @session_set_save_handler
441 *
442 * @param string $id The session id
443 * @param string $sessionData The data to be stored
444 * @throws Exception
445 * @return bool
446 */
447 public function write($id, $sessionData)
448 {
449 $sessionFile = $this->getSessionFile($id);
450 $result = false;
451 $changePermissions = !@is_file($sessionFile);
452 if ($fd = fopen($sessionFile, 'cb')) {
453 if (flock($fd, LOCK_EX)) {
454 ftruncate($fd, 0);
455 $res = fwrite($fd, $sessionData);
456 if ($res !== false) {
457 fflush($fd);
458 $result = true;
459 }
460 flock($fd, LOCK_UN);
461 }
462 fclose($fd);
463 // Change the permissions only if the file has just been created
464 if ($changePermissions) {
465 GeneralUtility::fixPermissions($sessionFile);
466 }
467 }
468 if (!$result) {
469 throw new Exception(
470 'Session file not writable. Please check permission on ' .
471 Environment::getVarPath() . '/session and its subdirectories.',
472 1424355157
473 );
474 }
475 return $result;
476 }
477
478 /**
479 * Destroys one session. See @session_set_save_handler
480 *
481 * @param string $id The session id
482 * @return string
483 */
484 public function destroy($id)
485 {
486 $sessionFile = $this->getSessionFile($id);
487 return @unlink($sessionFile);
488 }
489
490 /**
491 * Garbage collect session info. See @session_set_save_handler
492 *
493 * @param int $maxLifeTime The setting of session.gc_maxlifetime
494 * @return bool
495 */
496 public function gc($maxLifeTime)
497 {
498 $sessionSavePath = $this->getSessionSavePath();
499 $files = glob($sessionSavePath . '/hash_*');
500 if (!is_array($files)) {
501 return true;
502 }
503 foreach ($files as $filename) {
504 if (filemtime($filename) + $this->expireTimeInMinutes * 60 < time()) {
505 @unlink($filename);
506 }
507 }
508 return true;
509 }
510
511 /**
512 * Writes the session data at the end, to overcome a PHP APC bug.
513 *
514 * Writes the session data in a proper context that is not affected by the APC bug:
515 * http://pecl.php.net/bugs/bug.php?id=16721.
516 *
517 * This behaviour was introduced in #17511, where self::write() made use of GeneralUtility
518 * which due to the APC bug throws a "Fatal error: Class 'GeneralUtility' not found"
519 * (and the session data is not saved). Calling session_write_close() at this point
520 * seems to be the most easy solution, according to PHP author.
521 */
522 public function __destruct()
523 {
524 session_write_close();
525 }
526
527 /**
528 * Check if php session.auto_start is enabled
529 *
530 * @return bool TRUE if session.auto_start is enabled, FALSE if disabled
531 */
532 protected function isSessionAutoStartEnabled()
533 {
534 return $this->getIniValueBoolean('session.auto_start');
535 }
536
537 /**
538 * Cast an on/off php ini value to boolean
539 *
540 * @param string $configOption
541 * @return bool TRUE if the given option is enabled, FALSE if disabled
542 */
543 protected function getIniValueBoolean($configOption)
544 {
545 return filter_var(ini_get($configOption), FILTER_VALIDATE_BOOLEAN, [FILTER_REQUIRE_SCALAR, FILTER_NULL_ON_FAILURE]);
546 }
547 }