1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @package CleverStyle Framework |
4
|
|
|
* @author Nazar Mokrynskyi <[email protected]> |
5
|
|
|
* @copyright Copyright (c) 2011-2016, Nazar Mokrynskyi |
6
|
|
|
* @license MIT License, see license.txt |
7
|
|
|
*/ |
8
|
|
|
namespace cs\Session; |
9
|
|
|
use |
10
|
|
|
cs\Config, |
11
|
|
|
cs\Event, |
12
|
|
|
cs\Language, |
13
|
|
|
cs\Page, |
14
|
|
|
cs\Request, |
15
|
|
|
cs\Response, |
16
|
|
|
cs\User; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* @method \cs\DB\_Abstract db() |
20
|
|
|
* @method \cs\DB\_Abstract db_prime() |
21
|
|
|
*/ |
22
|
|
|
trait Management { |
23
|
|
|
/** |
24
|
|
|
* Id of current session |
25
|
|
|
* |
26
|
|
|
* @var false|string |
27
|
|
|
*/ |
28
|
|
|
protected $session_id; |
29
|
|
|
/** |
30
|
|
|
* User id of current session |
31
|
|
|
* |
32
|
|
|
* @var int |
33
|
|
|
*/ |
34
|
|
|
protected $user_id; |
35
|
|
|
/** |
36
|
|
|
* @var bool |
37
|
|
|
*/ |
38
|
|
|
protected $is_admin; |
39
|
|
|
/** |
40
|
|
|
* @var bool |
41
|
|
|
*/ |
42
|
|
|
protected $is_user; |
43
|
|
|
/** |
44
|
|
|
* @var bool |
45
|
|
|
*/ |
46
|
|
|
protected $is_guest; |
47
|
|
|
/** |
48
|
|
|
* Use cookie as source of session id, load session |
49
|
|
|
*/ |
50
|
30 |
|
protected function init_session () { |
51
|
30 |
|
$Request = Request::instance(); |
52
|
|
|
/** |
53
|
|
|
* If session exists |
54
|
|
|
*/ |
55
|
30 |
|
if ($Request->cookie('session')) { |
56
|
2 |
|
$this->user_id = $this->load(); |
57
|
|
|
} |
58
|
30 |
|
$this->update_user_is(); |
59
|
30 |
|
} |
60
|
|
|
/** |
61
|
|
|
* Updates information about who is user accessed by methods ::guest() ::user() admin() |
62
|
|
|
*/ |
63
|
30 |
|
protected function update_user_is () { |
64
|
30 |
|
$this->is_guest = $this->user_id == User::GUEST_ID; |
65
|
30 |
|
$this->is_user = false; |
66
|
30 |
|
$this->is_admin = false; |
67
|
30 |
|
if ($this->is_guest) { |
68
|
30 |
|
return; |
69
|
|
|
} |
70
|
|
|
/** |
71
|
|
|
* Checking of user type |
72
|
|
|
*/ |
73
|
22 |
|
$groups = User::instance()->get_groups($this->user_id) ?: []; |
74
|
22 |
|
if (in_array(User::ADMIN_GROUP_ID, $groups)) { |
75
|
8 |
|
$this->is_admin = true; |
76
|
8 |
|
$this->is_user = true; |
77
|
16 |
|
} elseif (in_array(User::USER_GROUP_ID, $groups)) { |
78
|
16 |
|
$this->is_user = true; |
79
|
|
|
} |
80
|
22 |
|
} |
81
|
|
|
/** |
82
|
|
|
* Is admin |
83
|
|
|
* |
84
|
|
|
* @return bool |
85
|
|
|
*/ |
86
|
10 |
|
function admin () { |
87
|
10 |
|
return $this->is_admin; |
88
|
|
|
} |
89
|
|
|
/** |
90
|
|
|
* Is user |
91
|
|
|
* |
92
|
|
|
* @return bool |
93
|
|
|
*/ |
94
|
2 |
|
function user () { |
95
|
2 |
|
return $this->is_user; |
96
|
|
|
} |
97
|
|
|
/** |
98
|
|
|
* Is guest |
99
|
|
|
* |
100
|
|
|
* @return bool |
101
|
|
|
*/ |
102
|
8 |
|
function guest () { |
103
|
8 |
|
return $this->is_guest; |
104
|
|
|
} |
105
|
|
|
/** |
106
|
|
|
* Returns id of current session |
107
|
|
|
* |
108
|
|
|
* @return false|string |
109
|
|
|
*/ |
110
|
26 |
|
function get_id () { |
111
|
26 |
|
return $this->session_id ?: false; |
112
|
|
|
} |
113
|
|
|
/** |
114
|
|
|
* Returns user id of current session |
115
|
|
|
* |
116
|
|
|
* @return int |
117
|
|
|
*/ |
118
|
30 |
|
function get_user () { |
119
|
30 |
|
return $this->user_id; |
120
|
|
|
} |
121
|
|
|
/** |
122
|
|
|
* Returns session details by session id |
123
|
|
|
* |
124
|
|
|
* @param false|null|string $session_id If `null` - loaded from `$this->session_id`, and if that also empty - from cookies |
125
|
|
|
* |
126
|
|
|
* @return false|array |
127
|
|
|
*/ |
128
|
2 |
|
function get ($session_id) { |
129
|
2 |
|
$session_data = $this->get_internal($session_id); |
130
|
2 |
|
unset($session_data['data']); |
131
|
2 |
|
return $session_data; |
132
|
|
|
} |
133
|
|
|
/** |
134
|
|
|
* @param false|null|string $session_id |
135
|
|
|
* |
136
|
|
|
* @return false|array |
137
|
|
|
*/ |
138
|
4 |
|
protected function get_internal ($session_id) { |
139
|
4 |
|
if (!$session_id) { |
140
|
2 |
|
if (!$this->session_id) { |
141
|
2 |
|
$this->session_id = Request::instance()->cookie('session'); |
142
|
|
|
} |
143
|
2 |
|
$session_id = $this->session_id; |
144
|
|
|
} |
145
|
4 |
|
if (!is_md5($session_id)) { |
146
|
2 |
|
return false; |
147
|
|
|
} |
148
|
4 |
|
$data = $this->cache->get( |
149
|
|
|
$session_id, |
150
|
4 |
|
function () use ($session_id) { |
151
|
4 |
|
$data = $this->read($session_id); |
152
|
4 |
|
if (!$data || $data['expire'] <= time()) { |
153
|
2 |
|
return false; |
154
|
|
|
} |
155
|
4 |
|
$data['data'] = $data['data'] ?: []; |
156
|
4 |
|
return $data; |
157
|
4 |
|
} |
158
|
|
|
); |
159
|
4 |
|
return $this->is_good_session($data) ? $data : false; |
160
|
|
|
} |
161
|
|
|
/** |
162
|
|
|
* Check whether session was not expired, user agent and IP corresponds to what is expected and user is actually active |
163
|
|
|
* |
164
|
|
|
* @param mixed $session_data |
165
|
|
|
* |
166
|
|
|
* @return bool |
167
|
|
|
*/ |
168
|
4 |
|
protected function is_good_session ($session_data) { |
169
|
|
|
return |
170
|
4 |
|
isset($session_data['expire'], $session_data['user']) && |
171
|
4 |
|
$session_data['expire'] > time() && |
172
|
4 |
|
$this->is_user_active($session_data['user']); |
173
|
|
|
} |
174
|
|
|
/** |
175
|
|
|
* Whether session data belongs to current visitor (user agent, remote addr and ip check) |
176
|
|
|
* |
177
|
|
|
* @param string $session_id |
178
|
|
|
* @param string $user_agent |
179
|
|
|
* @param string $remote_addr |
180
|
|
|
* @param string $ip |
181
|
|
|
* |
182
|
|
|
* @return bool |
183
|
|
|
*/ |
184
|
2 |
|
function is_session_owner ($session_id, $user_agent, $remote_addr, $ip) { |
185
|
2 |
|
$session_data = $this->get($session_id); |
186
|
2 |
|
return $session_data ? $this->is_session_owner_internal($session_data, $user_agent, $remote_addr, $ip) : false; |
187
|
|
|
} |
188
|
|
|
/** |
189
|
|
|
* Whether session data belongs to current visitor (user agent, remote addr and ip check) |
190
|
|
|
* |
191
|
|
|
* @param array $session_data |
192
|
|
|
* @param string|null $user_agent |
193
|
|
|
* @param string|null $remote_addr |
194
|
|
|
* @param string|null $ip |
195
|
|
|
* |
196
|
|
|
* @return bool |
197
|
|
|
*/ |
198
|
2 |
|
protected function is_session_owner_internal ($session_data, $user_agent = null, $remote_addr = null, $ip = null) { |
199
|
|
|
/** |
200
|
|
|
* md5() as protection against timing attacks |
201
|
|
|
*/ |
202
|
2 |
|
if ($user_agent === null && $remote_addr === null && $ip === null) { |
203
|
2 |
|
$Request = Request::instance(); |
204
|
2 |
|
$user_agent = $Request->header('user-agent'); |
205
|
2 |
|
$remote_addr = $Request->remote_addr; |
206
|
2 |
|
$ip = $Request->ip; |
207
|
|
|
} |
208
|
|
|
return |
209
|
2 |
|
md5($session_data['user_agent']) == md5($user_agent) && |
210
|
|
|
( |
211
|
2 |
|
!Config::instance()->core['remember_user_ip'] || |
212
|
|
|
( |
213
|
2 |
|
md5($session_data['remote_addr']) == md5(ip2hex($remote_addr)) && |
214
|
2 |
|
md5($session_data['ip']) == md5(ip2hex($ip)) |
215
|
|
|
) |
216
|
|
|
); |
217
|
|
|
} |
218
|
|
|
/** |
219
|
|
|
* Load session by id and return id of session owner (user), update session expiration |
220
|
|
|
* |
221
|
|
|
* @param false|null|string $session_id If not specified - loaded from `$this->session_id`, and if that also empty - from cookies |
222
|
|
|
* |
223
|
|
|
* @return int User id |
224
|
|
|
*/ |
225
|
2 |
|
function load ($session_id = null) { |
226
|
2 |
|
$session_data = $this->get_internal($session_id); |
227
|
2 |
|
if (!$session_data || !$this->is_session_owner_internal($session_data)) { |
228
|
2 |
|
$this->add(User::GUEST_ID); |
229
|
2 |
|
return User::GUEST_ID; |
230
|
|
|
} |
231
|
|
|
/** |
232
|
|
|
* Updating last online time and ip |
233
|
|
|
*/ |
234
|
2 |
|
$Config = Config::instance(); |
235
|
2 |
|
$time = time(); |
236
|
2 |
|
if ($session_data['expire'] - $time < $Config->core['session_expire'] * $Config->core['update_ratio'] / 100) { |
237
|
2 |
|
$session_data['expire'] = $time + $Config->core['session_expire']; |
238
|
2 |
|
$this->update($session_data); |
239
|
2 |
|
$this->cache->set($session_data['id'], $session_data); |
240
|
2 |
|
Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true); |
241
|
|
|
} |
242
|
2 |
|
unset($session_data['data']); |
243
|
2 |
|
Event::instance()->fire( |
244
|
2 |
|
'System/Session/load', |
245
|
|
|
[ |
246
|
2 |
|
'session_data' => $session_data |
247
|
|
|
] |
248
|
|
|
); |
249
|
2 |
|
return $this->load_initialization($session_data['id'], $session_data['user']); |
250
|
|
|
} |
251
|
|
|
/** |
252
|
|
|
* Initialize session (set user id, session id and update who user is) |
253
|
|
|
* |
254
|
|
|
* @param string $session_id |
255
|
|
|
* @param int $user_id |
256
|
|
|
* |
257
|
|
|
* @return int User id |
258
|
|
|
*/ |
259
|
22 |
|
protected function load_initialization ($session_id, $user_id) { |
260
|
22 |
|
$this->session_id = $session_id; |
261
|
22 |
|
$this->user_id = $user_id; |
262
|
22 |
|
$this->update_user_is(); |
263
|
22 |
|
return $user_id; |
264
|
|
|
} |
265
|
|
|
/** |
266
|
|
|
* Whether profile is activated, not disabled and not blocked |
267
|
|
|
* |
268
|
|
|
* @param int $user |
269
|
|
|
* |
270
|
|
|
* @return bool |
271
|
|
|
*/ |
272
|
22 |
|
protected function is_user_active ($user) { |
273
|
|
|
/** |
274
|
|
|
* Optimization, more data requested than actually used here, because data will be requested later, and it would be nice to have that data cached |
275
|
|
|
*/ |
276
|
22 |
|
$data = User::instance()->get( |
277
|
|
|
[ |
278
|
22 |
|
'login', |
279
|
|
|
'username', |
280
|
|
|
'language', |
281
|
|
|
'timezone', |
282
|
|
|
'status', |
283
|
|
|
'block_until', |
284
|
|
|
'avatar' |
285
|
|
|
], |
286
|
|
|
$user |
287
|
|
|
); |
288
|
22 |
|
if (!$data) { |
289
|
2 |
|
return false; |
290
|
|
|
} |
291
|
22 |
|
$L = Language::prefix('system_profile_sign_in_'); |
292
|
22 |
|
$Page = Page::instance(); |
293
|
22 |
|
switch ($data['status']) { |
294
|
22 |
|
case User::STATUS_INACTIVE: |
295
|
|
|
/** |
296
|
|
|
* If user is disabled |
297
|
|
|
*/ |
298
|
4 |
|
$Page->warning($L->your_account_disabled); |
299
|
4 |
|
return false; |
300
|
22 |
|
case User::STATUS_NOT_ACTIVATED: |
301
|
|
|
/** |
302
|
|
|
* If user is not active |
303
|
|
|
*/ |
304
|
2 |
|
$Page->warning($L->your_account_is_not_active); |
305
|
2 |
|
return false; |
306
|
|
|
} |
307
|
22 |
|
if ($data['block_until'] > time()) { |
308
|
|
|
/** |
309
|
|
|
* If user if blocked |
310
|
|
|
*/ |
311
|
2 |
|
$Page->warning($L->your_account_blocked_until(date($L->_datetime, $data['block_until']))); |
312
|
2 |
|
return false; |
313
|
|
|
} |
314
|
22 |
|
return true; |
315
|
|
|
} |
316
|
|
|
/** |
317
|
|
|
* Create the session for the user with specified id |
318
|
|
|
* |
319
|
|
|
* @param int $user |
320
|
|
|
* @param bool $delete_current_session |
321
|
|
|
* |
322
|
|
|
* @return false|string Session id on success, `false` otherwise |
323
|
|
|
*/ |
324
|
22 |
|
function add ($user, $delete_current_session = true) { |
325
|
22 |
|
$user = (int)$user ?: User::GUEST_ID; |
326
|
22 |
|
if ($delete_current_session && is_md5($this->session_id)) { |
327
|
8 |
|
$this->del_internal($this->session_id, false); |
328
|
|
|
} |
329
|
22 |
|
if (!$this->is_user_active($user)) { |
330
|
|
|
/** |
331
|
|
|
* If data was not loaded or account is not active - create guest session |
332
|
|
|
*/ |
333
|
4 |
|
return $this->add(User::GUEST_ID); |
334
|
|
|
} |
335
|
22 |
|
$session_data = $this->create_unique_session($user); |
336
|
22 |
|
Response::instance()->cookie('session', $session_data['id'], $session_data['expire'], true); |
337
|
22 |
|
$this->load_initialization($session_data['id'], $session_data['user']); |
338
|
|
|
/** |
339
|
|
|
* Delete old sessions using probability and system configuration of inserts limits and update ratio |
340
|
|
|
*/ |
341
|
22 |
|
$Config = Config::instance(); |
342
|
22 |
|
if (mt_rand(0, $Config->core['inserts_limit']) < $Config->core['inserts_limit'] / 100 * (100 - $Config->core['update_ratio']) / 5) { |
343
|
2 |
|
$this->delete_old_sessions(); |
344
|
|
|
} |
345
|
22 |
|
Event::instance()->fire( |
346
|
22 |
|
'System/Session/add', |
347
|
|
|
[ |
348
|
22 |
|
'session_data' => $session_data |
349
|
|
|
] |
350
|
|
|
); |
351
|
22 |
|
return $session_data['id']; |
352
|
|
|
} |
353
|
|
|
/** |
354
|
|
|
* @param int $user |
355
|
|
|
* |
356
|
|
|
* @return array Session data |
357
|
|
|
*/ |
358
|
22 |
|
protected function create_unique_session ($user) { |
359
|
22 |
|
$Config = Config::instance(); |
360
|
22 |
|
$Request = Request::instance(); |
361
|
22 |
|
$remote_addr = ip2hex($Request->remote_addr); |
362
|
22 |
|
$ip = ip2hex($Request->ip); |
363
|
|
|
/** |
364
|
|
|
* Many guests open only one page (or do not store any cookies), so create guest session only for 5 minutes max initially |
365
|
|
|
*/ |
366
|
22 |
|
$expire_in = $user == User::GUEST_ID ? min($Config->core['session_expire'], self::INITIAL_SESSION_EXPIRATION) : $Config->core['session_expire']; |
367
|
22 |
|
$expire = time() + $expire_in; |
368
|
|
|
/** |
369
|
|
|
* Create unique session |
370
|
|
|
*/ |
371
|
|
|
$session_data = [ |
372
|
22 |
|
'id' => null, |
373
|
22 |
|
'user' => $user, |
374
|
22 |
|
'created' => time(), |
375
|
22 |
|
'expire' => $expire, |
376
|
22 |
|
'user_agent' => $Request->header('user-agent'), |
377
|
22 |
|
'remote_addr' => $remote_addr, |
378
|
22 |
|
'ip' => $ip, |
379
|
|
|
'data' => [] |
380
|
|
|
]; |
381
|
|
|
do { |
382
|
22 |
|
$session_data['id'] = md5(random_bytes(1000)); |
383
|
22 |
|
} while (!$this->create($session_data)); |
384
|
22 |
|
return $session_data; |
385
|
|
|
} |
386
|
|
|
/** |
387
|
|
|
* Destroying of the session |
388
|
|
|
* |
389
|
|
|
* @param null|string $session_id |
390
|
|
|
* |
391
|
|
|
* @return bool |
392
|
|
|
*/ |
393
|
8 |
|
function del ($session_id = null) { |
394
|
8 |
|
return (bool)$this->del_internal($session_id); |
395
|
|
|
} |
396
|
|
|
/** |
397
|
|
|
* Deletion of the session |
398
|
|
|
* |
399
|
|
|
* @param string|null $session_id |
400
|
|
|
* @param bool $create_guest_session |
401
|
|
|
* |
402
|
|
|
* @return bool |
403
|
|
|
*/ |
404
|
10 |
|
protected function del_internal ($session_id = null, $create_guest_session = true) { |
405
|
10 |
|
$session_id = $session_id ?: $this->session_id; |
406
|
10 |
|
if (!is_md5($session_id)) { |
407
|
2 |
|
return false; |
408
|
|
|
} |
409
|
10 |
|
Event::instance()->fire( |
410
|
10 |
|
'System/Session/del/before', |
411
|
|
|
[ |
412
|
10 |
|
'id' => $session_id |
413
|
|
|
] |
414
|
|
|
); |
415
|
10 |
|
unset($this->cache->$session_id); |
416
|
10 |
|
if ($session_id == $this->session_id) { |
417
|
10 |
|
$this->session_id = false; |
418
|
10 |
|
$this->user_id = User::GUEST_ID; |
419
|
|
|
} |
420
|
10 |
|
Response::instance()->cookie('session', ''); |
421
|
10 |
|
$result = $this->delete($session_id); |
422
|
10 |
|
if ($result) { |
423
|
10 |
|
if ($create_guest_session) { |
424
|
8 |
|
return (bool)$this->add(User::GUEST_ID); |
425
|
|
|
} |
426
|
8 |
|
Event::instance()->fire( |
427
|
8 |
|
'System/Session/del/after', |
428
|
|
|
[ |
429
|
8 |
|
'id' => $session_id |
430
|
|
|
] |
431
|
|
|
); |
432
|
|
|
} |
433
|
8 |
|
return (bool)$result; |
434
|
|
|
} |
435
|
|
|
/** |
436
|
|
|
* Delete all old sessions from DB |
437
|
|
|
*/ |
438
|
2 |
|
protected function delete_old_sessions () { |
439
|
2 |
|
$this->db_prime()->q( |
440
|
|
|
"DELETE FROM `[prefix]sessions` |
|
|
|
|
441
|
2 |
|
WHERE `expire` < ".time() |
442
|
|
|
); |
443
|
2 |
|
} |
444
|
|
|
/** |
445
|
|
|
* Deletion of all user sessions |
446
|
|
|
* |
447
|
|
|
* @param false|int $user If not specified - current user assumed |
448
|
|
|
* |
449
|
|
|
* @return bool |
450
|
|
|
*/ |
451
|
16 |
|
function del_all ($user = false) { |
452
|
16 |
|
$user = $user ?: $this->user_id; |
453
|
16 |
|
if ($user == User::GUEST_ID) { |
454
|
2 |
|
return false; |
455
|
|
|
} |
456
|
16 |
|
Event::instance()->fire( |
457
|
16 |
|
'System/Session/del_all', |
458
|
|
|
[ |
459
|
16 |
|
'id' => $user |
460
|
|
|
] |
461
|
|
|
); |
462
|
16 |
|
$sessions = $this->db_prime()->qfas( |
463
|
|
|
"SELECT `id` |
464
|
|
|
FROM `[prefix]sessions` |
465
|
16 |
|
WHERE `user` = '$user'" |
466
|
|
|
); |
467
|
16 |
|
foreach ($sessions ?: [] as $session) { |
468
|
6 |
|
if (!$this->del($session)) { |
469
|
6 |
|
return false; |
470
|
|
|
} |
471
|
|
|
} |
472
|
16 |
|
return true; |
473
|
|
|
} |
474
|
|
|
} |
475
|
|
|
|
PHP provides two ways to mark string literals. Either with single quotes
'literal'
or with double quotes"literal"
. The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (
\'
) and the backslash (\\
). Every other character is displayed as is.Double quoted string literals may contain other variables or more complex escape sequences.
will print an indented:
Single is Value
If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.
For more information on PHP string literals and available escape sequences see the PHP core documentation.