xref: /webtrees/app/Session.php (revision e7f56f2af615447ab1a7646851f88b465ace9e04)
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 */
16*e7f56f2aSGreg Roachdeclare(strict_types=1);
17*e7f56f2aSGreg Roach
1876692c8bSGreg Roachnamespace Fisharebest\Webtrees;
1931bc7874SGreg Roach
204c891c40SGreg Roachuse Symfony\Component\HttpFoundation\Request;
214c891c40SGreg Roach
2231bc7874SGreg Roach/**
2342af74e7SGreg Roach * Session handling
2431bc7874SGreg Roach */
25c1010edaSGreg Roachclass Session
26c1010edaSGreg Roach{
2731bc7874SGreg Roach    /**
2831bc7874SGreg Roach     * Start a session
29c7ff4153SGreg Roach     *
30c7ff4153SGreg Roach     * @return void
3131bc7874SGreg Roach     */
3208df3d18SGreg Roach    public static function start()
33c1010edaSGreg Roach    {
3408df3d18SGreg Roach        $domain   = '';
3508df3d18SGreg Roach        $path     = parse_url(WT_BASE_URL, PHP_URL_PATH);
3608df3d18SGreg Roach        $secure   = parse_url(WT_BASE_URL, PHP_URL_SCHEME) === 'https';
3708df3d18SGreg Roach        $httponly = true;
3808df3d18SGreg Roach
3908df3d18SGreg Roach        // Paths containing UTF-8 characters need special handling.
4008df3d18SGreg Roach        $path = implode('/', array_map('rawurlencode', explode('/', $path)));
4108df3d18SGreg Roach
4208df3d18SGreg Roach        self::setSaveHandler();
4308df3d18SGreg Roach
4408df3d18SGreg Roach        session_name('WT_SESSION');
4531bc7874SGreg Roach        session_register_shutdown();
46a0dfa978SGreg Roach        session_set_cookie_params(0, $path, $domain, $secure, $httponly);
4731bc7874SGreg Roach        session_start();
4808df3d18SGreg Roach
4908df3d18SGreg Roach        // A new session? Prevent session fixation attacks by choosing a new session ID.
5008df3d18SGreg Roach        if (!self::get('initiated')) {
5108df3d18SGreg Roach            self::regenerate(true);
5208df3d18SGreg Roach            self::put('initiated', true);
5308df3d18SGreg Roach        }
5431bc7874SGreg Roach    }
5531bc7874SGreg Roach
5631bc7874SGreg Roach    /**
5731bc7874SGreg Roach     * Read a value from the session
5831bc7874SGreg Roach     *
5931bc7874SGreg Roach     * @param string $name
6031bc7874SGreg Roach     * @param mixed  $default
6131bc7874SGreg Roach     *
6231bc7874SGreg Roach     * @return mixed
6331bc7874SGreg Roach     */
64c7ff4153SGreg Roach    public static function get(string $name, $default = null)
65c1010edaSGreg Roach    {
6663485653SRico Sonntag        return $_SESSION[$name] ?? $default;
6731bc7874SGreg Roach    }
6831bc7874SGreg Roach
6931bc7874SGreg Roach    /**
7031bc7874SGreg Roach     * Write a value to the session
7131bc7874SGreg Roach     *
7231bc7874SGreg Roach     * @param string $name
7331bc7874SGreg Roach     * @param mixed  $value
74c7ff4153SGreg Roach     *
75c7ff4153SGreg Roach     * @return void
7631bc7874SGreg Roach     */
77c7ff4153SGreg Roach    public static function put(string $name, $value)
78c1010edaSGreg Roach    {
7931bc7874SGreg Roach        $_SESSION[$name] = $value;
8031bc7874SGreg Roach    }
8131bc7874SGreg Roach
8231bc7874SGreg Roach    /**
8331bc7874SGreg Roach     * Remove a value from the session
8431bc7874SGreg Roach     *
8531bc7874SGreg Roach     * @param string $name
86c7ff4153SGreg Roach     *
87c7ff4153SGreg Roach     * @return void
8831bc7874SGreg Roach     */
89c7ff4153SGreg Roach    public static function forget(string $name)
90c1010edaSGreg Roach    {
9131bc7874SGreg Roach        unset($_SESSION[$name]);
9231bc7874SGreg Roach    }
9331bc7874SGreg Roach
9431bc7874SGreg Roach    /**
9531bc7874SGreg Roach     * Does a session variable exist?
9631bc7874SGreg Roach     *
9731bc7874SGreg Roach     * @param string $name
9831bc7874SGreg Roach     *
99cbc1590aSGreg Roach     * @return bool
10031bc7874SGreg Roach     */
101c7ff4153SGreg Roach    public static function has(string $name): bool
102c1010edaSGreg Roach    {
10391fb15f0SGreg Roach        return isset($_SESSION[$name]);
10431bc7874SGreg Roach    }
10531bc7874SGreg Roach
10631bc7874SGreg Roach    /**
107f5004097SGreg Roach     * Remove all stored data from the session.
108c7ff4153SGreg Roach     *
109c7ff4153SGreg Roach     * @return void
110f5004097SGreg Roach     */
111c1010edaSGreg Roach    public static function clear()
112c1010edaSGreg Roach    {
11313abd6f3SGreg Roach        $_SESSION = [];
114f5004097SGreg Roach    }
115f5004097SGreg Roach
116f5004097SGreg Roach    /**
11731bc7874SGreg Roach     * After any change in authentication level, we should use a new session ID.
11831bc7874SGreg Roach     *
11931bc7874SGreg Roach     * @param bool $destroy
120c7ff4153SGreg Roach     *
121c7ff4153SGreg Roach     * @return void
12231bc7874SGreg Roach     */
123c7ff4153SGreg Roach    public static function regenerate(bool $destroy = false)
124c1010edaSGreg Roach    {
125f5004097SGreg Roach        if ($destroy) {
126f5004097SGreg Roach            self::clear();
127f5004097SGreg Roach        }
12831bc7874SGreg Roach        session_regenerate_id($destroy);
12931bc7874SGreg Roach    }
13031bc7874SGreg Roach
13131bc7874SGreg Roach    /**
13231bc7874SGreg Roach     * Set an explicit session ID. Typically used for search robots.
13331bc7874SGreg Roach     *
13431bc7874SGreg Roach     * @param string $id
135c7ff4153SGreg Roach     *
136c7ff4153SGreg Roach     * @return void
13731bc7874SGreg Roach     */
138c7ff4153SGreg Roach    public static function setId(string $id)
139c1010edaSGreg Roach    {
14031bc7874SGreg Roach        session_id($id);
14131bc7874SGreg Roach    }
14257514a4fSGreg Roach
14357514a4fSGreg Roach    /**
14457514a4fSGreg Roach     * Initialise our session save handler
145c7ff4153SGreg Roach     *
146c7ff4153SGreg Roach     * @return void
14757514a4fSGreg Roach     */
14808df3d18SGreg Roach    private static function setSaveHandler()
149c1010edaSGreg Roach    {
15057514a4fSGreg Roach        session_set_save_handler(
15157514a4fSGreg Roach            function (): bool {
15257514a4fSGreg Roach                return Session::open();
15357514a4fSGreg Roach            },
15457514a4fSGreg Roach            function (): bool {
15557514a4fSGreg Roach                return Session::close();
15657514a4fSGreg Roach            },
15757514a4fSGreg Roach            function (string $id): string {
15857514a4fSGreg Roach                return Session::read($id);
15957514a4fSGreg Roach            },
16057514a4fSGreg Roach            function (string $id, string $data): bool {
16157514a4fSGreg Roach                return Session::write($id, $data);
16257514a4fSGreg Roach            },
16357514a4fSGreg Roach            function (string $id): bool {
16457514a4fSGreg Roach                return Session::destroy($id);
16557514a4fSGreg Roach            },
16657514a4fSGreg Roach            function (int $maxlifetime): bool {
16757514a4fSGreg Roach                return Session::gc($maxlifetime);
16857514a4fSGreg Roach            }
16957514a4fSGreg Roach        );
17057514a4fSGreg Roach    }
17157514a4fSGreg Roach
17257514a4fSGreg Roach    /**
17357514a4fSGreg Roach     * For session_set_save_handler()
17457514a4fSGreg Roach     *
17557514a4fSGreg Roach     * @return bool
17657514a4fSGreg Roach     */
1778f53f488SRico Sonntag    private static function close(): bool
178c1010edaSGreg Roach    {
17957514a4fSGreg Roach        return true;
18057514a4fSGreg Roach    }
18157514a4fSGreg Roach
18257514a4fSGreg Roach    /**
18357514a4fSGreg Roach     * For session_set_save_handler()
18457514a4fSGreg Roach     *
18557514a4fSGreg Roach     * @param string $id
18657514a4fSGreg Roach     *
18757514a4fSGreg Roach     * @return bool
18857514a4fSGreg Roach     */
1898f53f488SRico Sonntag    private static function destroy(string $id): bool
190c1010edaSGreg Roach    {
19157514a4fSGreg Roach        Database::prepare(
19257514a4fSGreg Roach            "DELETE FROM `##session` WHERE session_id = :session_id"
19357514a4fSGreg Roach        )->execute([
194c1010edaSGreg Roach            'session_id' => $id,
19557514a4fSGreg Roach        ]);
19657514a4fSGreg Roach
19757514a4fSGreg Roach        return true;
19857514a4fSGreg Roach    }
19957514a4fSGreg Roach
20057514a4fSGreg Roach    /**
20157514a4fSGreg Roach     * For session_set_save_handler()
20257514a4fSGreg Roach     *
20357514a4fSGreg Roach     * @param int $maxlifetime
20457514a4fSGreg Roach     *
20557514a4fSGreg Roach     * @return bool
20657514a4fSGreg Roach     */
2078f53f488SRico Sonntag    private static function gc(int $maxlifetime): bool
208c1010edaSGreg Roach    {
20957514a4fSGreg Roach        Database::prepare(
21057514a4fSGreg Roach            "DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL :maxlifetime SECOND)"
21157514a4fSGreg Roach        )->execute([
212c1010edaSGreg Roach            'maxlifetime' => $maxlifetime,
21357514a4fSGreg Roach        ]);
21457514a4fSGreg Roach
21557514a4fSGreg Roach        return true;
21657514a4fSGreg Roach    }
21757514a4fSGreg Roach
21857514a4fSGreg Roach    /**
21957514a4fSGreg Roach     * For session_set_save_handler()
22057514a4fSGreg Roach     *
22157514a4fSGreg Roach     * @return bool
22257514a4fSGreg Roach     */
2238f53f488SRico Sonntag    private static function open(): bool
224c1010edaSGreg Roach    {
22557514a4fSGreg Roach        return true;
22657514a4fSGreg Roach    }
22757514a4fSGreg Roach
22857514a4fSGreg Roach    /**
22957514a4fSGreg Roach     * For session_set_save_handler()
23057514a4fSGreg Roach     *
23157514a4fSGreg Roach     * @param string $id
23257514a4fSGreg Roach     *
23357514a4fSGreg Roach     * @return string
23457514a4fSGreg Roach     */
235c1010edaSGreg Roach    private static function read(string $id): string
236c1010edaSGreg Roach    {
23757514a4fSGreg Roach        return (string) Database::prepare(
23857514a4fSGreg Roach            "SELECT session_data FROM `##session` WHERE session_id = :session_id"
23957514a4fSGreg Roach        )->execute([
240c1010edaSGreg Roach            'session_id' => $id,
24157514a4fSGreg Roach        ])->fetchOne();
24257514a4fSGreg Roach    }
24357514a4fSGreg Roach
24457514a4fSGreg Roach    /**
24557514a4fSGreg Roach     * For session_set_save_handler()
24657514a4fSGreg Roach     *
24757514a4fSGreg Roach     * @param string $id
24857514a4fSGreg Roach     * @param string $data
24957514a4fSGreg Roach     *
25057514a4fSGreg Roach     * @return bool
25157514a4fSGreg Roach     */
252c1010edaSGreg Roach    private static function write(string $id, string $data): bool
253c1010edaSGreg Roach    {
2544c891c40SGreg Roach        $request = Request::createFromGlobals();
2554c891c40SGreg Roach
25657514a4fSGreg Roach        // Only update the session table once per minute, unless the session data has actually changed.
25757514a4fSGreg Roach        Database::prepare(
25857514a4fSGreg Roach            "INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" .
2594c891c40SGreg Roach            " VALUES (:session_id, :user_id, :ip_address, :data, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" .
26057514a4fSGreg Roach            " ON DUPLICATE KEY UPDATE" .
26157514a4fSGreg Roach            " user_id      = VALUES(user_id)," .
26257514a4fSGreg Roach            " ip_address   = VALUES(ip_address)," .
26357514a4fSGreg Roach            " session_data = VALUES(session_data)," .
26457514a4fSGreg Roach            " session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)"
26557514a4fSGreg Roach        )->execute([
2664c891c40SGreg Roach            'session_id' => $id,
2674c891c40SGreg Roach            'user_id'    => (int) Auth::id(),
2684c891c40SGreg Roach            'ip_address' => $request->getClientIp(),
2694c891c40SGreg Roach            'data'       => $data,
2704c891c40SGreg Roach        ]);
27157514a4fSGreg Roach
27257514a4fSGreg Roach        return true;
27357514a4fSGreg Roach    }
274a45f9889SGreg Roach
275a45f9889SGreg Roach
276a45f9889SGreg Roach    /**
277a45f9889SGreg Roach     * Cross-Site Request Forgery tokens - ensure that the user is submitting
278a45f9889SGreg Roach     * a form that was generated by the current session.
279a45f9889SGreg Roach     *
280a45f9889SGreg Roach     * @return string
281a45f9889SGreg Roach     */
2828f53f488SRico Sonntag    public static function getCsrfToken(): string
283a45f9889SGreg Roach    {
284a45f9889SGreg Roach        if (!Session::has('CSRF_TOKEN')) {
285a45f9889SGreg Roach            $charset    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789';
286a45f9889SGreg Roach            $csrf_token = '';
287a45f9889SGreg Roach            for ($n = 0; $n < 32; ++$n) {
288a45f9889SGreg Roach                $csrf_token .= substr($charset, random_int(0, 61), 1);
289a45f9889SGreg Roach            }
290a45f9889SGreg Roach            Session::put('CSRF_TOKEN', $csrf_token);
291a45f9889SGreg Roach        }
292a45f9889SGreg Roach
293a45f9889SGreg Roach        return Session::get('CSRF_TOKEN');
294a45f9889SGreg Roach    }
29531bc7874SGreg Roach}
296