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