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