Completed
Push — master ( a276ba...2a702e )
by
unknown
18:45
created

SessionService::renewSession()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 8
rs 10
c 2
b 0
f 0
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
16
namespace TYPO3\CMS\Install\Service;
17
18
use Symfony\Component\HttpFoundation\Cookie;
19
use TYPO3\CMS\Core\Core\Environment;
20
use TYPO3\CMS\Core\Http\CookieHeaderTrait;
21
use TYPO3\CMS\Core\Messaging\FlashMessage;
22
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
23
use TYPO3\CMS\Core\SingletonInterface;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
use TYPO3\CMS\Install\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, TYPO3\CMS\Install\Service\Exception. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
26
use TYPO3\CMS\Install\Service\Session\FileSessionHandler;
27
28
/**
29
 * Secure session handling for the install tool.
30
 *
31
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
32
 */
33
class SessionService implements SingletonInterface
34
{
35
    use BlockSerializationTrait;
36
    use CookieHeaderTrait;
37
38
    /**
39
     * the cookie to store the session ID of the install tool
40
     *
41
     * @var string
42
     */
43
    private $cookieName = 'Typo3InstallTool';
44
45
    /**
46
     * time (minutes) to expire an unused session
47
     *
48
     * @var int
49
     */
50
    private $expireTimeInMinutes = 60;
51
52
    /**
53
     * time (minutes) to generate a new session id for our current session
54
     *
55
     * @var int
56
     */
57
    private $regenerateSessionIdTime = 5;
58
59
    /**
60
     * Constructor. Starts PHP session handling in our own private store
61
     *
62
     * Side-effect: might set a cookie, so must be called before any other output.
63
     */
64
    public function __construct()
65
    {
66
        // Register our "save" session handler
67
        $sessionHandler = GeneralUtility::makeInstance(
68
            FileSessionHandler::class,
69
            Environment::getVarPath() . '/session',
70
            $this->expireTimeInMinutes
71
        );
72
        session_set_save_handler($sessionHandler);
73
        session_name($this->cookieName);
74
        ini_set('session.cookie_httponly', true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $newvalue of ini_set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

74
        ini_set('session.cookie_httponly', /** @scrutinizer ignore-type */ true);
Loading history...
75
        if ($this->hasSameSiteCookieSupport()) {
76
            ini_set('session.cookie_samesite', Cookie::SAMESITE_STRICT);
77
        }
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 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 Exception($sessionCreationError, 1294587486);
93
        }
94
    }
95
96
    public function initializeSession()
97
    {
98
        if (session_status() === PHP_SESSION_ACTIVE) {
99
            return;
100
        }
101
        session_start();
102
        if (!$this->hasSameSiteCookieSupport()) {
103
            $this->resendCookieHeader();
104
        }
105
    }
106
107
    /**
108
     * Starts a new session
109
     *
110
     * @return string The session ID
111
     */
112
    public function startSession()
113
    {
114
        $this->initializeSession();
115
        // check if session is already active
116
        if ($_SESSION['active'] ?? false) {
117
            return session_id();
118
        }
119
        $_SESSION['active'] = true;
120
        // Be sure to use our own session id, so create a new one
121
        return $this->renewSession();
122
    }
123
124
    /**
125
     * Destroys a session
126
     */
127
    public function destroySession()
128
    {
129
        if ($this->hasSessionCookie()) {
130
            $this->initializeSession();
131
            $_SESSION = [];
132
            $params = session_get_cookie_params();
133
            setcookie(
134
                session_name(),
135
                '',
136
                time() - 42000,
137
                $params['path'],
138
                $params['domain'],
139
                $params['secure'],
140
                $params['httponly']
141
            );
142
            session_destroy();
143
        }
144
    }
145
146
    /**
147
     * Reset session. Sets _SESSION to empty array.
148
     */
149
    public function resetSession()
150
    {
151
        $this->initializeSession();
152
        $_SESSION = [];
153
        $_SESSION['active'] = false;
154
    }
155
156
    /**
157
     * Generates a new session ID and sends it to the client.
158
     *
159
     * @return string the new session ID
160
     */
161
    private function renewSession()
162
    {
163
        // we do not have parallel ajax requests so we can safely remove the old session data
164
        session_regenerate_id(true);
165
        if (!$this->hasSameSiteCookieSupport()) {
166
            $this->resendCookieHeader([$this->cookieName]);
167
        }
168
        return session_id();
169
    }
170
171
    /**
172
     * Checks whether whether is session cookie is set
173
     *
174
     * @return bool
175
     */
176
    public function hasSessionCookie(): bool
177
    {
178
        return isset($_COOKIE[$this->cookieName]);
179
    }
180
181
    /**
182
     * Marks this session as an "authorized" one (login successful).
183
     * Should only be called if:
184
     * a) we have a valid session running
185
     * b) the "password" or some other authorization mechanism really matched
186
     */
187
    public function setAuthorized()
188
    {
189
        $_SESSION['authorized'] = true;
190
        $_SESSION['lastSessionId'] = time();
191
        $_SESSION['tstamp'] = time();
192
        $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
193
        // Renew the session id to avoid session fixation
194
        $this->renewSession();
195
    }
196
197
    /**
198
     * Marks this session as an "authorized by backend user" one.
199
     * This is called by BackendModuleController from backend context.
200
     */
201
    public function setAuthorizedBackendSession()
202
    {
203
        $_SESSION['authorized'] = true;
204
        $_SESSION['lastSessionId'] = time();
205
        $_SESSION['tstamp'] = time();
206
        $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
207
        $_SESSION['isBackendSession'] = true;
208
        // Renew the session id to avoid session fixation
209
        $this->renewSession();
210
    }
211
212
    /**
213
     * Check if we have an already authorized session
214
     *
215
     * @return bool TRUE if this session has been authorized before (by a correct password)
216
     */
217
    public function isAuthorized()
218
    {
219
        if (!$this->hasSessionCookie()) {
220
            return false;
221
        }
222
        $this->initializeSession();
223
        if (empty($_SESSION['authorized'])) {
224
            return false;
225
        }
226
        return !$this->isExpired();
227
    }
228
229
    /**
230
     * Check if we have an authorized session from a system maintainer
231
     *
232
     * @return bool TRUE if this session has been authorized before and initialized by a backend system maintainer
233
     */
234
    public function isAuthorizedBackendUserSession()
235
    {
236
        if (!$this->hasSessionCookie()) {
237
            return false;
238
        }
239
        $this->initializeSession();
240
        if (empty($_SESSION['authorized']) || empty($_SESSION['isBackendSession'])) {
241
            return false;
242
        }
243
        return !$this->isExpired();
244
    }
245
246
    /**
247
     * Check if our session is expired.
248
     * Useful only right after a FALSE "isAuthorized" to see if this is the
249
     * reason for not being authorized anymore.
250
     *
251
     * @return bool TRUE if an authorized session exists, but is expired
252
     */
253
    public function isExpired()
254
    {
255
        if (!$this->hasSessionCookie()) {
256
            // Session never existed, means it is not "expired"
257
            return false;
258
        }
259
        $this->initializeSession();
260
        if (empty($_SESSION['authorized'])) {
261
            // Session never authorized, means it is not "expired"
262
            return false;
263
        }
264
        return $_SESSION['expires'] <= time();
265
    }
266
267
    /**
268
     * Refreshes our session information, rising the expire time.
269
     * Also generates a new session ID every 5 minutes to minimize the risk of
270
     * session hijacking.
271
     */
272
    public function refreshSession()
273
    {
274
        $_SESSION['tstamp'] = time();
275
        $_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
276
        if (time() > $_SESSION['lastSessionId'] + $this->regenerateSessionIdTime * 60) {
277
            // Renew our session ID
278
            $_SESSION['lastSessionId'] = time();
279
            $this->renewSession();
280
        }
281
    }
282
283
    /**
284
     * Add a message to "Flash" message storage.
285
     *
286
     * @param FlashMessage $message A message to add
287
     */
288
    public function addMessage(FlashMessage $message)
289
    {
290
        if (!is_array($_SESSION['messages'])) {
291
            $_SESSION['messages'] = [];
292
        }
293
        $_SESSION['messages'][] = $message;
294
    }
295
296
    /**
297
     * Return stored session messages and flush.
298
     *
299
     * @return FlashMessage[] Messages
300
     */
301
    public function getMessagesAndFlush()
302
    {
303
        $messages = [];
304
        if (is_array($_SESSION['messages'])) {
305
            $messages = $_SESSION['messages'];
306
        }
307
        $_SESSION['messages'] = [];
308
        return $messages;
309
    }
310
311
    /**
312
     * Check if php session.auto_start is enabled
313
     *
314
     * @return bool TRUE if session.auto_start is enabled, FALSE if disabled
315
     */
316
    protected function isSessionAutoStartEnabled()
317
    {
318
        return $this->getIniValueBoolean('session.auto_start');
319
    }
320
321
    /**
322
     * Cast an on/off php ini value to boolean
323
     *
324
     * @param string $configOption
325
     * @return bool TRUE if the given option is enabled, FALSE if disabled
326
     */
327
    protected function getIniValueBoolean($configOption)
328
    {
329
        return filter_var(
330
            ini_get($configOption),
331
            FILTER_VALIDATE_BOOLEAN,
332
            [FILTER_REQUIRE_SCALAR, FILTER_NULL_ON_FAILURE]
333
        );
334
    }
335
}
336