xref: /webtrees/app/Session.php (revision c1010eda29c0909ed4d5d463f32d32bfefdd4dfe)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2018 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 */
16namespace Fisharebest\Webtrees;
17
18use Symfony\Component\HttpFoundation\Request;
19
20/**
21 * Session handling
22 */
23class Session
24{
25    /**
26     * Start a session
27     *
28     * @param array $config
29     */
30    public static function start(array $config = [])
31    {
32        $default_config = [
33            'use_cookies'     => '1',
34            'name'            => 'WT_SESSION',
35            'cookie_lifetime' => '0',
36            'gc_maxlifetime'  => '7200',
37            'gc_probability'  => '1',
38            'gc_divisor'      => '100',
39            'cookie_path'     => '',
40            'cookie_httponly' => '1',
41        ];
42        session_register_shutdown();
43        foreach ($config + $default_config as $key => $value) {
44            ini_set('session.' . $key, $value);
45        }
46        session_start();
47    }
48
49    /**
50     * Read a value from the session
51     *
52     * @param string $name
53     * @param mixed  $default
54     *
55     * @return mixed
56     */
57    public static function get($name, $default = null)
58    {
59        return $_SESSION[$name] ?? $default;
60    }
61
62    /**
63     * Write a value to the session
64     *
65     * @param string $name
66     * @param mixed  $value
67     */
68    public static function put($name, $value)
69    {
70        $_SESSION[$name] = $value;
71    }
72
73    /**
74     * Remove a value from the session
75     *
76     * @param string $name
77     */
78    public static function forget($name)
79    {
80        unset($_SESSION[$name]);
81    }
82
83    /**
84     * Does a session variable exist?
85     *
86     * @param string $name
87     *
88     * @return bool
89     */
90    public static function has($name)
91    {
92        return isset($_SESSION[$name]);
93    }
94
95    /**
96     * Remove all stored data from the session.
97     */
98    public static function clear()
99    {
100        $_SESSION = [];
101    }
102
103    /**
104     * After any change in authentication level, we should use a new session ID.
105     *
106     * @param bool $destroy
107     */
108    public static function regenerate($destroy = false)
109    {
110        if ($destroy) {
111            self::clear();
112        }
113        session_regenerate_id($destroy);
114    }
115
116    /**
117     * Set an explicit session ID. Typically used for search robots.
118     *
119     * @param string $id
120     */
121    public static function setId($id)
122    {
123        session_id($id);
124    }
125
126    /**
127     * Initialise our session save handler
128     */
129    public static function setSaveHandler()
130    {
131        session_set_save_handler(
132            function (): bool {
133                return Session::open();
134            },
135            function (): bool {
136                return Session::close();
137            },
138            function (string $id): string {
139                return Session::read($id);
140            },
141            function (string $id, string $data): bool {
142                return Session::write($id, $data);
143            },
144            function (string $id): bool {
145                return Session::destroy($id);
146            },
147            function (int $maxlifetime): bool {
148                return Session::gc($maxlifetime);
149            }
150        );
151    }
152
153    /**
154     * For session_set_save_handler()
155     *
156     * @return bool
157     */
158    private static function close()
159    {
160        return true;
161    }
162
163    /**
164     * For session_set_save_handler()
165     *
166     * @param string $id
167     *
168     * @return bool
169     */
170    private static function destroy(string $id)
171    {
172        Database::prepare(
173            "DELETE FROM `##session` WHERE session_id = :session_id"
174        )->execute([
175            'session_id' => $id,
176        ]);
177
178        return true;
179    }
180
181    /**
182     * For session_set_save_handler()
183     *
184     * @param int $maxlifetime
185     *
186     * @return bool
187     */
188    private static function gc(int $maxlifetime)
189    {
190        Database::prepare(
191            "DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL :maxlifetime SECOND)"
192        )->execute([
193            'maxlifetime' => $maxlifetime,
194        ]);
195
196        return true;
197    }
198
199    /**
200     * For session_set_save_handler()
201     *
202     * @return bool
203     */
204    private static function open()
205    {
206        return true;
207    }
208
209    /**
210     * For session_set_save_handler()
211     *
212     * @param string $id
213     *
214     * @return string
215     */
216    private static function read(string $id): string
217    {
218        return (string)Database::prepare(
219            "SELECT session_data FROM `##session` WHERE session_id = :session_id"
220        )->execute([
221            'session_id' => $id,
222        ])->fetchOne();
223    }
224
225    /**
226     * For session_set_save_handler()
227     *
228     * @param string $id
229     * @param string $data
230     *
231     * @return bool
232     */
233    private static function write(string $id, string $data): bool
234    {
235        $request = Request::createFromGlobals();
236
237        // Only update the session table once per minute, unless the session data has actually changed.
238        Database::prepare(
239            "INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" .
240            " VALUES (:session_id, :user_id, :ip_address, :data, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" .
241            " ON DUPLICATE KEY UPDATE" .
242            " user_id      = VALUES(user_id)," .
243            " ip_address   = VALUES(ip_address)," .
244            " session_data = VALUES(session_data)," .
245            " session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)"
246        )->execute([
247            'session_id' => $id,
248            'user_id'    => (int)Auth::id(),
249            'ip_address' => $request->getClientIp(),
250            'data'       => $data,
251        ]);
252
253        return true;
254    }
255}
256