xref: /webtrees/app/Session.php (revision c7ff415313b94d2d6d06ec673249f39fae9865e9)
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 */
1676692c8bSGreg Roachnamespace Fisharebest\Webtrees;
1731bc7874SGreg Roach
184c891c40SGreg Roachuse Symfony\Component\HttpFoundation\Request;
194c891c40SGreg Roach
2031bc7874SGreg Roach/**
2142af74e7SGreg Roach * Session handling
2231bc7874SGreg Roach */
23c1010edaSGreg Roachclass Session
24c1010edaSGreg Roach{
2531bc7874SGreg Roach    /**
2631bc7874SGreg Roach     * Start a session
27*c7ff4153SGreg Roach     *
28*c7ff4153SGreg Roach     * @return void
2931bc7874SGreg Roach     */
3008df3d18SGreg Roach    public static function start()
31c1010edaSGreg Roach    {
3208df3d18SGreg Roach        $domain   = '';
3308df3d18SGreg Roach        $path     = parse_url(WT_BASE_URL, PHP_URL_PATH);
3408df3d18SGreg Roach        $secure   = parse_url(WT_BASE_URL, PHP_URL_SCHEME) === 'https';
3508df3d18SGreg Roach        $httponly = true;
3608df3d18SGreg Roach
3708df3d18SGreg Roach        // Paths containing UTF-8 characters need special handling.
3808df3d18SGreg Roach        $path = implode('/', array_map('rawurlencode', explode('/', $path)));
3908df3d18SGreg Roach
4008df3d18SGreg Roach        self::setSaveHandler();
4108df3d18SGreg Roach
4208df3d18SGreg Roach        session_name('WT_SESSION');
4331bc7874SGreg Roach        session_register_shutdown();
44a0dfa978SGreg Roach        session_set_cookie_params(0, $path, $domain, $secure, $httponly);
4531bc7874SGreg Roach        session_start();
4608df3d18SGreg Roach
4708df3d18SGreg Roach        // A new session? Prevent session fixation attacks by choosing a new session ID.
4808df3d18SGreg Roach        if (!self::get('initiated')) {
4908df3d18SGreg Roach            self::regenerate(true);
5008df3d18SGreg Roach            self::put('initiated', true);
5108df3d18SGreg Roach        }
5231bc7874SGreg Roach    }
5331bc7874SGreg Roach
5431bc7874SGreg Roach    /**
5531bc7874SGreg Roach     * Read a value from the session
5631bc7874SGreg Roach     *
5731bc7874SGreg Roach     * @param string $name
5831bc7874SGreg Roach     * @param mixed  $default
5931bc7874SGreg Roach     *
6031bc7874SGreg Roach     * @return mixed
6131bc7874SGreg Roach     */
62*c7ff4153SGreg Roach    public static function get(string $name, $default = null)
63c1010edaSGreg Roach    {
6463485653SRico Sonntag        return $_SESSION[$name] ?? $default;
6531bc7874SGreg Roach    }
6631bc7874SGreg Roach
6731bc7874SGreg Roach    /**
6831bc7874SGreg Roach     * Write a value to the session
6931bc7874SGreg Roach     *
7031bc7874SGreg Roach     * @param string $name
7131bc7874SGreg Roach     * @param mixed  $value
72*c7ff4153SGreg Roach     *
73*c7ff4153SGreg Roach     * @return void
7431bc7874SGreg Roach     */
75*c7ff4153SGreg Roach    public static function put(string $name, $value)
76c1010edaSGreg Roach    {
7731bc7874SGreg Roach        $_SESSION[$name] = $value;
7831bc7874SGreg Roach    }
7931bc7874SGreg Roach
8031bc7874SGreg Roach    /**
8131bc7874SGreg Roach     * Remove a value from the session
8231bc7874SGreg Roach     *
8331bc7874SGreg Roach     * @param string $name
84*c7ff4153SGreg Roach     *
85*c7ff4153SGreg Roach     * @return void
8631bc7874SGreg Roach     */
87*c7ff4153SGreg Roach    public static function forget(string $name)
88c1010edaSGreg Roach    {
8931bc7874SGreg Roach        unset($_SESSION[$name]);
9031bc7874SGreg Roach    }
9131bc7874SGreg Roach
9231bc7874SGreg Roach    /**
9331bc7874SGreg Roach     * Does a session variable exist?
9431bc7874SGreg Roach     *
9531bc7874SGreg Roach     * @param string $name
9631bc7874SGreg Roach     *
97cbc1590aSGreg Roach     * @return bool
9831bc7874SGreg Roach     */
99*c7ff4153SGreg Roach    public static function has(string $name): bool
100c1010edaSGreg Roach    {
10191fb15f0SGreg Roach        return isset($_SESSION[$name]);
10231bc7874SGreg Roach    }
10331bc7874SGreg Roach
10431bc7874SGreg Roach    /**
105f5004097SGreg Roach     * Remove all stored data from the session.
106*c7ff4153SGreg Roach     *
107*c7ff4153SGreg Roach     * @return void
108f5004097SGreg Roach     */
109c1010edaSGreg Roach    public static function clear()
110c1010edaSGreg Roach    {
11113abd6f3SGreg Roach        $_SESSION = [];
112f5004097SGreg Roach    }
113f5004097SGreg Roach
114f5004097SGreg Roach    /**
11531bc7874SGreg Roach     * After any change in authentication level, we should use a new session ID.
11631bc7874SGreg Roach     *
11731bc7874SGreg Roach     * @param bool $destroy
118*c7ff4153SGreg Roach     *
119*c7ff4153SGreg Roach     * @return void
12031bc7874SGreg Roach     */
121*c7ff4153SGreg Roach    public static function regenerate(bool $destroy = false)
122c1010edaSGreg Roach    {
123f5004097SGreg Roach        if ($destroy) {
124f5004097SGreg Roach            self::clear();
125f5004097SGreg Roach        }
12631bc7874SGreg Roach        session_regenerate_id($destroy);
12731bc7874SGreg Roach    }
12831bc7874SGreg Roach
12931bc7874SGreg Roach    /**
13031bc7874SGreg Roach     * Set an explicit session ID. Typically used for search robots.
13131bc7874SGreg Roach     *
13231bc7874SGreg Roach     * @param string $id
133*c7ff4153SGreg Roach     *
134*c7ff4153SGreg Roach     * @return void
13531bc7874SGreg Roach     */
136*c7ff4153SGreg Roach    public static function setId(string $id)
137c1010edaSGreg Roach    {
13831bc7874SGreg Roach        session_id($id);
13931bc7874SGreg Roach    }
14057514a4fSGreg Roach
14157514a4fSGreg Roach    /**
14257514a4fSGreg Roach     * Initialise our session save handler
143*c7ff4153SGreg Roach     *
144*c7ff4153SGreg Roach     * @return void
14557514a4fSGreg Roach     */
14608df3d18SGreg Roach    private static function setSaveHandler()
147c1010edaSGreg Roach    {
14857514a4fSGreg Roach        session_set_save_handler(
14957514a4fSGreg Roach            function (): bool {
15057514a4fSGreg Roach                return Session::open();
15157514a4fSGreg Roach            },
15257514a4fSGreg Roach            function (): bool {
15357514a4fSGreg Roach                return Session::close();
15457514a4fSGreg Roach            },
15557514a4fSGreg Roach            function (string $id): string {
15657514a4fSGreg Roach                return Session::read($id);
15757514a4fSGreg Roach            },
15857514a4fSGreg Roach            function (string $id, string $data): bool {
15957514a4fSGreg Roach                return Session::write($id, $data);
16057514a4fSGreg Roach            },
16157514a4fSGreg Roach            function (string $id): bool {
16257514a4fSGreg Roach                return Session::destroy($id);
16357514a4fSGreg Roach            },
16457514a4fSGreg Roach            function (int $maxlifetime): bool {
16557514a4fSGreg Roach                return Session::gc($maxlifetime);
16657514a4fSGreg Roach            }
16757514a4fSGreg Roach        );
16857514a4fSGreg Roach    }
16957514a4fSGreg Roach
17057514a4fSGreg Roach    /**
17157514a4fSGreg Roach     * For session_set_save_handler()
17257514a4fSGreg Roach     *
17357514a4fSGreg Roach     * @return bool
17457514a4fSGreg Roach     */
1758f53f488SRico Sonntag    private static function close(): bool
176c1010edaSGreg Roach    {
17757514a4fSGreg Roach        return true;
17857514a4fSGreg Roach    }
17957514a4fSGreg Roach
18057514a4fSGreg Roach    /**
18157514a4fSGreg Roach     * For session_set_save_handler()
18257514a4fSGreg Roach     *
18357514a4fSGreg Roach     * @param string $id
18457514a4fSGreg Roach     *
18557514a4fSGreg Roach     * @return bool
18657514a4fSGreg Roach     */
1878f53f488SRico Sonntag    private static function destroy(string $id): bool
188c1010edaSGreg Roach    {
18957514a4fSGreg Roach        Database::prepare(
19057514a4fSGreg Roach            "DELETE FROM `##session` WHERE session_id = :session_id"
19157514a4fSGreg Roach        )->execute([
192c1010edaSGreg Roach            'session_id' => $id,
19357514a4fSGreg Roach        ]);
19457514a4fSGreg Roach
19557514a4fSGreg Roach        return true;
19657514a4fSGreg Roach    }
19757514a4fSGreg Roach
19857514a4fSGreg Roach    /**
19957514a4fSGreg Roach     * For session_set_save_handler()
20057514a4fSGreg Roach     *
20157514a4fSGreg Roach     * @param int $maxlifetime
20257514a4fSGreg Roach     *
20357514a4fSGreg Roach     * @return bool
20457514a4fSGreg Roach     */
2058f53f488SRico Sonntag    private static function gc(int $maxlifetime): bool
206c1010edaSGreg Roach    {
20757514a4fSGreg Roach        Database::prepare(
20857514a4fSGreg Roach            "DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL :maxlifetime SECOND)"
20957514a4fSGreg Roach        )->execute([
210c1010edaSGreg Roach            'maxlifetime' => $maxlifetime,
21157514a4fSGreg Roach        ]);
21257514a4fSGreg Roach
21357514a4fSGreg Roach        return true;
21457514a4fSGreg Roach    }
21557514a4fSGreg Roach
21657514a4fSGreg Roach    /**
21757514a4fSGreg Roach     * For session_set_save_handler()
21857514a4fSGreg Roach     *
21957514a4fSGreg Roach     * @return bool
22057514a4fSGreg Roach     */
2218f53f488SRico Sonntag    private static function open(): bool
222c1010edaSGreg Roach    {
22357514a4fSGreg Roach        return true;
22457514a4fSGreg Roach    }
22557514a4fSGreg Roach
22657514a4fSGreg Roach    /**
22757514a4fSGreg Roach     * For session_set_save_handler()
22857514a4fSGreg Roach     *
22957514a4fSGreg Roach     * @param string $id
23057514a4fSGreg Roach     *
23157514a4fSGreg Roach     * @return string
23257514a4fSGreg Roach     */
233c1010edaSGreg Roach    private static function read(string $id): string
234c1010edaSGreg Roach    {
23557514a4fSGreg Roach        return (string) Database::prepare(
23657514a4fSGreg Roach            "SELECT session_data FROM `##session` WHERE session_id = :session_id"
23757514a4fSGreg Roach        )->execute([
238c1010edaSGreg Roach            'session_id' => $id,
23957514a4fSGreg Roach        ])->fetchOne();
24057514a4fSGreg Roach    }
24157514a4fSGreg Roach
24257514a4fSGreg Roach    /**
24357514a4fSGreg Roach     * For session_set_save_handler()
24457514a4fSGreg Roach     *
24557514a4fSGreg Roach     * @param string $id
24657514a4fSGreg Roach     * @param string $data
24757514a4fSGreg Roach     *
24857514a4fSGreg Roach     * @return bool
24957514a4fSGreg Roach     */
250c1010edaSGreg Roach    private static function write(string $id, string $data): bool
251c1010edaSGreg Roach    {
2524c891c40SGreg Roach        $request = Request::createFromGlobals();
2534c891c40SGreg Roach
25457514a4fSGreg Roach        // Only update the session table once per minute, unless the session data has actually changed.
25557514a4fSGreg Roach        Database::prepare(
25657514a4fSGreg Roach            "INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" .
2574c891c40SGreg Roach            " VALUES (:session_id, :user_id, :ip_address, :data, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" .
25857514a4fSGreg Roach            " ON DUPLICATE KEY UPDATE" .
25957514a4fSGreg Roach            " user_id      = VALUES(user_id)," .
26057514a4fSGreg Roach            " ip_address   = VALUES(ip_address)," .
26157514a4fSGreg Roach            " session_data = VALUES(session_data)," .
26257514a4fSGreg Roach            " session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)"
26357514a4fSGreg Roach        )->execute([
2644c891c40SGreg Roach            'session_id' => $id,
2654c891c40SGreg Roach            'user_id'    => (int) Auth::id(),
2664c891c40SGreg Roach            'ip_address' => $request->getClientIp(),
2674c891c40SGreg Roach            'data'       => $data,
2684c891c40SGreg Roach        ]);
26957514a4fSGreg Roach
27057514a4fSGreg Roach        return true;
27157514a4fSGreg Roach    }
272a45f9889SGreg Roach
273a45f9889SGreg Roach
274a45f9889SGreg Roach    /**
275a45f9889SGreg Roach     * Cross-Site Request Forgery tokens - ensure that the user is submitting
276a45f9889SGreg Roach     * a form that was generated by the current session.
277a45f9889SGreg Roach     *
278a45f9889SGreg Roach     * @return string
279a45f9889SGreg Roach     */
2808f53f488SRico Sonntag    public static function getCsrfToken(): string
281a45f9889SGreg Roach    {
282a45f9889SGreg Roach        if (!Session::has('CSRF_TOKEN')) {
283a45f9889SGreg Roach            $charset    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789';
284a45f9889SGreg Roach            $csrf_token = '';
285a45f9889SGreg Roach            for ($n = 0; $n < 32; ++$n) {
286a45f9889SGreg Roach                $csrf_token .= substr($charset, random_int(0, 61), 1);
287a45f9889SGreg Roach            }
288a45f9889SGreg Roach            Session::put('CSRF_TOKEN', $csrf_token);
289a45f9889SGreg Roach        }
290a45f9889SGreg Roach
291a45f9889SGreg Roach        return Session::get('CSRF_TOKEN');
292a45f9889SGreg Roach    }
29331bc7874SGreg Roach}
294