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