xref: /webtrees/app/Session.php (revision 94026f200c17dcfccf296678dc90ea88b14f6246)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2019 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16declare(strict_types=1);
17
18namespace Fisharebest\Webtrees;
19
20use function session_status;
21use Symfony\Component\HttpFoundation\Request;
22
23/**
24 * Session handling
25 */
26class Session
27{
28    /**
29     * Start a session
30     *
31     * @return void
32     */
33    public static function start()
34    {
35        $domain   = '';
36        $path     = parse_url(WT_BASE_URL, PHP_URL_PATH);
37        $secure   = parse_url(WT_BASE_URL, PHP_URL_SCHEME) === 'https';
38        $httponly = true;
39
40        // Paths containing UTF-8 characters need special handling.
41        $path = implode('/', array_map('rawurlencode', explode('/', $path)));
42
43        self::setSaveHandler();
44
45        session_name('WT_SESSION');
46        session_register_shutdown();
47        session_set_cookie_params(0, $path, $domain, $secure, $httponly);
48        session_start();
49
50        // A new session? Prevent session fixation attacks by choosing a new session ID.
51        if (!self::get('initiated')) {
52            self::regenerate(true);
53            self::put('initiated', true);
54        }
55    }
56
57    /**
58     * Read a value from the session
59     *
60     * @param string $name
61     * @param mixed  $default
62     *
63     * @return mixed
64     */
65    public static function get(string $name, $default = null)
66    {
67        return $_SESSION[$name] ?? $default;
68    }
69
70    /**
71     * Write a value to the session
72     *
73     * @param string $name
74     * @param mixed  $value
75     *
76     * @return void
77     */
78    public static function put(string $name, $value)
79    {
80        $_SESSION[$name] = $value;
81    }
82
83    /**
84     * Remove a value from the session
85     *
86     * @param string $name
87     *
88     * @return void
89     */
90    public static function forget(string $name)
91    {
92        unset($_SESSION[$name]);
93    }
94
95    /**
96     * Does a session variable exist?
97     *
98     * @param string $name
99     *
100     * @return bool
101     */
102    public static function has(string $name): bool
103    {
104        return isset($_SESSION[$name]);
105    }
106
107    /**
108     * Remove all stored data from the session.
109     *
110     * @return void
111     */
112    public static function clear()
113    {
114        $_SESSION = [];
115    }
116
117    /**
118     * After any change in authentication level, we should use a new session ID.
119     *
120     * @param bool $destroy
121     *
122     * @return void
123     */
124    public static function regenerate(bool $destroy = false)
125    {
126        if ($destroy) {
127            self::clear();
128        }
129
130        if (session_status() === PHP_SESSION_ACTIVE) {
131            session_regenerate_id($destroy);
132        }
133    }
134
135    /**
136     * Set an explicit session ID. Typically used for search robots.
137     *
138     * @param string $id
139     *
140     * @return void
141     */
142    public static function setId(string $id)
143    {
144        session_id($id);
145    }
146
147    /**
148     * Initialise our session save handler
149     *
150     * @return void
151     */
152    private static function setSaveHandler()
153    {
154        session_set_save_handler(
155            function (): bool {
156                return Session::open();
157            },
158            function (): bool {
159                return Session::close();
160            },
161            function (string $id): string {
162                return Session::read($id);
163            },
164            function (string $id, string $data): bool {
165                return Session::write($id, $data);
166            },
167            function (string $id): bool {
168                return Session::destroy($id);
169            },
170            function (int $maxlifetime): bool {
171                return Session::gc($maxlifetime);
172            }
173        );
174    }
175
176    /**
177     * For session_set_save_handler()
178     *
179     * @return bool
180     */
181    private static function close(): bool
182    {
183        return true;
184    }
185
186    /**
187     * For session_set_save_handler()
188     *
189     * @param string $id
190     *
191     * @return bool
192     */
193    private static function destroy(string $id): bool
194    {
195        Database::prepare(
196            "DELETE FROM `##session` WHERE session_id = :session_id"
197        )->execute([
198            'session_id' => $id,
199        ]);
200
201        return true;
202    }
203
204    /**
205     * For session_set_save_handler()
206     *
207     * @param int $maxlifetime
208     *
209     * @return bool
210     */
211    private static function gc(int $maxlifetime): bool
212    {
213        Database::prepare(
214            "DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL :maxlifetime SECOND)"
215        )->execute([
216            'maxlifetime' => $maxlifetime,
217        ]);
218
219        return true;
220    }
221
222    /**
223     * For session_set_save_handler()
224     *
225     * @return bool
226     */
227    private static function open(): bool
228    {
229        return true;
230    }
231
232    /**
233     * For session_set_save_handler()
234     *
235     * @param string $id
236     *
237     * @return string
238     */
239    private static function read(string $id): string
240    {
241        return (string) Database::prepare(
242            "SELECT session_data FROM `##session` WHERE session_id = :session_id"
243        )->execute([
244            'session_id' => $id,
245        ])->fetchOne();
246    }
247
248    /**
249     * For session_set_save_handler()
250     *
251     * @param string $id
252     * @param string $data
253     *
254     * @return bool
255     */
256    private static function write(string $id, string $data): bool
257    {
258        $request = Request::createFromGlobals();
259
260        // Only update the session table once per minute, unless the session data has actually changed.
261        Database::prepare(
262            "INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" .
263            " VALUES (:session_id, :user_id, :ip_address, :data, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" .
264            " ON DUPLICATE KEY UPDATE" .
265            " user_id      = VALUES(user_id)," .
266            " ip_address   = VALUES(ip_address)," .
267            " session_data = VALUES(session_data)," .
268            " session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)"
269        )->execute([
270            'session_id' => $id,
271            'user_id'    => (int) Auth::id(),
272            'ip_address' => $request->getClientIp(),
273            'data'       => $data,
274        ]);
275
276        return true;
277    }
278
279
280    /**
281     * Cross-Site Request Forgery tokens - ensure that the user is submitting
282     * a form that was generated by the current session.
283     *
284     * @return string
285     */
286    public static function getCsrfToken(): string
287    {
288        if (!Session::has('CSRF_TOKEN')) {
289            $charset    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789';
290            $csrf_token = '';
291            for ($n = 0; $n < 32; ++$n) {
292                $csrf_token .= substr($charset, random_int(0, 61), 1);
293            }
294            Session::put('CSRF_TOKEN', $csrf_token);
295        }
296
297        return Session::get('CSRF_TOKEN');
298    }
299}
300