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