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 * Checks if the session hasn't been hijacked
83 *
84 * @author Art <a.molcanovas@gmail.com>
85 * @return boolean TRUE if the check has passed, FALSE if not and the
86 * session has been terminated.
87 */
88 protected function identityCheck() {
89 $token = self::getToken();
90
91 if(!\get($this->data[ALO_SESSION_FINGERPRINT])) {
92 $this->data[ALO_SESSION_FINGERPRINT] = $token;
93 \Log::debug('Session identity check passed');
94 } elseif($token !== $this->data[ALO_SESSION_FINGERPRINT]) {
95 \Log::debug('Session identity check failed');
96 $this->terminate();
97
98 return false;
99 }
100
101 return true;
102 }
103
104 /**
105 * Generates a session token
106 *
107 * @author Art <a.molcanovas@gmail.com>
108 * @return string
109 */
110 protected static function getToken() {
111 return md5('sЕss' . Security::getFingerprint() . 'ия');
112 }
113
114 /**
115 * Terminates the session
116 *
117 * @author Art <a.molcanovas@gmail.com>
118 * @return AbstractSession
119 */
120 function terminate() {
121 $this->save = false;
122 Cookie::delete(ALO_SESSION_COOKIE);
123 \Log::debug('Terminated session');
124
125 return $this;
126 }
127
128 /**
129 * Removes expired session keys
130 *
131 * @author Art <a.molcanovas@gmail.com>
132 * @return SQLSession
133 */
134 protected function removeExpired() {
135 if(isset($this->data[self::EXPIRE_KEY])) {
136 foreach($this->data[self::EXPIRE_KEY] as $k => $v) {
137 if($this->time > $v) {
138 unset($this->data[self::EXPIRE_KEY][$k], $this->data[$k]);
139 }
140 }
141 if(empty($this->data[self::EXPIRE_KEY])) {
142 unset($this->data[self::EXPIRE_KEY]);
143 }
144
145 \Log::debug('Removed expired session keys');
146 }
147
148 return $this;
149 }
150
151 /**
152 * Fetches session data
153 *
154 * @author Art <a.molcanovas@gmail.com>
155 * @return AbstractSession
156 */
157 abstract protected function fetch();
158
159 /**
160 * Refreshes the user's session token. This will have no effect unless you overwrite the token during runtime.
161 *
162 * @author Art <a.molcanovas@gmail.com>
163 * @return bool Whether the user passes the identity check after the token refresh. The session is terminated if
164 * the identity check fails.
165 */
166 function refreshToken() {
167 unset($this->data[ALO_SESSION_FINGERPRINT]);
168
169 return $this->identityCheck();
170 }
171
172 /**
173 * Returns the expected session token
174 *
175 * @author Art <a.molcanovas@gmail.com>
176 * @return string
177 */
178 function getTokenExpected() {
179 return self::getToken();
180 }
181
182 /**
183 * Returns the actual session token
184 *
185 * @author Art <a.molcanovas@gmail.com>
186 * @return string|null
187 */
188 function getTokenActual() {
189 return \get($this->data[ALO_SESSION_FINGERPRINT]);
190 }
191
192 /**
193 * Clears all session variables except for the token
194 *
195 * @author Art <a.molcanovas@gmail.com>
196 * @return AbstractSession
197 */
198 function clear() {
199 $token = \get($this->data[ALO_SESSION_FINGERPRINT]);
200 $this->data = [];
201
202 if($token) {
203 $this->data[ALO_SESSION_FINGERPRINT] = $token;
204 }
205
206 return $this;
207 }
208
209 /**
210 * Gets a session value
211 *
212 * @author Art <a.molcanovas@gmail.com>
213 *
214 * @param string $key The identifier
215 *
216 * @return mixed
217 */
218 function __get($key) {
219 return \get($this->data[$key]);
220 }
221
222 /**
223 * Sets a session value
224 *
225 * @author Art <a.molcanovas@gmail.com>
226 *
227 * @param string $key The identifier
228 * @param mixed $val The value
229 */
230 function __set($key, $val) {
231 $this->data[$key] = $val;
232 }
233
234 /**
235 * Force-calls the write method
236 *
237 * @author Art <a.molcanovas@gmail.com>
238 * @see AbstractSession::write()
239 * @return AbstractSession
240 */
241 function forceWrite() {
242 return $this->write();
243 }
244
245 /**
246 * Saves session data
247 *
248 * @author Art <a.molcanovas@gmail.com>
249 * @return AbstractSession
250 */
251 abstract protected function write();
252
253 /**
254 * Unsets a session key
255 *
256 * @author Art <a.molcanovas@gmail.com>
257 *
258 * @param string $key The session value's key
259 */
260 function __unset($key) {
261 $this->delete($key);
262 }
263
264 /**
265 * Deletes a session value
266 *
267 * @author Art <a.molcanovas@gmail.com>
268 *
269 * @param string|array $key The corresponding key or array of keys
270 *
271 * @return AbstractSession
272 */
273 function delete($key) {
274 if(is_array($key)) {
275 foreach($key as $k) {
276 unset($this->data[$k]);
277 }
278 } else {
279 \Log::debug('Removed session key ' . $key);
280 unset($this->data[$key]);
281 }
282
283 return $this;
284 }
285
286 /**
287 * Checks if a session key is set
288 *
289 * @author Art <a.molcanovas@gmail.com>
290 *
291 * @param string $key The key
292 *
293 * @return bool
294 */
295 function __isset($key) {
296 return isset($this->data[$key]);
297 }
298
299 /**
300 * Returns a string representation of the session data
301 *
302 * @author Art <a.molcanovas@gmail.com>
303 * @return string
304 */
305 function __toString() {
306 $r = 'ID: ' . $this->id . "\nData:";
307
308 foreach($this->data as $k => $v) {
309 echo "\n\t$k => $v";
310 }
311
312 return $r;
313 }
314
315 /**
316 * Returns all session data in an associative array
317 *
318 * @author Art <a.molcanovas@gmail.com>
319 * @return array
320 */
321 function getAll() {
322 return $this->data;
323 }
324
325 /**
326 * Returns the session ID
327 *
328 * @author Art <a.molcanovas@gmail.com>
329 * @return string
330 */
331 function getID() {
332 return $this->id;
333 }
334
335 /**
336 * Sets the session ID variable & the cookie
337 *
338 * @author Art <a.molcanovas@gmail.com>
339 * @return SQLSession
340 */
341 protected function setID() {
342 $c = \get($_COOKIE[ALO_SESSION_COOKIE]);
343
344 if($c && strlen($c) == 128) {
345 $this->id = $c;
346 } else {
347 $this->id = Security::getUniqid(self::HASH_ALGO, 'session');
348 }
349
350 \Log::debug('Session ID set to ' . $this->id);
351 Cookie::set(ALO_SESSION_COOKIE, $this->id, $this->time + ALO_SESSION_TIMEOUT, '/', '', false, true);
352
353 return $this;
354 }
355
356 /**
357 * Sets a session key to expire
358 *
359 * @author Art <a.molcanovas@gmail.com>
360 *
361 * @param string $key The key
362 * @param int $time Expiration time in seconds
363 *
364 * @return AbstractSession
365 */
366 function expire($key, $time) {
367 $e = &$this->data[self::EXPIRE_KEY][$key];
368 $e = $this->time + $time;
369
370 \Log::debug('Set the session key ' . $key . ' to expire in ' . $time . ' seconds');
371
372 return $this;
373 }
374
375 /**
376 * Saves session data if $this->save hasn't been changed to false
377 *
378 * @author Art <a.molcanovas@gmail.com>
379 */
380 function __destruct() {
381 if($this->save) {
382 $this->write();
383 }
384 }
385
386 }