xref: /webtrees/app/Session.php (revision 1792ff1cf1956b41f3e3c853cfb279a803a71ed2)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2019 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees;
21
22use Illuminate\Support\Str;
23use Psr\Http\Message\ServerRequestInterface;
24
25use function array_map;
26use function explode;
27use function implode;
28use function parse_url;
29use function session_name;
30use function session_regenerate_id;
31use function session_register_shutdown;
32use function session_set_cookie_params;
33use function session_set_save_handler;
34use function session_start;
35use function session_status;
36use function session_write_close;
37
38use const PHP_SESSION_ACTIVE;
39use const PHP_URL_HOST;
40use const PHP_URL_PATH;
41use const PHP_URL_SCHEME;
42use const PHP_VERSION_ID;
43
44/**
45 * Session handling
46 */
47class Session
48{
49    private const SESSION_NAME = 'WT2_SESSION';
50
51    /**
52     * Start a session
53     *
54     * @param ServerRequestInterface $request
55     *
56     * @return void
57     */
58    public static function start(ServerRequestInterface $request): void
59    {
60        // Store sessions in the database
61        session_set_save_handler(new SessionDatabaseHandler($request));
62
63        $url    = $request->getAttribute('base_url');
64        $secure = parse_url($url, PHP_URL_SCHEME) === 'https';
65        $domain = parse_url($url, PHP_URL_HOST);
66        $path   = parse_url($url, PHP_URL_PATH) ?? '';
67
68        // Paths containing UTF-8 characters need special handling.
69        $path = implode('/', array_map('rawurlencode', explode('/', $path)));
70
71        session_name(self::SESSION_NAME);
72        session_register_shutdown();
73        // Since PHP 7.3, we can set "SameSite: Lax" to help protect against CSRF attacks.
74        if (PHP_VERSION_ID > 70300) {
75            session_set_cookie_params([
76                'lifetime' => 0,
77                'path'     => $path . '/',
78                'domain'   => $domain,
79                'secure'   => $secure,
80                'httponly' => true,
81                'samesite' => 'Lax',
82            ]);
83        } else {
84            session_set_cookie_params(0, $path . '/', $domain, $secure, true);
85        }
86        session_start();
87
88        // A new session? Prevent session fixation attacks by choosing a new session ID.
89        if (self::get('initiated') !== true) {
90            self::regenerate(true);
91            self::put('initiated', true);
92        }
93    }
94
95    /**
96     * Save/close the session.  This releases the session lock.
97     * Closing early can help concurrent connections.
98     */
99    public static function save(): void
100    {
101        if (session_status() === PHP_SESSION_ACTIVE) {
102            session_write_close();
103        }
104    }
105
106    /**
107     * Read a value from the session
108     *
109     * @param string $name
110     * @param mixed  $default
111     *
112     * @return mixed
113     */
114    public static function get(string $name, $default = null)
115    {
116        return $_SESSION[$name] ?? $default;
117    }
118
119    /**
120     * Read a value from the session and remove it.
121     *
122     * @param string $name
123     * @param mixed  $default
124     *
125     * @return mixed
126     */
127    public static function pull(string $name, $default = null)
128    {
129        $value = self::get($name, $default);
130        self::forget($name);
131
132        return $value;
133    }
134
135    /**
136     * After any change in authentication level, we should use a new session ID.
137     *
138     * @param bool $destroy
139     *
140     * @return void
141     */
142    public static function regenerate(bool $destroy = false): void
143    {
144        if ($destroy) {
145            self::clear();
146        }
147
148        if (session_status() === PHP_SESSION_ACTIVE) {
149            session_regenerate_id($destroy);
150        }
151    }
152
153    /**
154     * Remove all stored data from the session.
155     *
156     * @return void
157     */
158    public static function clear(): void
159    {
160        $_SESSION = [];
161    }
162
163    /**
164     * Write a value to the session
165     *
166     * @param string $name
167     * @param mixed  $value
168     *
169     * @return void
170     */
171    public static function put(string $name, $value): void
172    {
173        $_SESSION[$name] = $value;
174    }
175
176    /**
177     * Remove a value from the session
178     *
179     * @param string $name
180     *
181     * @return void
182     */
183    public static function forget(string $name): void
184    {
185        unset($_SESSION[$name]);
186    }
187
188    /**
189     * Cross-Site Request Forgery tokens - ensure that the user is submitting
190     * a form that was generated by the current session.
191     *
192     * @return string
193     */
194    public static function getCsrfToken(): string
195    {
196        if (!self::has('CSRF_TOKEN')) {
197            self::put('CSRF_TOKEN', Str::random(32));
198        }
199
200        return self::get('CSRF_TOKEN');
201    }
202
203    /**
204     * Does a session variable exist?
205     *
206     * @param string $name
207     *
208     * @return bool
209     */
210    public static function has(string $name): bool
211    {
212        return isset($_SESSION[$name]);
213    }
214}
215