WebOrbiton
v3.0.0.2

FlatlyPage

390 lines · 11.8 KB
  1. <?php
  2. class LoginTracker
  3. {
  4. private $loginsFile;
  5. ​
  6. public function __construct()
  7. {
  8. $this->loginsFile = DATA_DIR . '/private/logins.xml';
  9. $this->ensureLoginsFileExists();
  10. }
  11. ​
  12. private function ensureLoginsFileExists()
  13. {
  14. $privateDir = DATA_DIR . '/private';
  15. if (!is_dir($privateDir)) {
  16. mkdir($privateDir, 0755, true);
  17. }
  18. ​
  19. if (!file_exists($this->loginsFile)) {
  20. $xml = new DOMDocument('1.0', 'UTF-8');
  21. $xml->formatOutput = true;
  22. $root = $xml->createElement('logins');
  23. $xml->appendChild($root);
  24. $xml->save($this->loginsFile);
  25. chmod($this->loginsFile, 0600);
  26. }
  27. }
  28. ​
  29. private function getClientInfo()
  30. {
  31. return [
  32. 'ip' => $this->getClientIP(),
  33. 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown',
  34. 'language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'Unknown',
  35. 'platform' => $this->getPlatform(),
  36. 'browser' => $this->getBrowser()
  37. ];
  38. }
  39. ​
  40. public function getClientIP()
  41. {
  42. $ip = 'Unknown';
  43. ​
  44. if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
  45. $ip = $_SERVER['HTTP_CLIENT_IP'];
  46. } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  47. $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
  48. $ip = trim($ips[0]);
  49. } elseif (!empty($_SERVER['REMOTE_ADDR'])) {
  50. $ip = $_SERVER['REMOTE_ADDR'];
  51. }
  52. ​
  53. if (filter_var($ip, FILTER_VALIDATE_IP)) {
  54. return $ip;
  55. }
  56. ​
  57. return $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
  58. }
  59. ​
  60. private function getPlatform()
  61. {
  62. $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
  63. ​
  64. if (preg_match('/windows/i', $ua)) return 'Windows';
  65. if (preg_match('/macintosh|mac os x/i', $ua)) return 'macOS';
  66. if (preg_match('/linux/i', $ua)) return 'Linux';
  67. if (preg_match('/android/i', $ua)) return 'Android';
  68. if (preg_match('/iphone|ipad|ipod/i', $ua)) return 'iOS';
  69. ​
  70. return 'Unknown';
  71. }
  72. ​
  73. private function getBrowser()
  74. {
  75. $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
  76. ​
  77. if (preg_match('/edg/i', $ua)) return 'Edge';
  78. if (preg_match('/chrome/i', $ua)) return 'Chrome';
  79. if (preg_match('/firefox/i', $ua)) return 'Firefox';
  80. if (preg_match('/safari/i', $ua) && !preg_match('/chrome/i', $ua)) return 'Safari';
  81. if (preg_match('/opera|opr/i', $ua)) return 'Opera';
  82. ​
  83. return 'Unknown';
  84. }
  85. ​
  86. private function generateSessionToken()
  87. {
  88. return bin2hex(random_bytes(32));
  89. }
  90. ​
  91. public function recordLogin($username)
  92. {
  93. $xml = new DOMDocument('1.0', 'UTF-8');
  94. $xml->formatOutput = true;
  95. $xml->preserveWhiteSpace = false;
  96. ​
  97. if (!$xml->load($this->loginsFile)) {
  98. return false;
  99. }
  100. ​
  101. $root = $xml->documentElement;
  102. $clientInfo = $this->getClientInfo();
  103. $sessionToken = $this->generateSessionToken();
  104. ​
  105. $login = $xml->createElement('login');
  106. $login->setAttribute('id', uniqid('login_', true));
  107. $login->setAttribute('session_token', $sessionToken);
  108. ​
  109. $usernameNode = $xml->createElement('username');
  110. $usernameNode->appendChild($xml->createTextNode($username));
  111. $login->appendChild($usernameNode);
  112. ​
  113. $ipNode = $xml->createElement('ip');
  114. $ipNode->appendChild($xml->createTextNode($clientInfo['ip']));
  115. $login->appendChild($ipNode);
  116. ​
  117. $uaNode = $xml->createElement('user_agent');
  118. $uaNode->appendChild($xml->createCDATASection($clientInfo['user_agent']));
  119. $login->appendChild($uaNode);
  120. ​
  121. $langNode = $xml->createElement('language');
  122. $langNode->appendChild($xml->createTextNode($clientInfo['language']));
  123. $login->appendChild($langNode);
  124. ​
  125. $platformNode = $xml->createElement('platform');
  126. $platformNode->appendChild($xml->createTextNode($clientInfo['platform']));
  127. $login->appendChild($platformNode);
  128. ​
  129. $browserNode = $xml->createElement('browser');
  130. $browserNode->appendChild($xml->createTextNode($clientInfo['browser']));
  131. $login->appendChild($browserNode);
  132. ​
  133. $loginTimeNode = $xml->createElement('login_time');
  134. $loginTimeNode->appendChild($xml->createTextNode(date('Y-m-d H:i:s')));
  135. $login->appendChild($loginTimeNode);
  136. ​
  137. $lastActivityNode = $xml->createElement('last_activity');
  138. $lastActivityNode->appendChild($xml->createTextNode(date('Y-m-d H:i:s')));
  139. $login->appendChild($lastActivityNode);
  140. ​
  141. $statusNode = $xml->createElement('status');
  142. $statusNode->appendChild($xml->createTextNode('active'));
  143. $login->appendChild($statusNode);
  144. ​
  145. $root->appendChild($login);
  146. ​
  147. if ($xml->save($this->loginsFile)) {
  148. $_SESSION['login_token'] = $sessionToken;
  149. return true;
  150. }
  151. ​
  152. return false;
  153. }
  154. ​
  155. public function updateActivity()
  156. {
  157. if (!isset($_SESSION['login_token']) || !is_string($_SESSION['login_token']) || preg_match('/^[a-f0-9]{64}$/', $_SESSION['login_token']) !== 1) {
  158. return false;
  159. }
  160. ​
  161. $xml = new DOMDocument('1.0', 'UTF-8');
  162. $xml->formatOutput = true;
  163. $xml->preserveWhiteSpace = false;
  164. ​
  165. if (!$xml->load($this->loginsFile)) {
  166. return false;
  167. }
  168. ​
  169. $xpath = new DOMXPath($xml);
  170. $sessionToken = $_SESSION['login_token'];
  171. ​
  172. $nodes = $xpath->query("//login[@session_token='$sessionToken']");
  173. ​
  174. if ($nodes->length > 0) {
  175. $loginNode = $nodes->item(0);
  176. ​
  177. $lastActivityNodes = $xpath->query('last_activity', $loginNode);
  178. if ($lastActivityNodes->length > 0) {
  179. $lastActivityNodes->item(0)->nodeValue = date('Y-m-d H:i:s');
  180. return $xml->save($this->loginsFile);
  181. }
  182. }
  183. ​
  184. return false;
  185. }
  186. ​
  187. public function getActiveLogins()
  188. {
  189. $xml = new DOMDocument('1.0', 'UTF-8');
  190. ​
  191. if (!$xml->load($this->loginsFile)) {
  192. return [];
  193. }
  194. ​
  195. $xpath = new DOMXPath($xml);
  196. $logins = [];
  197. ​
  198. $nodes = $xpath->query("//login[status='active']");
  199. ​
  200. foreach ($nodes as $node) {
  201. $loginId = $node->getAttribute('id');
  202. $sessionToken = $node->getAttribute('session_token');
  203. ​
  204. $logins[] = [
  205. 'id' => $loginId,
  206. 'session_token' => $sessionToken,
  207. 'username' => $xpath->query('username', $node)->item(0)->nodeValue,
  208. 'ip' => $xpath->query('ip', $node)->item(0)->nodeValue,
  209. 'user_agent' => $xpath->query('user_agent', $node)->item(0)->nodeValue,
  210. 'language' => $xpath->query('language', $node)->item(0)->nodeValue,
  211. 'platform' => $xpath->query('platform', $node)->item(0)->nodeValue,
  212. 'browser' => $xpath->query('browser', $node)->item(0)->nodeValue,
  213. 'login_time' => $xpath->query('login_time', $node)->item(0)->nodeValue,
  214. 'last_activity' => $xpath->query('last_activity', $node)->item(0)->nodeValue,
  215. 'is_current' => ($sessionToken === ($_SESSION['login_token'] ?? ''))
  216. ];
  217. }
  218. ​
  219. usort($logins, function ($a, $b) {
  220. return strtotime($b['login_time']) - strtotime($a['login_time']);
  221. });
  222. ​
  223. return $logins;
  224. }
  225. ​
  226. public function logoutSession($loginId)
  227. {
  228. if (!is_string($loginId) || preg_match('/^login_[a-f0-9]+\.[0-9]+$/', $loginId) !== 1) {
  229. return false;
  230. }
  231. ​
  232. $xml = new DOMDocument('1.0', 'UTF-8');
  233. $xml->formatOutput = true;
  234. $xml->preserveWhiteSpace = false;
  235. ​
  236. if (!$xml->load($this->loginsFile)) {
  237. return false;
  238. }
  239. ​
  240. $xpath = new DOMXPath($xml);
  241. $nodes = $xpath->query("//login[@id='$loginId']");
  242. ​
  243. if ($nodes->length > 0) {
  244. $loginNode = $nodes->item(0);
  245. ​
  246. $statusNodes = $xpath->query('status', $loginNode);
  247. if ($statusNodes->length > 0) {
  248. $statusNodes->item(0)->nodeValue = 'logged_out';
  249. }
  250. ​
  251. $logoutTimeNode = $xml->createElement('logout_time');
  252. $logoutTimeNode->appendChild($xml->createTextNode(date('Y-m-d H:i:s')));
  253. $loginNode->appendChild($logoutTimeNode);
  254. ​
  255. return $xml->save($this->loginsFile);
  256. }
  257. ​
  258. return false;
  259. }
  260. ​
  261. public function banIP($ip)
  262. {
  263. if (!is_string($ip) || filter_var($ip, FILTER_VALIDATE_IP) === false) {
  264. return false;
  265. }
  266. ​
  267. $bannedFile = DATA_DIR . '/private/banned_ips.json';
  268. ​
  269. $banned = [];
  270. if (file_exists($bannedFile)) {
  271. $banned = json_decode(file_get_contents($bannedFile), true) ?: [];
  272. }
  273. ​
  274. if (!in_array($ip, $banned)) {
  275. $banned[] = $ip;
  276. file_put_contents($bannedFile, json_encode($banned, JSON_PRETTY_PRINT));
  277. chmod($bannedFile, 0600);
  278. ​
  279. $this->logoutByIP($ip);
  280. ​
  281. return true;
  282. }
  283. ​
  284. return false;
  285. }
  286. ​
  287. public function unbanIP($ip)
  288. {
  289. $bannedFile = DATA_DIR . '/private/banned_ips.json';
  290. ​
  291. if (file_exists($bannedFile)) {
  292. $banned = json_decode(file_get_contents($bannedFile), true) ?: [];
  293. $banned = array_filter($banned, function ($bannedIp) use ($ip) {
  294. return $bannedIp !== $ip;
  295. });
  296. ​
  297. file_put_contents($bannedFile, json_encode(array_values($banned), JSON_PRETTY_PRINT));
  298. return true;
  299. }
  300. ​
  301. return false;
  302. }
  303. ​
  304. public function isIPBanned($ip)
  305. {
  306. $bannedFile = DATA_DIR . '/private/banned_ips.json';
  307. ​
  308. if (file_exists($bannedFile)) {
  309. $banned = json_decode(file_get_contents($bannedFile), true) ?: [];
  310. return in_array($ip, $banned);
  311. }
  312. ​
  313. return false;
  314. }
  315. ​
  316. public function getBannedIPs()
  317. {
  318. $bannedFile = DATA_DIR . '/private/banned_ips.json';
  319. ​
  320. if (file_exists($bannedFile)) {
  321. return json_decode(file_get_contents($bannedFile), true) ?: [];
  322. }
  323. ​
  324. return [];
  325. }
  326. ​
  327. private function logoutByIP($ip)
  328. {
  329. if (!is_string($ip) || filter_var($ip, FILTER_VALIDATE_IP) === false) {
  330. return false;
  331. }
  332. ​
  333. $xml = new DOMDocument('1.0', 'UTF-8');
  334. $xml->formatOutput = true;
  335. $xml->preserveWhiteSpace = false;
  336. ​
  337. if (!$xml->load($this->loginsFile)) {
  338. return false;
  339. }
  340. ​
  341. $xpath = new DOMXPath($xml);
  342. $nodes = $xpath->query("//login[ip='$ip' and status='active']");
  343. ​
  344. foreach ($nodes as $loginNode) {
  345. $statusNodes = $xpath->query('status', $loginNode);
  346. if ($statusNodes->length > 0) {
  347. $statusNodes->item(0)->nodeValue = 'banned';
  348. }
  349. ​
  350. $logoutTimeNode = $xml->createElement('logout_time');
  351. $logoutTimeNode->appendChild($xml->createTextNode(date('Y-m-d H:i:s')));
  352. $loginNode->appendChild($logoutTimeNode);
  353. }
  354. ​
  355. return $xml->save($this->loginsFile);
  356. }
  357. ​
  358. public function cleanOldSessions($daysOld = 30)
  359. {
  360. $xml = new DOMDocument('1.0', 'UTF-8');
  361. $xml->formatOutput = true;
  362. $xml->preserveWhiteSpace = false;
  363. ​
  364. if (!$xml->load($this->loginsFile)) {
  365. return false;
  366. }
  367. ​
  368. $xpath = new DOMXPath($xml);
  369. $cutoffDate = date('Y-m-d H:i:s', strtotime("-$daysOld days"));
  370. ​
  371. $nodes = $xpath->query("//login[status!='active']");
  372. $removed = 0;
  373. ​
  374. foreach ($nodes as $node) {
  375. $lastActivity = $xpath->query('last_activity', $node)->item(0)->nodeValue;
  376. ​
  377. if ($lastActivity < $cutoffDate) {
  378. $node->parentNode->removeChild($node);
  379. $removed++;
  380. }
  381. }
  382. ​
  383. if ($removed > 0) {
  384. return $xml->save($this->loginsFile);
  385. }
  386. ​
  387. return true;
  388. }
  389. }
  390. ​