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