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