[BUGFIX] TceformsUpdateWizard shows up too often
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Service / SessionService.php
1 <?php
2 namespace TYPO3\CMS\Install\Service;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2009-2013 Ernesto Baschny <ernst@cron-it.de>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the text file GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31
32 /**
33 * Secure session handling for the install tool.
34 *
35 * @author Ernesto Baschny <ernst@cron-it.de>
36 */
37 class SessionService implements \TYPO3\CMS\Core\SingletonInterface {
38
39 /**
40 * The path to our typo3temp (where we can write our sessions). Set in the
41 * constructor.
42 *
43 * @var string
44 */
45 private $typo3tempPath;
46
47 /**
48 * Path where to store our session files in typo3temp. %s will be
49 * non-guessable.
50 *
51 * @var string
52 */
53 private $sessionPath = 'InstallToolSessions/%s';
54
55 /**
56 * the cookie to store the session ID of the install tool
57 *
58 * @var string
59 */
60 private $cookieName = 'Typo3InstallTool';
61
62 /**
63 * time (minutes) to expire an unused session
64 *
65 * @var integer
66 */
67 private $expireTimeInMinutes = 60;
68
69 /**
70 * time (minutes) to generate a new session id for our current session
71 *
72 * @var integer
73 */
74 private $regenerateSessionIdTime = 5;
75
76 /**
77 * Constructor. Starts PHP session handling in our own private store
78 *
79 * Side-effect: might set a cookie, so must be called before any other output.
80 */
81 public function __construct() {
82 $this->typo3tempPath = PATH_site . 'typo3temp/';
83 // Start our PHP session early so that hasSession() works
84 $sessionSavePath = $this->getSessionSavePath();
85 // Register our "save" session handler
86 session_set_save_handler(array($this, 'open'), array($this, 'close'), array($this, 'read'), array($this, 'write'), array($this, 'destroy'), array($this, 'gc'));
87 session_save_path($sessionSavePath);
88 session_name($this->cookieName);
89 ini_set('session.cookie_path', GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
90 // Always call the garbage collector to clean up stale session files
91 ini_set('session.gc_probability', 100);
92 ini_set('session.gc_divisor', 100);
93 ini_set('session.gc_maxlifetime', $this->expireTimeInMinutes * 2 * 60);
94 if (\TYPO3\CMS\Core\Utility\PhpOptionsUtility::isSessionAutoStartEnabled()) {
95 $sessionCreationError = 'Error: session.auto-start is enabled.<br />';
96 $sessionCreationError .= 'The PHP option session.auto-start is enabled. Disable this option in php.ini or .htaccess:<br />';
97 $sessionCreationError .= '<pre>php_value session.auto_start Off</pre>';
98 throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587485);
99 } elseif (defined('SID')) {
100 $sessionCreationError = 'Session already started by session_start().<br />';
101 $sessionCreationError .= 'Make sure no installed extension is starting a session in its ext_localconf.php or ext_tables.php.';
102 throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587486);
103 }
104 session_start();
105 }
106
107 /**
108 * Returns the path where to store our session files
109 *
110 * @throws \TYPO3\CMS\Install\Exception
111 * @return string Session save path
112 */
113 private function getSessionSavePath() {
114 if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
115 throw new \TYPO3\CMS\Install\Exception(
116 'No encryption key set to secure session',
117 1371243449
118 );
119 }
120 $sessionSavePath = sprintf(
121 $this->typo3tempPath . $this->sessionPath,
122 GeneralUtility::hmac('session:' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])
123 );
124 $this->ensureSessionSavePathExists($sessionSavePath);
125 return $sessionSavePath;
126 }
127
128 /**
129 * Create directories for the session save path
130 * and throw an exception if that fails.
131 *
132 * @param string $sessionSavePath The absolute path to the session files
133 * @throws \TYPO3\CMS\Install\Exception
134 */
135 private function ensureSessionSavePathExists($sessionSavePath) {
136 if (!is_dir($sessionSavePath)) {
137 try {
138 GeneralUtility::mkdir_deep($sessionSavePath);
139 } catch (\RuntimeException $exception) {
140 throw new \TYPO3\CMS\Install\Exception(
141 'Could not create session folder in typo3temp/. Make sure it is writeable!',
142 1294587484
143 );
144 }
145 GeneralUtility::writeFile($sessionSavePath . '/.htaccess', 'Order deny, allow' . LF . 'Deny from all');
146 $indexContent = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">';
147 $indexContent .= '<HTML><HEAD<TITLE></TITLE><META http-equiv=Refresh Content="0; Url=../../">';
148 $indexContent .= '</HEAD></HTML>';
149 GeneralUtility::writeFile($sessionSavePath . '/index.html', $indexContent);
150 }
151 }
152
153 /**
154 * Starts a new session
155 *
156 * @return string The session ID
157 */
158 public function startSession() {
159 $_SESSION['active'] = TRUE;
160 // Be sure to use our own session id, so create a new one
161 return $this->renewSession();
162 }
163
164 /**
165 * Destroys a session
166 */
167 public function destroySession() {
168 session_destroy();
169 }
170
171 /**
172 * Reset session. Sets _SESSION to empty array.
173 */
174 public function resetSession() {
175 $_SESSION = array();
176 $_SESSION['active'] = FALSE;
177 }
178
179 /**
180 * Generates a new session ID and sends it to the client.
181 *
182 * @return string the new session ID
183 */
184 private function renewSession() {
185 session_regenerate_id();
186 return session_id();
187 }
188
189 /**
190 * Checks whether we already have an active session.
191 *
192 * @return boolean TRUE if there is an active session, FALSE otherwise
193 */
194 public function hasSession() {
195 return ($_SESSION['active'] === TRUE);
196 }
197
198 /**
199 * Returns the session ID of the running session.
200 *
201 * @return string the session ID
202 */
203 public function getSessionId() {
204 return session_id();
205 }
206
207 /**
208 * Returns a session hash, which can only be calculated by the server.
209 * Used to store our session files without exposing the session ID.
210 *
211 * @param string $sessionId An alternative session ID. Defaults to our current session ID
212 * @throws \TYPO3\CMS\Install\Exception
213 * @return string the session hash
214 */
215 private function getSessionHash($sessionId = '') {
216 if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
217 throw new \TYPO3\CMS\Install\Exception(
218 'No encryption key set to secure session',
219 1371243450
220 );
221 }
222 if (!$sessionId) {
223 $sessionId = $this->getSessionId();
224 }
225 return md5($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . '|' . $sessionId);
226 }
227
228 /**
229 * Marks this session as an "authorized" one (login successful).
230 * Should only be called if:
231 * a) we have a valid session running
232 * b) the "password" or some other authorization mechanism really matched
233 *
234 * @return void
235 */
236 public function setAuthorized() {
237 $_SESSION['authorized'] = TRUE;
238 $_SESSION['lastSessionId'] = time();
239 $_SESSION['tstamp'] = time();
240 $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
241 // Renew the session id to avoid session fixation
242 $this->renewSession();
243 }
244
245 /**
246 * Check if we have an already authorized session
247 *
248 * @return boolean TRUE if this session has been authorized before (by a correct password)
249 */
250 public function isAuthorized() {
251 if (!$_SESSION['authorized']) {
252 return FALSE;
253 }
254 if ($_SESSION['expires'] < time()) {
255 // This session has already expired
256 return FALSE;
257 }
258 return TRUE;
259 }
260
261 /**
262 * Check if our session is expired.
263 * Useful only right after a FALSE "isAuthorized" to see if this is the
264 * reason for not being authorized anymore.
265 *
266 * @return boolean TRUE if an authorized session exists, but is expired
267 */
268 public function isExpired() {
269 if (!$_SESSION['authorized']) {
270 // Session never existed, means it is not "expired"
271 return FALSE;
272 }
273 if ($_SESSION['expires'] < time()) {
274 // This session was authorized before, but has expired
275 return TRUE;
276 }
277 return FALSE;
278 }
279
280 /**
281 * Refreshes our session information, rising the expire time.
282 * Also generates a new session ID every 5 minutes to minimize the risk of
283 * session hijacking.
284 *
285 * @return void
286 */
287 public function refreshSession() {
288 $_SESSION['tstamp'] = time();
289 $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
290 if (time() > $_SESSION['lastSessionId'] + $this->regenerateSessionIdTime * 60) {
291 // Renew our session ID
292 $_SESSION['lastSessionId'] = time();
293 $this->renewSession();
294 }
295 }
296
297 /**
298 * Add a message to "Flash" message storage.
299 *
300 * @param \TYPO3\CMS\Install\Status\StatusInterface $message A message to add
301 * @return void
302 */
303 public function addMessage(\TYPO3\CMS\Install\Status\StatusInterface $message) {
304 if (!is_array($_SESSION['messages'])) {
305 $_SESSION['messages'] = array();
306 }
307 $_SESSION['messages'][] = $message;
308 }
309
310 /**
311 * Return stored session messages and flush.
312 *
313 * @return array<\TYPO3\CMS\Install\Status\StatusInterface> Messages
314 */
315 public function getMessagesAndFlush() {
316 $messages = array();
317 if (is_array($_SESSION['messages'])) {
318 $messages = $_SESSION['messages'];
319 }
320 $_SESSION['messages'] = array();
321 return $messages;
322 }
323
324 /*************************
325 *
326 * PHP session handling with "secure" session files (hashed session id)
327 * see http://www.php.net/manual/en/function.session-set-save-handler.php
328 *
329 *************************/
330 /**
331 * Returns the file where to store our session data
332 *
333 * @param string $id
334 * @return string A filename
335 */
336 private function getSessionFile($id) {
337 $sessionSavePath = $this->getSessionSavePath();
338 return $sessionSavePath . '/hash_' . $this->getSessionHash($id);
339 }
340
341 /**
342 * Open function. See @session_set_save_handler
343 *
344 * @param string $savePath
345 * @param string $sessionName
346 * @return boolean
347 */
348 public function open($savePath, $sessionName) {
349 return TRUE;
350 }
351
352 /**
353 * Close function. See @session_set_save_handler
354 *
355 * @return boolean
356 */
357 public function close() {
358 return TRUE;
359 }
360
361 /**
362 * Read session data. See @session_set_save_handler
363 *
364 * @param string $id The session id
365 * @return string
366 */
367 public function read($id) {
368 $sessionFile = $this->getSessionFile($id);
369 return (string) (@file_get_contents($sessionFile));
370 }
371
372 /**
373 * Write session data. See @session_set_save_handler
374 *
375 * @param string $id The session id
376 * @param string $sessionData The data to be stored
377 * @return boolean
378 */
379 public function write($id, $sessionData) {
380 $sessionFile = $this->getSessionFile($id);
381 return GeneralUtility::writeFile($sessionFile, $sessionData);
382 }
383
384 /**
385 * Destroys one session. See @session_set_save_handler
386 *
387 * @param string $id The session id
388 * @return string
389 */
390 public function destroy($id) {
391 $sessionFile = $this->getSessionFile($id);
392 return @unlink($sessionFile);
393 }
394
395 /**
396 * Garbage collect session info. See @session_set_save_handler
397 *
398 * @param integer $maxLifeTime The setting of session.gc_maxlifetime
399 * @return boolean
400 */
401 public function gc($maxLifeTime) {
402 $sessionSavePath = $this->getSessionSavePath();
403 $files = glob($sessionSavePath . '/hash_*');
404 if (!is_array($files)) {
405 return TRUE;
406 }
407 foreach ($files as $filename) {
408 if (filemtime($filename) + $this->expireTimeInMinutes * 60 < time()) {
409 @unlink($filename);
410 }
411 }
412 return TRUE;
413 }
414
415 /**
416 * Writes the session data at the end, to overcome a PHP APC bug.
417 *
418 * Writes the session data in a proper context that is not affected by the APC bug:
419 * http://pecl.php.net/bugs/bug.php?id=16721.
420 *
421 * This behaviour was introduced in #17511, where self::write() made use of GeneralUtility
422 * which due to the APC bug throws a "Fatal error: Class 'GeneralUtility' not found"
423 * (and the session data is not saved). Calling session_write_close() at this point
424 * seems to be the most easy solution, according to PHP author.
425 *
426 * @return void
427 */
428 public function __destruct() {
429 session_write_close();
430 }
431 }