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