Completed
Push — master ( eeeacf...af5244 )
by
unknown
15:28
created

FileSessionHandler::getSessionFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace TYPO3\CMS\Install\Service\Session;
5
6
/*
7
 * This file is part of the TYPO3 CMS project.
8
 *
9
 * It is free software; you can redistribute it and/or modify it under
10
 * the terms of the GNU General Public License, either version 2
11
 * of the License, or any later version.
12
 *
13
 * For the full copyright and license information, please read the
14
 * LICENSE.txt file that was distributed with this source code.
15
 *
16
 * The TYPO3 project - inspiring people to share!
17
 */
18
19
use TYPO3\CMS\Core\Utility\GeneralUtility;
20
use TYPO3\CMS\Install\Service\Exception;
21
22
/**
23
 * PHP session handling with "secure" session files (hashed session id)
24
 * see http://www.php.net/manual/en/function.session-set-save-handler.php
25
 */
26
class FileSessionHandler implements \SessionHandlerInterface
27
{
28
    /**
29
     * The path to our var/session/ folder (where we can write our sessions). Set in the
30
     * constructor.
31
     * Path where to store our session files in var/session/.
32
     *
33
     * @var string
34
     */
35
    private $sessionPath = 'session/';
36
37
    /**
38
     * time (minutes) to expire an unused session
39
     *
40
     * @var int
41
     */
42
    private $expirationTimeInMinutes = 60;
43
44
    public function __construct(string $sessionPath, int $expirationTimeInMinutes)
45
    {
46
        $this->sessionPath = rtrim($sessionPath, '/') . '/';
47
        $this->expirationTimeInMinutes = $expirationTimeInMinutes;
48
        // Start our PHP session early so that hasSession() works
49
        session_save_path($this->getSessionSavePath());
50
    }
51
52
    /**
53
     * Returns the path where to store our session files
54
     *
55
     * @throws \TYPO3\CMS\Install\Exception
56
     * @return string Session save path
57
     */
58
    private function getSessionSavePath()
59
    {
60
        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
61
            throw new \TYPO3\CMS\Install\Exception(
62
                'No encryption key set to secure session',
63
                1371243449
64
            );
65
        }
66
        $sessionSavePath = $this->sessionPath . GeneralUtility::hmac('session:' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']);
67
        $this->ensureSessionSavePathExists($sessionSavePath);
68
        return $sessionSavePath;
69
    }
70
71
    /**
72
     * Returns the file where to store our session data
73
     *
74
     * @param string $id
75
     * @return string A filename
76
     */
77
    private function getSessionFile($id)
78
    {
79
        $sessionSavePath = $this->getSessionSavePath();
80
        return $sessionSavePath . '/hash_' . $this->getSessionHash($id);
81
    }
82
83
    /**
84
     * Open function. See @session_set_save_handler
85
     *
86
     * @param string $savePath
87
     * @param string $sessionName
88
     * @return bool
89
     */
90
    public function open($savePath, $sessionName)
91
    {
92
        return true;
93
    }
94
95
    /**
96
     * Close function. See @session_set_save_handler
97
     *
98
     * @return bool
99
     */
100
    public function close()
101
    {
102
        return true;
103
    }
104
105
    /**
106
     * Read session data. See @session_set_save_handler
107
     *
108
     * @param string $id The session id
109
     * @return string
110
     */
111
    public function read($id)
112
    {
113
        $sessionFile = $this->getSessionFile($id);
114
        $content = '';
115
        if (file_exists($sessionFile)) {
116
            if ($fd = fopen($sessionFile, 'rb')) {
117
                $lockres = flock($fd, LOCK_SH);
118
                if ($lockres) {
119
                    $length = filesize($sessionFile);
120
                    if ($length > 0) {
121
                        $content = fread($fd, $length);
122
                    }
123
                    flock($fd, LOCK_UN);
124
                }
125
                fclose($fd);
126
            }
127
        }
128
        // Do a "test write" of the session file after opening it. The real session data is written in
129
        // __destruct() and we can not create a sane error message there anymore, so this test should fail
130
        // before if final session file can not be written due to permission problems.
131
        $this->write($id, $content);
132
        return $content;
133
    }
134
135
    /**
136
     * Write session data. See @session_set_save_handler
137
     *
138
     * @param string $id The session id
139
     * @param string $sessionData The data to be stored
140
     * @throws Exception
141
     * @return bool
142
     */
143
    public function write($id, $sessionData)
144
    {
145
        $sessionFile = $this->getSessionFile($id);
146
        $result = false;
147
        $changePermissions = !@is_file($sessionFile);
148
        if ($fd = fopen($sessionFile, 'cb')) {
149
            if (flock($fd, LOCK_EX)) {
150
                ftruncate($fd, 0);
151
                $res = fwrite($fd, $sessionData);
152
                if ($res !== false) {
153
                    fflush($fd);
154
                    $result = true;
155
                }
156
                flock($fd, LOCK_UN);
157
            }
158
            fclose($fd);
159
            // Change the permissions only if the file has just been created
160
            if ($changePermissions) {
161
                GeneralUtility::fixPermissions($sessionFile);
162
            }
163
        }
164
        if (!$result) {
165
            throw new Exception(
166
                'Session file not writable. Please check permission on ' .
167
                $this->sessionPath . ' and its subdirectories.',
168
                1424355157
169
            );
170
        }
171
        return $result;
172
    }
173
174
    /**
175
     * Destroys one session. See @session_set_save_handler
176
     *
177
     * @param string $id The session id
178
     * @return bool
179
     */
180
    public function destroy($id): bool
181
    {
182
        $sessionFile = $this->getSessionFile($id);
183
        return @unlink($sessionFile);
184
    }
185
186
    /**
187
     * Garbage collect session info. See @session_set_save_handler
188
     *
189
     * @param int $maxLifeTime The setting of session.gc_maxlifetime
190
     * @return bool
191
     */
192
    public function gc($maxLifeTime)
193
    {
194
        $sessionSavePath = $this->getSessionSavePath();
195
        $files = glob($sessionSavePath . '/hash_*');
196
        if (!is_array($files)) {
197
            return true;
198
        }
199
        foreach ($files as $filename) {
200
            if (filemtime($filename) + $this->expirationTimeInMinutes * 60 < time()) {
201
                @unlink($filename);
202
            }
203
        }
204
        return true;
205
    }
206
207
    /**
208
     * Writes the session data at the end, to overcome a PHP APC bug.
209
     *
210
     * Writes the session data in a proper context that is not affected by the APC bug:
211
     * http://pecl.php.net/bugs/bug.php?id=16721.
212
     *
213
     * This behaviour was introduced in #17511, where self::write() made use of GeneralUtility
214
     * which due to the APC bug throws a "Fatal error: Class 'GeneralUtility' not found"
215
     * (and the session data is not saved). Calling session_write_close() at this point
216
     * seems to be the most easy solution, according to PHP author.
217
     */
218
    public function __destruct()
219
    {
220
        session_write_close();
221
    }
222
223
    /**
224
     * Returns the session ID of the running session.
225
     *
226
     * @return string the session ID
227
     */
228
    public function getSessionId()
229
    {
230
        return session_id();
231
    }
232
233
    /**
234
     * Returns a session hash, which can only be calculated by the server.
235
     * Used to store our session files without exposing the session ID.
236
     *
237
     * @param string $sessionId An alternative session ID. Defaults to our current session ID
238
     * @throws \TYPO3\CMS\Install\Exception
239
     * @return string the session hash
240
     */
241
    private function getSessionHash($sessionId = '')
242
    {
243
        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
244
            throw new \TYPO3\CMS\Install\Exception(
245
                'No encryption key set to secure session',
246
                1371243450
247
            );
248
        }
249
        if (!$sessionId) {
250
            $sessionId = $this->getSessionId();
251
        }
252
        return md5($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . '|' . $sessionId);
253
    }
254
255
    /**
256
     * Create directories for the session save path
257
     * and throw an exception if that fails.
258
     *
259
     * @param string $sessionSavePath The absolute path to the session files
260
     * @throws \TYPO3\CMS\Install\Exception
261
     */
262
    private function ensureSessionSavePathExists($sessionSavePath)
263
    {
264
        if (!is_dir($sessionSavePath)) {
265
            try {
266
                GeneralUtility::mkdir_deep($sessionSavePath);
267
            } catch (\RuntimeException $exception) {
268
                throw new \TYPO3\CMS\Install\Exception(
269
                    'Could not create session folder in ' . $this->sessionPath . '. Make sure it is writeable!',
270
                    1294587484
271
                );
272
            }
273
            $htaccessContent = '
274
# Apache < 2.3
275
<IfModule !mod_authz_core.c>
276
	Order allow,deny
277
	Deny from all
278
	Satisfy All
279
</IfModule>
280
281
# Apache ≥ 2.3
282
<IfModule mod_authz_core.c>
283
	Require all denied
284
</IfModule>
285
			';
286
            GeneralUtility::writeFile($sessionSavePath . '/.htaccess', $htaccessContent);
287
            $indexContent = '<!DOCTYPE html>';
288
            $indexContent .= '<html><head><title></title><meta http-equiv=Refresh Content="0; Url=../../"/>';
289
            $indexContent .= '</head></html>';
290
            GeneralUtility::writeFile($sessionSavePath . '/index.html', $indexContent);
291
        }
292
    }
293
}
294