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