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