1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace phpMyFAQ; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* The main Session class. |
7
|
|
|
* |
8
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public License, |
9
|
|
|
* v. 2.0. If a copy of the MPL was not distributed with this file, You can |
10
|
|
|
* obtain one at http://mozilla.org/MPL/2.0/. |
11
|
|
|
* |
12
|
|
|
* @package phpMyFAQ |
13
|
|
|
* @author Thorsten Rinne <[email protected]> |
14
|
|
|
* @copyright 2007-2019 phpMyFAQ Team |
15
|
|
|
* @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 |
16
|
|
|
* @link https://www.phpmyfaq.de |
17
|
|
|
* @since 2007-03-31 |
18
|
|
|
*/ |
19
|
|
|
|
20
|
|
|
use phpMyFAQ\Configuration; |
21
|
|
|
use phpMyFAQ\Db; |
22
|
|
|
use phpMyFAQ\Exception; |
23
|
|
|
use phpMyFAQ\Filter; |
24
|
|
|
use phpMyFAQ\Network; |
25
|
|
|
|
26
|
|
|
if (!defined('IS_VALID_PHPMYFAQ')) { |
27
|
|
|
exit(); |
28
|
|
|
} |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Class Session. |
32
|
|
|
* |
33
|
|
|
* @package phpMyFAQ |
34
|
|
|
* @author Thorsten Rinne <[email protected]> |
35
|
|
|
* @copyright 2007-2019 phpMyFAQ Team |
36
|
|
|
* @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 |
37
|
|
|
* @link https://www.phpmyfaq.de |
38
|
|
|
* @since 2007-03-31 |
39
|
|
|
*/ |
40
|
|
|
class Session |
41
|
|
|
{ |
42
|
|
|
/** Constants. */ |
43
|
|
|
const PMF_COOKIE_NAME_REMEMBERME = 'pmf_rememberme'; |
44
|
|
|
const PMF_COOKIE_NAME_AUTH = 'pmf_auth'; |
45
|
|
|
const PMF_COOKIE_NAME_SESSIONID = 'pmf_sid'; |
46
|
|
|
|
47
|
|
|
/** @var Configuration */ |
48
|
|
|
private $config; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Constructor. |
52
|
|
|
* |
53
|
|
|
* @param Configuration |
54
|
|
|
*/ |
55
|
|
|
public function __construct(Configuration $config) |
56
|
|
|
{ |
57
|
|
|
$this->config = $config; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Tracks the user and log what he did. |
62
|
|
|
* |
63
|
|
|
* @param string $action Action string |
64
|
|
|
* @param string $data |
65
|
|
|
* |
66
|
|
|
* @throws Exception |
67
|
|
|
*/ |
68
|
|
|
public function userTracking(string $action, $data = null) |
69
|
|
|
{ |
70
|
|
|
global $sessionId, $user, $botBlacklist; |
71
|
|
|
|
72
|
|
|
if ($this->config->get('main.enableUserTracking')) { |
73
|
|
|
$bots = 0; |
74
|
|
|
$banned = false; |
75
|
|
|
$agent = $_SERVER['HTTP_USER_AGENT']; |
76
|
|
|
$sessionId = Filter::filterInput(INPUT_GET, PMF_GET_KEY_NAME_SESSIONID, FILTER_VALIDATE_INT); |
77
|
|
|
$cookieId = Filter::filterInput(INPUT_COOKIE, self::PMF_COOKIE_NAME_SESSIONID, FILTER_VALIDATE_INT); |
78
|
|
|
|
79
|
|
|
if (!is_null($cookieId)) { |
80
|
|
|
$sessionId = $cookieId; |
81
|
|
|
} |
82
|
|
|
if ($action == 'old_session') { |
83
|
|
|
$sessionId = null; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
foreach ($botBlacklist as $bot) { |
87
|
|
|
if ((bool)Strings::strstr($agent, $bot)) { |
88
|
|
|
++$bots; |
89
|
|
|
} |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
$network = new Network($this->config); |
93
|
|
|
|
94
|
|
|
// if we're running behind a reverse proxy like nginx/varnish, fix the client IP |
95
|
|
|
$remoteAddress = $_SERVER['REMOTE_ADDR']; |
96
|
|
|
$localAddresses = ['127.0.0.1', '::1']; |
97
|
|
|
|
98
|
|
|
if (in_array($remoteAddress, $localAddresses) && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { |
99
|
|
|
$remoteAddress = $_SERVER['HTTP_X_FORWARDED_FOR']; |
100
|
|
|
} |
101
|
|
|
// clean up as well |
102
|
|
|
$remoteAddress = preg_replace('([^0-9a-z:\.]+)i', '', $remoteAddress); |
103
|
|
|
|
104
|
|
|
if (!$network->checkIp($remoteAddress)) { |
105
|
|
|
$banned = true; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
if (0 === $bots && false === $banned) { |
109
|
|
|
if (!isset($sessionId)) { |
110
|
|
|
$sessionId = $this->config->getDb()->nextId(Db::getTablePrefix().'faqsessions', 'sid'); |
111
|
|
|
// Sanity check: force the session cookie to contains the current $sid |
112
|
|
|
if (!is_null($cookieId) && (!$cookieId != $sessionId)) { |
113
|
|
|
self::setCookie(self::PMF_COOKIE_NAME_SESSIONID, $sessionId); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
$query = sprintf(" |
117
|
|
|
INSERT INTO |
118
|
|
|
%sfaqsessions |
119
|
|
|
(sid, user_id, ip, time) |
120
|
|
|
VALUES |
121
|
|
|
(%d, %d, '%s', %d)", |
122
|
|
|
Db::getTablePrefix(), |
123
|
|
|
$sessionId, |
124
|
|
|
($user ? $user->getUserId() : -1), |
125
|
|
|
$remoteAddress, |
126
|
|
|
$_SERVER['REQUEST_TIME'] |
127
|
|
|
); |
128
|
|
|
$this->config->getDb()->query($query); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
$data = $sessionId.';'. |
132
|
|
|
str_replace(';', ',', $action).';'. |
133
|
|
|
$data.';'. |
134
|
|
|
$remoteAddress.';'. |
135
|
|
|
str_replace(';', ',', isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '').';'. |
136
|
|
|
str_replace(';', ',', isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '').';'. |
137
|
|
|
str_replace(';', ',', urldecode($_SERVER['HTTP_USER_AGENT'])).';'. |
138
|
|
|
$_SERVER['REQUEST_TIME'].";\n"; |
139
|
|
|
$file = PMF_ROOT_DIR.'/data/tracking'.date('dmY'); |
140
|
|
|
|
141
|
|
|
if (!is_file($file)) { |
142
|
|
|
touch($file); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
if (is_writeable($file)) { |
146
|
|
|
file_put_contents($file, $data, FILE_APPEND | LOCK_EX); |
147
|
|
|
} else { |
148
|
|
|
throw new Exception('Cannot write to '.$file); |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Returns the timestamp of a session. |
156
|
|
|
* |
157
|
|
|
* @param int $sid Session ID |
158
|
|
|
* |
159
|
|
|
* @return int |
160
|
|
|
*/ |
161
|
|
View Code Duplication |
public function getTimeFromSessionId(int $sid) |
|
|
|
|
162
|
|
|
{ |
163
|
|
|
$timestamp = 0; |
164
|
|
|
|
165
|
|
|
$query = sprintf(' |
166
|
|
|
SELECT |
167
|
|
|
time |
168
|
|
|
FROM |
169
|
|
|
%sfaqsessions |
170
|
|
|
WHERE |
171
|
|
|
sid = %d', |
172
|
|
|
Db::getTablePrefix(), |
173
|
|
|
$sid); |
174
|
|
|
|
175
|
|
|
$result = $this->config->getDb()->query($query); |
176
|
|
|
|
177
|
|
|
if ($result) { |
178
|
|
|
$res = $this->config->getDb()->fetchObject($result); |
179
|
|
|
$timestamp = $res->time; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
return $timestamp; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Returns all session from a date. |
187
|
|
|
* |
188
|
|
|
* @param int $firstHour First hour |
189
|
|
|
* @param int $lastHour Last hour |
190
|
|
|
* |
191
|
|
|
* @return array |
192
|
|
|
*/ |
193
|
|
View Code Duplication |
public function getSessionsByDate(int $firstHour, int $lastHour): array |
|
|
|
|
194
|
|
|
{ |
195
|
|
|
$sessions = []; |
196
|
|
|
|
197
|
|
|
$query = sprintf(' |
198
|
|
|
SELECT |
199
|
|
|
sid, ip, time |
200
|
|
|
FROM |
201
|
|
|
%sfaqsessions |
202
|
|
|
WHERE |
203
|
|
|
time > %d |
204
|
|
|
AND |
205
|
|
|
time < %d |
206
|
|
|
ORDER BY |
207
|
|
|
time', |
208
|
|
|
Db::getTablePrefix(), |
209
|
|
|
$firstHour, |
210
|
|
|
$lastHour |
211
|
|
|
); |
212
|
|
|
|
213
|
|
|
$result = $this->config->getDb()->query($query); |
214
|
|
|
while ($row = $this->config->getDb()->fetchObject($result)) { |
215
|
|
|
$sessions[$row->sid] = array( |
216
|
|
|
'ip' => $row->ip, |
217
|
|
|
'time' => $row->time, |
218
|
|
|
); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
return $sessions; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Returns the number of sessions. |
226
|
|
|
* |
227
|
|
|
* @return int |
228
|
|
|
*/ |
229
|
|
View Code Duplication |
public function getNumberOfSessions(): int |
|
|
|
|
230
|
|
|
{ |
231
|
|
|
$num = 0; |
232
|
|
|
|
233
|
|
|
$query = sprintf(' |
234
|
|
|
SELECT |
235
|
|
|
COUNT(sid) as num_sessions |
236
|
|
|
FROM |
237
|
|
|
%sfaqsessions', |
238
|
|
|
Db::getTablePrefix()); |
239
|
|
|
|
240
|
|
|
$result = $this->config->getDb()->query($query); |
241
|
|
|
if ($result) { |
242
|
|
|
$row = $this->config->getDb()->fetchObject($result); |
243
|
|
|
$num = $row->num_sessions; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
return $num; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Deletes the sessions for a given timespan. |
251
|
|
|
* |
252
|
|
|
* @param int $first Frist session ID |
253
|
|
|
* @param int $last Last session ID |
254
|
|
|
* |
255
|
|
|
* @return bool |
256
|
|
|
*/ |
257
|
|
View Code Duplication |
public function deleteSessions(int $first, int $last): bool |
|
|
|
|
258
|
|
|
{ |
259
|
|
|
$query = sprintf(' |
260
|
|
|
DELETE FROM |
261
|
|
|
%sfaqsessions |
262
|
|
|
WHERE |
263
|
|
|
time >= %d |
264
|
|
|
AND |
265
|
|
|
time <= %d', |
266
|
|
|
Db::getTablePrefix(), |
267
|
|
|
$first, |
268
|
|
|
$last); |
269
|
|
|
|
270
|
|
|
$this->config->getDb()->query($query); |
271
|
|
|
|
272
|
|
|
return true; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Deletes all entries in the table. |
277
|
|
|
* |
278
|
|
|
* @return mixed |
279
|
|
|
*/ |
280
|
|
|
public function deleteAllSessions() |
281
|
|
|
{ |
282
|
|
|
$query = sprintf('DELETE FROM %sfaqsessions', Db::getTablePrefix()); |
283
|
|
|
|
284
|
|
|
return $this->config->getDb()->query($query); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Checks the Session ID. |
289
|
|
|
* |
290
|
|
|
* @param int $sessionIdToCheck Session ID |
291
|
|
|
* @param string $ip IP |
292
|
|
|
* @throws |
293
|
|
|
*/ |
294
|
|
|
public function checkSessionId(int $sessionIdToCheck, string $ip) |
295
|
|
|
{ |
296
|
|
|
global $sessionId, $user; |
297
|
|
|
|
298
|
|
|
$query = sprintf(" |
299
|
|
|
SELECT |
300
|
|
|
sid |
301
|
|
|
FROM |
302
|
|
|
%sfaqsessions |
303
|
|
|
WHERE |
304
|
|
|
sid = %d |
305
|
|
|
AND |
306
|
|
|
ip = '%s' |
307
|
|
|
AND |
308
|
|
|
time > %d", |
309
|
|
|
Db::getTablePrefix(), |
310
|
|
|
$sessionIdToCheck, |
311
|
|
|
$ip, |
312
|
|
|
$_SERVER['REQUEST_TIME'] - 86400 |
313
|
|
|
); |
314
|
|
|
$result = $this->config->getDb()->query($query); |
315
|
|
|
|
316
|
|
|
if ($this->config->getDb()->numRows($result) == 0) { |
317
|
|
|
$this->userTracking('old_session', $sessionIdToCheck); |
318
|
|
|
} else { |
319
|
|
|
// Update global session id |
320
|
|
|
$sessionId = $sessionIdToCheck; |
321
|
|
|
// Update db tracking |
322
|
|
|
$query = sprintf(" |
323
|
|
|
UPDATE |
324
|
|
|
%sfaqsessions |
325
|
|
|
SET |
326
|
|
|
time = %d, |
327
|
|
|
user_id = %d |
328
|
|
|
WHERE |
329
|
|
|
sid = %d |
330
|
|
|
AND ip = '%s'", |
331
|
|
|
Db::getTablePrefix(), |
332
|
|
|
$_SERVER['REQUEST_TIME'], |
333
|
|
|
($user ? $user->getUserId() : '-1'), |
334
|
|
|
$sessionIdToCheck, |
335
|
|
|
$ip |
336
|
|
|
); |
337
|
|
|
$this->config->getDb()->query($query); |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
/** |
341
|
|
|
* Returns the number of anonymous users and registered ones. |
342
|
|
|
* These are the numbers of unique users who have performed |
343
|
|
|
* some activities within the last five minutes. |
344
|
|
|
* |
345
|
|
|
* @param int $activityTimeWindow Optionally set the time window size in sec. |
346
|
|
|
* Default: 300sec, 5 minutes |
347
|
|
|
* |
348
|
|
|
* @return array |
349
|
|
|
*/ |
350
|
|
|
public function getUsersOnline(int $activityTimeWindow = 300): array |
351
|
|
|
{ |
352
|
|
|
$users = array(0, 0); |
353
|
|
|
|
354
|
|
|
if ($this->config->get('main.enableUserTracking')) { |
355
|
|
|
$timeNow = ($_SERVER['REQUEST_TIME'] - $activityTimeWindow); |
356
|
|
|
|
357
|
|
|
if (!$this->config->get('security.enableLoginOnly')) { |
358
|
|
|
// Count all sids within the time window for public installations |
359
|
|
|
$query = sprintf(' |
360
|
|
|
SELECT |
361
|
|
|
count(sid) AS anonymous_users |
362
|
|
|
FROM |
363
|
|
|
%sfaqsessions |
364
|
|
|
WHERE |
365
|
|
|
user_id = -1 |
366
|
|
|
AND |
367
|
|
|
time > %d', |
368
|
|
|
Db::getTablePrefix(), |
369
|
|
|
$timeNow |
370
|
|
|
); |
371
|
|
|
|
372
|
|
|
$result = $this->config->getDb()->query($query); |
373
|
|
|
|
374
|
|
View Code Duplication |
if (isset($result)) { |
375
|
|
|
$row = $this->config->getDb()->fetchObject($result); |
376
|
|
|
$users[0] = $row->anonymous_users; |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
// Count all faquser records within the time window |
381
|
|
|
$query = sprintf(' |
382
|
|
|
SELECT |
383
|
|
|
count(session_id) AS registered_users |
384
|
|
|
FROM |
385
|
|
|
%sfaquser |
386
|
|
|
WHERE |
387
|
|
|
session_timestamp > %d', |
388
|
|
|
Db::getTablePrefix(), |
389
|
|
|
$timeNow |
390
|
|
|
); |
391
|
|
|
|
392
|
|
|
$result = $this->config->getDb()->query($query); |
393
|
|
|
|
394
|
|
View Code Duplication |
if (isset($result)) { |
395
|
|
|
$row = $this->config->getDb()->fetchObject($result); |
396
|
|
|
$users[1] = $row->registered_users; |
397
|
|
|
} |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
return $users; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Calculates the number of visits per day the last 30 days. |
405
|
|
|
* |
406
|
|
|
* @returns array |
407
|
|
|
*/ |
408
|
|
|
public function getLast30DaysVisits(): array |
409
|
|
|
{ |
410
|
|
|
$stats = $visits = []; |
411
|
|
|
|
412
|
|
|
$startDate = strtotime('-1 month'); |
413
|
|
|
$endDate = $_SERVER['REQUEST_TIME']; |
414
|
|
|
|
415
|
|
|
$query = sprintf(' |
416
|
|
|
SELECT |
417
|
|
|
time |
418
|
|
|
FROM |
419
|
|
|
%sfaqsessions |
420
|
|
|
WHERE |
421
|
|
|
time > %d |
422
|
|
|
AND |
423
|
|
|
time < %d;', |
424
|
|
|
Db::getTablePrefix(), |
425
|
|
|
$startDate, |
426
|
|
|
$endDate |
427
|
|
|
); |
428
|
|
|
$result = $this->config->getDb()->query($query); |
429
|
|
|
|
430
|
|
|
while ($row = $this->config->getDb()->fetchObject($result)) { |
431
|
|
|
$visits[] = $row->time; |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
for ($date = $startDate; $date <= $endDate; $date += 86400) { |
435
|
|
|
$stats[date('Y-m-d', $date)] = 0; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
foreach ($visits as $visitDate) { |
439
|
|
|
isset($stats[date('Y-m-d', $visitDate)]) ? $stats[date('Y-m-d', $visitDate)]++ : null; |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
return $stats; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Store the Session ID into a persistent cookie expiring |
447
|
|
|
* PMF_SESSION_EXPIRED_TIME seconds after the page request. |
448
|
|
|
* |
449
|
|
|
* @param string $name Cookie name |
450
|
|
|
* @param string|null $sessionId Session ID |
451
|
|
|
* @param int $timeout Cookie timeout |
452
|
|
|
* |
453
|
|
|
* @return bool |
454
|
|
|
*/ |
455
|
|
|
public function setCookie(string $name, $sessionId = '', int $timeout = PMF_SESSION_EXPIRED_TIME): bool |
456
|
|
|
{ |
457
|
|
|
$protocol = 'http'; |
458
|
|
|
if (isset($_SERVER['HTTPS']) && strtoupper($_SERVER['HTTPS']) === 'ON') { |
459
|
|
|
$protocol = 'https'; |
460
|
|
|
} |
461
|
|
|
return setcookie( |
462
|
|
|
$name, |
463
|
|
|
$sessionId, |
464
|
|
|
$_SERVER['REQUEST_TIME'] + $timeout, |
465
|
|
|
dirname($_SERVER['SCRIPT_NAME']), |
466
|
|
|
$this->config->getDefaultUrl(), |
467
|
|
|
('https' === $protocol) ? true : false, |
468
|
|
|
true |
469
|
|
|
); |
470
|
|
|
} |
471
|
|
|
} |
472
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.