xref: /webtrees/app/Session.php (revision 3e983931fdde6db78f1490364106d7d46e77dea7)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2017 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		if (isset($_SESSION[$name])) {
57			return $_SESSION[$name];
58		} else {
59			return $default;
60		}
61	}
62
63	/**
64	 * Write a value to the session
65	 *
66	 * @param string $name
67	 * @param mixed  $value
68	 */
69	public static function put($name, $value) {
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		unset($_SESSION[$name]);
80	}
81
82	/**
83	 * Does a session variable exist?
84	 *
85	 * @param string $name
86	 *
87	 * @return bool
88	 */
89	public static function has($name) {
90		return isset($_SESSION[$name]);
91	}
92
93	/**
94	 * Remove all stored data from the session.
95	 */
96	public static function clear() {
97		$_SESSION = [];
98	}
99
100	/**
101	 * After any change in authentication level, we should use a new session ID.
102	 *
103	 * @param bool $destroy
104	 */
105	public static function regenerate($destroy = false) {
106		if ($destroy) {
107			self::clear();
108		}
109		session_regenerate_id($destroy);
110	}
111
112	/**
113	 * Set an explicit session ID. Typically used for search robots.
114	 *
115	 * @param string $id
116	 */
117	public static function setId($id) {
118		session_id($id);
119	}
120
121	/**
122	 * Initialise our session save handler
123	 */
124	public static function setSaveHandler() {
125		session_set_save_handler(
126			function (): bool {
127				return Session::open();
128			},
129			function ():bool {
130				return Session::close();
131			},
132			function (string $id): string {
133				return Session::read($id);
134			},
135			function (string $id, string $data): bool {
136				return Session::write($id, $data);
137			},
138			function (string $id): bool {
139				return Session::destroy($id);
140			},
141			function (int $maxlifetime):bool {
142				return Session::gc($maxlifetime);
143			}
144		);
145	}
146
147	/**
148	 * For session_set_save_handler()
149	 *
150	 * @return bool
151	 */
152	private static function close() {
153		return true;
154	}
155
156	/**
157	 * For session_set_save_handler()
158	 *
159	 * @param string $id
160	 *
161	 * @return bool
162	 */
163	private static function destroy(string $id) {
164		Database::prepare(
165			"DELETE FROM `##session` WHERE session_id = :session_id"
166		)->execute([
167			'session_id' => $id
168		]);
169
170		return true;
171	}
172
173	/**
174	 * For session_set_save_handler()
175	 *
176	 * @param int $maxlifetime
177	 *
178	 * @return bool
179	 */
180	private static function gc(int $maxlifetime) {
181		Database::prepare(
182			"DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL :maxlifetime SECOND)"
183		)->execute([
184			'maxlifetime' => $maxlifetime
185		]);
186
187		return true;
188	}
189
190	/**
191	 * For session_set_save_handler()
192	 *
193	 * @return bool
194	 */
195	private static function open() {
196		return true;
197	}
198
199	/**
200	 * For session_set_save_handler()
201	 *
202	 * @param string $id
203	 *
204	 * @return string
205	 */
206	private static function read(string $id): string {
207		return (string) Database::prepare(
208			"SELECT session_data FROM `##session` WHERE session_id = :session_id"
209		)->execute([
210			'session_id' => $id
211		])->fetchOne();
212	}
213
214	/**
215	 * For session_set_save_handler()
216	 *
217	 * @param string $id
218	 * @param string $data
219	 *
220	 * @return bool
221	 */
222	private static function write(string $id, string $data): bool {
223		$request = Request::createFromGlobals();
224
225		// Only update the session table once per minute, unless the session data has actually changed.
226		Database::prepare(
227			"INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" .
228			" VALUES (:session_id, :user_id, :ip_address, :data, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" .
229			" ON DUPLICATE KEY UPDATE" .
230			" user_id      = VALUES(user_id)," .
231			" ip_address   = VALUES(ip_address)," .
232			" session_data = VALUES(session_data)," .
233			" session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)"
234		)->execute([
235			'session_id' => $id,
236			'user_id'    => (int) Auth::id(),
237			'ip_address' => $request->getClientIp(),
238			'data'       => $data,
239		]);
240
241		return true;
242	}
243}
244