[BUGFIX] Installtool blocked when session file is empty
[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 $length = filesize($sessionFile);
393 if ($length > 0) {
394 $content = fread($fd, $length);
395 }
396 flock($fd, LOCK_UN);
397 }
398 fclose($fd);
399 }
400 }
401 // Do a "test write" of the session file after opening it. The real session data is written in
402 // __destruct() and we can not create a sane error message there anymore, so this test should fail
403 // before if final session file can not be written due to permission problems.
404 $this->write($id, $content);
405 return $content;
406 }
407
408 /**
409 * Write session data. See @session_set_save_handler
410 *
411 * @param string $id The session id
412 * @param string $sessionData The data to be stored
413 * @throws Exception
414 * @return bool
415 */
416 public function write($id, $sessionData)
417 {
418 $sessionFile = $this->getSessionFile($id);
419 $result = false;
420 $changePermissions = !@is_file($sessionFile);
421 if ($fd = fopen($sessionFile, 'cb')) {
422 if (flock($fd, LOCK_EX)) {
423 ftruncate($fd, 0);
424 $res = fwrite($fd, $sessionData);
425 if ($res !== false) {
426 fflush($fd);
427 $result = true;
428 }
429 flock($fd, LOCK_UN);
430 }
431 fclose($fd);
432 // Change the permissions only if the file has just been created
433 if ($changePermissions) {
434 GeneralUtility::fixPermissions($sessionFile);
435 }
436 }
437 if (!$result) {
438 throw new Exception(
439 'Session file not writable. Please check permission on typo3temp/var/InstallToolSessions and its subdirectories.',
440 1424355157
441 );
442 }
443 return $result;
444 }
445
446 /**
447 * Destroys one session. See @session_set_save_handler
448 *
449 * @param string $id The session id
450 * @return string
451 */
452 public function destroy($id)
453 {
454 $sessionFile = $this->getSessionFile($id);
455 return @unlink($sessionFile);
456 }
457
458 /**
459 * Garbage collect session info. See @session_set_save_handler
460 *
461 * @param int $maxLifeTime The setting of session.gc_maxlifetime
462 * @return bool
463 */
464 public function gc($maxLifeTime)
465 {
466 $sessionSavePath = $this->getSessionSavePath();
467 $files = glob($sessionSavePath . '/hash_*');
468 if (!is_array($files)) {
469 return true;
470 }
471 foreach ($files as $filename) {
472 if (filemtime($filename) + $this->expireTimeInMinutes * 60 < time()) {
473 @unlink($filename);
474 }
475 }
476 return true;
477 }
478
479 /**
480 * Writes the session data at the end, to overcome a PHP APC bug.
481 *
482 * Writes the session data in a proper context that is not affected by the APC bug:
483 * http://pecl.php.net/bugs/bug.php?id=16721.
484 *
485 * This behaviour was introduced in #17511, where self::write() made use of GeneralUtility
486 * which due to the APC bug throws a "Fatal error: Class 'GeneralUtility' not found"
487 * (and the session data is not saved). Calling session_write_close() at this point
488 * seems to be the most easy solution, according to PHP author.
489 *
490 * @return void
491 */
492 public function __destruct()
493 {
494 session_write_close();
495 }
496 }