1 <?php
2
3 namespace Alo\Session;
4
5 use Alo\Statics\Cookie;
6 use Alo\Statics\Security;
7
8 if (!defined('GEN_START')) {
9 http_response_code(404);
10 die();
11 }
12 \Alo::loadConfig('session');
13
14 /**
15 * The session interface
16 *
17 * @author Art <a.molcanovas@gmail.com>
18 * @package Session
19 */
20 abstract class AbstractSession {
21
22 /**
23 * Hash algorithm in use
24 *
25 * @var string
26 */
27 const HASH_ALGO = 'sha512';
28
29 /**
30 * Session key under which key expiration data is stored
31 *
32 * @var string
33 */
34 const EXPIRE_KEY = '__expire';
35
36 /**
37 * The data array
38 *
39 * @var array
40 */
41 protected $data;
42
43 /**
44 * Whether to save session data
45 *
46 * @var boolean
47 */
48 protected $save;
49
50 /**
51 * Value of time()
52 *
53 * @var int
54 */
55 protected $time;
56
57 /**
58 * The session ID
59 *
60 * @var string
61 */
62 protected $id;
63
64 /**
65 * Instantiates the class
66 *
67 * @author Art <a.molcanovas@gmail.com>
68 */
69 function __construct() {
70 $this->data = [];
71 $this->time = time();
72 $this->save = true;
73
74 $this->setID();
75
76 if (\Alo::$router->is_cli_request() || $this->identityCheck()) {
77 $this->fetch()->removeExpired();
78 }
79 }
80
81 /**
82 * Saves session data
83 *
84 * @author Art <a.molcanovas@gmail.com>
85 * @return AbstractSession
86 */
87 abstract protected function write();
88
89 /**
90 * Sets the session ID variable & the cookie
91 *
92 * @author Art <a.molcanovas@gmail.com>
93 * @return SQLSession
94 */
95 protected function setID() {
96 $c = \get($_COOKIE[ALO_SESSION_COOKIE]);
97
98 if ($c && strlen($c) == 128) {
99 $this->id = $c;
100 } else {
101 $this->id = Security::getUniqid(self::HASH_ALGO, 'session');
102 }
103
104 \Log::debug('Session ID set to ' . $this->id);
105 Cookie::set(ALO_SESSION_COOKIE, $this->id, $this->time + ALO_SESSION_TIMEOUT, '/', '', false, true);
106
107 return $this;
108 }
109
110 /**
111 * Fetches session data
112 *
113 * @author Art <a.molcanovas@gmail.com>
114 * @return AbstractSession
115 */
116 abstract protected function fetch();
117
118 /**
119 * Removes expired session keys
120 *
121 * @author Art <a.molcanovas@gmail.com>
122 * @return SQLSession
123 */
124 protected function removeExpired() {
125 if (isset($this->data[self::EXPIRE_KEY])) {
126 foreach ($this->data[self::EXPIRE_KEY] as $k => $v) {
127 if ($this->time > $v) {
128 unset($this->data[self::EXPIRE_KEY][$k], $this->data[$k]);
129 }
130 }
131 if (empty($this->data[self::EXPIRE_KEY])) {
132 unset($this->data[self::EXPIRE_KEY]);
133 }
134
135 \Log::debug('Removed expired session keys');
136 }
137
138 return $this;
139 }
140
141 /**
142 * Checks if the session hasn't been hijacked
143 *
144 * @author Art <a.molcanovas@gmail.com>
145 * @return boolean TRUE if the check has passed, FALSE if not and the
146 * session has been terminated.
147 */
148 protected function identityCheck() {
149 $token = self::getToken();
150
151 if (!\get($this->data[ALO_SESSION_FINGERPRINT])) {
152 $this->data[ALO_SESSION_FINGERPRINT] = $token;
153 \Log::debug('Session identity check passed');
154 } elseif ($token !== $this->data[ALO_SESSION_FINGERPRINT]) {
155 \Log::debug('Session identity check failed');
156 $this->terminate();
157
158 return false;
159 }
160
161 return true;
162 }
163
164 /**
165 * Refreshes the user's session token. This will have no effect unless you overwrite the token during runtime.
166 *
167 * @author Art <a.molcanovas@gmail.com>
168 * @return bool Whether the user passes the identity check after the token refresh. The session is terminated if
169 * the identity check fails.
170 */
171 function refreshToken() {
172 unset($this->data[ALO_SESSION_FINGERPRINT]);
173
174 return $this->identityCheck();
175 }
176
177 /**
178 * Generates a session token
179 *
180 * @author Art <a.molcanovas@gmail.com>
181 * @return string
182 */
183 protected static function getToken() {
184 return md5('sЕss' . Security::getFingerprint() . 'ия');
185 }
186
187 /**
188 * Returns the expected session token
189 *
190 * @author Art <a.molcanovas@gmail.com>
191 * @return string
192 */
193 function getTokenExpected() {
194 return self::getToken();
195 }
196
197 /**
198 * Returns the actual session token
199 *
200 * @author Art <a.molcanovas@gmail.com>
201 * @return string|null
202 */
203 function getTokenActual() {
204 return \get($this->data[ALO_SESSION_FINGERPRINT]);
205 }
206
207 /**
208 * Clears all session variables except for the token
209 *
210 * @author Art <a.molcanovas@gmail.com>
211 * @return AbstractSession
212 */
213 function clear() {
214 $token = \get($this->data[ALO_SESSION_FINGERPRINT]);
215 $this->data = [];
216
217 if ($token) {
218 $this->data[ALO_SESSION_FINGERPRINT] = $token;
219 }
220
221 return $this;
222 }
223
224 /**
225 * Gets a session value
226 *
227 * @author Art <a.molcanovas@gmail.com>
228 * @param string $key The identifier
229 * @return mixed
230 */
231 function __get($key) {
232 return \get($this->data[$key]);
233 }
234
235 /**
236 * Force-calls the write method
237 *
238 * @author Art <a.molcanovas@gmail.com>
239 * @see AbstractSession::write()
240 * @return AbstractSession
241 */
242 function forceWrite() {
243 return $this->write();
244 }
245
246 /**
247 * Sets a session value
248 *
249 * @author Art <a.molcanovas@gmail.com>
250 * @param string $key The identifier
251 * @param mixed $val The value
252 */
253 function __set($key, $val) {
254 $this->data[$key] = $val;
255 }
256
257 /**
258 * Unsets a session key
259 *
260 * @author Art <a.molcanovas@gmail.com>
261 * @param string $key The session value's key
262 */
263 function __unset($key) {
264 $this->delete($key);
265 }
266
267 /**
268 * Checks if a session key is set
269 *
270 * @author Art <a.molcanovas@gmail.com>
271 * @param string $key The key
272 * @return bool
273 */
274 function __isset($key) {
275 return isset($this->data[$key]);
276 }
277
278 /**
279 * Returns a string representation of the session data
280 *
281 * @author Art <a.molcanovas@gmail.com>
282 * @return string
283 */
284 function __toString() {
285 $r = 'ID: ' . $this->id . "\nData:";
286
287 foreach ($this->data as $k => $v) {
288 echo "\n\t$k => $v";
289 }
290
291 return $r;
292 }
293
294 /**
295 * Deletes a session value
296 *
297 * @author Art <a.molcanovas@gmail.com>
298 * @param string|array $key The corresponding key or array of keys
299 * @return AbstractSession
300 */
301 function delete($key) {
302 if (is_array($key)) {
303 foreach ($key as $k) {
304 unset($this->data[$k]);
305 }
306 } else {
307 \Log::debug('Removed session key ' . $key);
308 unset($this->data[$key]);
309 }
310
311 return $this;
312 }
313
314 /**
315 * Returns all session data in an associative array
316 *
317 * @author Art <a.molcanovas@gmail.com>
318 * @return array
319 */
320 function getAll() {
321 return $this->data;
322 }
323
324 /**
325 * Returns the session ID
326 *
327 * @author Art <a.molcanovas@gmail.com>
328 * @return string
329 */
330 function getID() {
331 return $this->id;
332 }
333
334 /**
335 * Sets a session key to expire
336 *
337 * @author Art <a.molcanovas@gmail.com>
338 * @param string $key The key
339 * @param int $time Expiration time in seconds
340 * @return AbstractSession
341 */
342 function expire($key, $time) {
343 $e = &$this->data[self::EXPIRE_KEY][$key];
344 $e = $this->time + $time;
345
346 \Log::debug('Set the session key ' . $key . ' to expire in ' . $time . ' seconds');
347
348 return $this;
349 }
350
351 /**
352 * Terminates the session
353 *
354 * @author Art <a.molcanovas@gmail.com>
355 * @return AbstractSession
356 */
357 function terminate() {
358 $this->save = false;
359 Cookie::delete(ALO_SESSION_COOKIE);
360 \Log::debug('Terminated session');
361
362 return $this;
363 }
364
365 /**
366 * Saves session data if $this->save hasn't been changed to false
367 *
368 * @author Art <a.molcanovas@gmail.com>
369 */
370 function __destruct() {
371 if ($this->save) {
372 $this->write();
373 }
374 }
375
376 }