xref: /webtrees/app/Session.php (revision 01461f8661111b35c96e8f511f9f4a2267c68123)
131bc7874SGreg Roach<?php
231bc7874SGreg Roach/**
331bc7874SGreg Roach * webtrees: online genealogy
41062a142SGreg Roach * Copyright (C) 2018 webtrees development team
531bc7874SGreg Roach * This program is free software: you can redistribute it and/or modify
631bc7874SGreg Roach * it under the terms of the GNU General Public License as published by
731bc7874SGreg Roach * the Free Software Foundation, either version 3 of the License, or
831bc7874SGreg Roach * (at your option) any later version.
931bc7874SGreg Roach * This program is distributed in the hope that it will be useful,
1031bc7874SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
1131bc7874SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1231bc7874SGreg Roach * GNU General Public License for more details.
1331bc7874SGreg Roach * You should have received a copy of the GNU General Public License
1431bc7874SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
1531bc7874SGreg Roach */
16e7f56f2aSGreg Roachdeclare(strict_types=1);
17e7f56f2aSGreg Roach
1876692c8bSGreg Roachnamespace Fisharebest\Webtrees;
1931bc7874SGreg Roach
20*01461f86SGreg Roachuse function session_status;
214c891c40SGreg Roachuse Symfony\Component\HttpFoundation\Request;
224c891c40SGreg Roach
2331bc7874SGreg Roach/**
2442af74e7SGreg Roach * Session handling
2531bc7874SGreg Roach */
26c1010edaSGreg Roachclass Session
27c1010edaSGreg Roach{
2831bc7874SGreg Roach    /**
2931bc7874SGreg Roach     * Start a session
30c7ff4153SGreg Roach     *
31c7ff4153SGreg Roach     * @return void
3231bc7874SGreg Roach     */
3308df3d18SGreg Roach    public static function start()
34c1010edaSGreg Roach    {
3508df3d18SGreg Roach        $domain   = '';
3608df3d18SGreg Roach        $path     = parse_url(WT_BASE_URL, PHP_URL_PATH);
3708df3d18SGreg Roach        $secure   = parse_url(WT_BASE_URL, PHP_URL_SCHEME) === 'https';
3808df3d18SGreg Roach        $httponly = true;
3908df3d18SGreg Roach
4008df3d18SGreg Roach        // Paths containing UTF-8 characters need special handling.
4108df3d18SGreg Roach        $path = implode('/', array_map('rawurlencode', explode('/', $path)));
4208df3d18SGreg Roach
4308df3d18SGreg Roach        self::setSaveHandler();
4408df3d18SGreg Roach
4508df3d18SGreg Roach        session_name('WT_SESSION');
4631bc7874SGreg Roach        session_register_shutdown();
47a0dfa978SGreg Roach        session_set_cookie_params(0, $path, $domain, $secure, $httponly);
4831bc7874SGreg Roach        session_start();
4908df3d18SGreg Roach
5008df3d18SGreg Roach        // A new session? Prevent session fixation attacks by choosing a new session ID.
5108df3d18SGreg Roach        if (!self::get('initiated')) {
5208df3d18SGreg Roach            self::regenerate(true);
5308df3d18SGreg Roach            self::put('initiated', true);
5408df3d18SGreg Roach        }
5531bc7874SGreg Roach    }
5631bc7874SGreg Roach
5731bc7874SGreg Roach    /**
5831bc7874SGreg Roach     * Read a value from the session
5931bc7874SGreg Roach     *
6031bc7874SGreg Roach     * @param string $name
6131bc7874SGreg Roach     * @param mixed  $default
6231bc7874SGreg Roach     *
6331bc7874SGreg Roach     * @return mixed
6431bc7874SGreg Roach     */
65c7ff4153SGreg Roach    public static function get(string $name, $default = null)
66c1010edaSGreg Roach    {
6763485653SRico Sonntag        return $_SESSION[$name] ?? $default;
6831bc7874SGreg Roach    }
6931bc7874SGreg Roach
7031bc7874SGreg Roach    /**
7131bc7874SGreg Roach     * Write a value to the session
7231bc7874SGreg Roach     *
7331bc7874SGreg Roach     * @param string $name
7431bc7874SGreg Roach     * @param mixed  $value
75c7ff4153SGreg Roach     *
76c7ff4153SGreg Roach     * @return void
7731bc7874SGreg Roach     */
78c7ff4153SGreg Roach    public static function put(string $name, $value)
79c1010edaSGreg Roach    {
8031bc7874SGreg Roach        $_SESSION[$name] = $value;
8131bc7874SGreg Roach    }
8231bc7874SGreg Roach
8331bc7874SGreg Roach    /**
8431bc7874SGreg Roach     * Remove a value from the session
8531bc7874SGreg Roach     *
8631bc7874SGreg Roach     * @param string $name
87c7ff4153SGreg Roach     *
88c7ff4153SGreg Roach     * @return void
8931bc7874SGreg Roach     */
90c7ff4153SGreg Roach    public static function forget(string $name)
91c1010edaSGreg Roach    {
9231bc7874SGreg Roach        unset($_SESSION[$name]);
9331bc7874SGreg Roach    }
9431bc7874SGreg Roach
9531bc7874SGreg Roach    /**
9631bc7874SGreg Roach     * Does a session variable exist?
9731bc7874SGreg Roach     *
9831bc7874SGreg Roach     * @param string $name
9931bc7874SGreg Roach     *
100cbc1590aSGreg Roach     * @return bool
10131bc7874SGreg Roach     */
102c7ff4153SGreg Roach    public static function has(string $name): bool
103c1010edaSGreg Roach    {
10491fb15f0SGreg Roach        return isset($_SESSION[$name]);
10531bc7874SGreg Roach    }
10631bc7874SGreg Roach
10731bc7874SGreg Roach    /**
108f5004097SGreg Roach     * Remove all stored data from the session.
109c7ff4153SGreg Roach     *
110c7ff4153SGreg Roach     * @return void
111f5004097SGreg Roach     */
112c1010edaSGreg Roach    public static function clear()
113c1010edaSGreg Roach    {
11413abd6f3SGreg Roach        $_SESSION = [];
115f5004097SGreg Roach    }
116f5004097SGreg Roach
117f5004097SGreg Roach    /**
11831bc7874SGreg Roach     * After any change in authentication level, we should use a new session ID.
11931bc7874SGreg Roach     *
12031bc7874SGreg Roach     * @param bool $destroy
121c7ff4153SGreg Roach     *
122c7ff4153SGreg Roach     * @return void
12331bc7874SGreg Roach     */
124c7ff4153SGreg Roach    public static function regenerate(bool $destroy = false)
125c1010edaSGreg Roach    {
126f5004097SGreg Roach        if ($destroy) {
127f5004097SGreg Roach            self::clear();
128f5004097SGreg Roach        }
129*01461f86SGreg Roach
130*01461f86SGreg Roach        if (session_status() === PHP_SESSION_ACTIVE) {
13131bc7874SGreg Roach            session_regenerate_id($destroy);
13231bc7874SGreg Roach        }
133*01461f86SGreg Roach    }
13431bc7874SGreg Roach
13531bc7874SGreg Roach    /**
13631bc7874SGreg Roach     * Set an explicit session ID. Typically used for search robots.
13731bc7874SGreg Roach     *
13831bc7874SGreg Roach     * @param string $id
139c7ff4153SGreg Roach     *
140c7ff4153SGreg Roach     * @return void
14131bc7874SGreg Roach     */
142c7ff4153SGreg Roach    public static function setId(string $id)
143c1010edaSGreg Roach    {
14431bc7874SGreg Roach        session_id($id);
14531bc7874SGreg Roach    }
14657514a4fSGreg Roach
14757514a4fSGreg Roach    /**
14857514a4fSGreg Roach     * Initialise our session save handler
149c7ff4153SGreg Roach     *
150c7ff4153SGreg Roach     * @return void
15157514a4fSGreg Roach     */
15208df3d18SGreg Roach    private static function setSaveHandler()
153c1010edaSGreg Roach    {
15457514a4fSGreg Roach        session_set_save_handler(
15557514a4fSGreg Roach            function (): bool {
15657514a4fSGreg Roach                return Session::open();
15757514a4fSGreg Roach            },
15857514a4fSGreg Roach            function (): bool {
15957514a4fSGreg Roach                return Session::close();
16057514a4fSGreg Roach            },
16157514a4fSGreg Roach            function (string $id): string {
16257514a4fSGreg Roach                return Session::read($id);
16357514a4fSGreg Roach            },
16457514a4fSGreg Roach            function (string $id, string $data): bool {
16557514a4fSGreg Roach                return Session::write($id, $data);
16657514a4fSGreg Roach            },
16757514a4fSGreg Roach            function (string $id): bool {
16857514a4fSGreg Roach                return Session::destroy($id);
16957514a4fSGreg Roach            },
17057514a4fSGreg Roach            function (int $maxlifetime): bool {
17157514a4fSGreg Roach                return Session::gc($maxlifetime);
17257514a4fSGreg Roach            }
17357514a4fSGreg Roach        );
17457514a4fSGreg Roach    }
17557514a4fSGreg Roach
17657514a4fSGreg Roach    /**
17757514a4fSGreg Roach     * For session_set_save_handler()
17857514a4fSGreg Roach     *
17957514a4fSGreg Roach     * @return bool
18057514a4fSGreg Roach     */
1818f53f488SRico Sonntag    private static function close(): bool
182c1010edaSGreg Roach    {
18357514a4fSGreg Roach        return true;
18457514a4fSGreg Roach    }
18557514a4fSGreg Roach
18657514a4fSGreg Roach    /**
18757514a4fSGreg Roach     * For session_set_save_handler()
18857514a4fSGreg Roach     *
18957514a4fSGreg Roach     * @param string $id
19057514a4fSGreg Roach     *
19157514a4fSGreg Roach     * @return bool
19257514a4fSGreg Roach     */
1938f53f488SRico Sonntag    private static function destroy(string $id): bool
194c1010edaSGreg Roach    {
19557514a4fSGreg Roach        Database::prepare(
19657514a4fSGreg Roach            "DELETE FROM `##session` WHERE session_id = :session_id"
19757514a4fSGreg Roach        )->execute([
198c1010edaSGreg Roach            'session_id' => $id,
19957514a4fSGreg Roach        ]);
20057514a4fSGreg Roach
20157514a4fSGreg Roach        return true;
20257514a4fSGreg Roach    }
20357514a4fSGreg Roach
20457514a4fSGreg Roach    /**
20557514a4fSGreg Roach     * For session_set_save_handler()
20657514a4fSGreg Roach     *
20757514a4fSGreg Roach     * @param int $maxlifetime
20857514a4fSGreg Roach     *
20957514a4fSGreg Roach     * @return bool
21057514a4fSGreg Roach     */
2118f53f488SRico Sonntag    private static function gc(int $maxlifetime): bool
212c1010edaSGreg Roach    {
21357514a4fSGreg Roach        Database::prepare(
21457514a4fSGreg Roach            "DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL :maxlifetime SECOND)"
21557514a4fSGreg Roach        )->execute([
216c1010edaSGreg Roach            'maxlifetime' => $maxlifetime,
21757514a4fSGreg Roach        ]);
21857514a4fSGreg Roach
21957514a4fSGreg Roach        return true;
22057514a4fSGreg Roach    }
22157514a4fSGreg Roach
22257514a4fSGreg Roach    /**
22357514a4fSGreg Roach     * For session_set_save_handler()
22457514a4fSGreg Roach     *
22557514a4fSGreg Roach     * @return bool
22657514a4fSGreg Roach     */
2278f53f488SRico Sonntag    private static function open(): bool
228c1010edaSGreg Roach    {
22957514a4fSGreg Roach        return true;
23057514a4fSGreg Roach    }
23157514a4fSGreg Roach
23257514a4fSGreg Roach    /**
23357514a4fSGreg Roach     * For session_set_save_handler()
23457514a4fSGreg Roach     *
23557514a4fSGreg Roach     * @param string $id
23657514a4fSGreg Roach     *
23757514a4fSGreg Roach     * @return string
23857514a4fSGreg Roach     */
239c1010edaSGreg Roach    private static function read(string $id): string
240c1010edaSGreg Roach    {
24157514a4fSGreg Roach        return (string) Database::prepare(
24257514a4fSGreg Roach            "SELECT session_data FROM `##session` WHERE session_id = :session_id"
24357514a4fSGreg Roach        )->execute([
244c1010edaSGreg Roach            'session_id' => $id,
24557514a4fSGreg Roach        ])->fetchOne();
24657514a4fSGreg Roach    }
24757514a4fSGreg Roach
24857514a4fSGreg Roach    /**
24957514a4fSGreg Roach     * For session_set_save_handler()
25057514a4fSGreg Roach     *
25157514a4fSGreg Roach     * @param string $id
25257514a4fSGreg Roach     * @param string $data
25357514a4fSGreg Roach     *
25457514a4fSGreg Roach     * @return bool
25557514a4fSGreg Roach     */
256c1010edaSGreg Roach    private static function write(string $id, string $data): bool
257c1010edaSGreg Roach    {
2584c891c40SGreg Roach        $request = Request::createFromGlobals();
2594c891c40SGreg Roach
26057514a4fSGreg Roach        // Only update the session table once per minute, unless the session data has actually changed.
26157514a4fSGreg Roach        Database::prepare(
26257514a4fSGreg Roach            "INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" .
2634c891c40SGreg Roach            " VALUES (:session_id, :user_id, :ip_address, :data, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" .
26457514a4fSGreg Roach            " ON DUPLICATE KEY UPDATE" .
26557514a4fSGreg Roach            " user_id      = VALUES(user_id)," .
26657514a4fSGreg Roach            " ip_address   = VALUES(ip_address)," .
26757514a4fSGreg Roach            " session_data = VALUES(session_data)," .
26857514a4fSGreg Roach            " session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)"
26957514a4fSGreg Roach        )->execute([
2704c891c40SGreg Roach            'session_id' => $id,
2714c891c40SGreg Roach            'user_id'    => (int) Auth::id(),
2724c891c40SGreg Roach            'ip_address' => $request->getClientIp(),
2734c891c40SGreg Roach            'data'       => $data,
2744c891c40SGreg Roach        ]);
27557514a4fSGreg Roach
27657514a4fSGreg Roach        return true;
27757514a4fSGreg Roach    }
278a45f9889SGreg Roach
279a45f9889SGreg Roach
280a45f9889SGreg Roach    /**
281a45f9889SGreg Roach     * Cross-Site Request Forgery tokens - ensure that the user is submitting
282a45f9889SGreg Roach     * a form that was generated by the current session.
283a45f9889SGreg Roach     *
284a45f9889SGreg Roach     * @return string
285a45f9889SGreg Roach     */
2868f53f488SRico Sonntag    public static function getCsrfToken(): string
287a45f9889SGreg Roach    {
288a45f9889SGreg Roach        if (!Session::has('CSRF_TOKEN')) {
289a45f9889SGreg Roach            $charset    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789';
290a45f9889SGreg Roach            $csrf_token = '';
291a45f9889SGreg Roach            for ($n = 0; $n < 32; ++$n) {
292a45f9889SGreg Roach                $csrf_token .= substr($charset, random_int(0, 61), 1);
293a45f9889SGreg Roach            }
294a45f9889SGreg Roach            Session::put('CSRF_TOKEN', $csrf_token);
295a45f9889SGreg Roach        }
296a45f9889SGreg Roach
297a45f9889SGreg Roach        return Session::get('CSRF_TOKEN');
298a45f9889SGreg Roach    }
29931bc7874SGreg Roach}
300