doomguy.class.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. <?php
  2. /**
  3. * Bibliotheca
  4. *
  5. * Copyright 2018-2023 Johannes Keßler
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see http://www.gnu.org/licenses/gpl-3.0.
  19. */
  20. /**
  21. * User object
  22. * access rights and information about the current logged in user
  23. */
  24. class Doomguy {
  25. /**
  26. * the global DB object
  27. *
  28. * @var mysqli
  29. */
  30. private mysqli $_DB;
  31. /**
  32. * if the user is logged in or not
  33. *
  34. * @var boolean
  35. */
  36. protected bool $isSignedIn = false;
  37. /**
  38. * the data from the current user
  39. *
  40. * @var array
  41. */
  42. protected array $userData = array();
  43. /**
  44. * the user ID from user management or default
  45. *
  46. * @var string|int
  47. */
  48. protected string|int $userID = 0;
  49. /**
  50. * the rights string defined the mysql query !
  51. * the syntax is for mysql only
  52. *
  53. * @var array
  54. */
  55. protected array $_rightsArray = array(
  56. 'user' => array(
  57. 'read' => 'r________',
  58. 'write' => 'rw_______',
  59. 'delete' => 'rwx______'
  60. ),
  61. 'group' => array(
  62. 'read' => '___r_____',
  63. 'write' => '___rw____',
  64. 'delete' => '___rwx___'
  65. ),
  66. 'world' => array(
  67. 'read' => '______r__',
  68. 'write' => '______rw_',
  69. 'delete' => '______rwx'
  70. )
  71. );
  72. /**
  73. * Doomguy constructor.
  74. *
  75. * @param mysqli $db The database object
  76. * @return void
  77. */
  78. public function __construct(mysqli $db) {
  79. $this->_DB = $db;
  80. if($this->_checkSession() === true) {
  81. $this->isSignedIn = true;
  82. $this->_loadUser();
  83. }
  84. else {
  85. # anonymoose ;-)
  86. $this->userID = ANON_USER_ID;
  87. $this->_loadUser();
  88. }
  89. }
  90. /**
  91. * get the value of the specified param from the user data array
  92. *
  93. * @param string $param
  94. * @return bool|mixed
  95. */
  96. public function param(string $param): mixed {
  97. $ret = false;
  98. $param = trim($param);
  99. if(!empty($param) && isset($this->userData[$param])) {
  100. $ret = $this->userData[$param];
  101. }
  102. return $ret;
  103. }
  104. /**
  105. * Get the currently loaded user data info from $this->userData
  106. *
  107. * @return array
  108. */
  109. public function getAllUserData(): array {
  110. return $this->userData;
  111. }
  112. /**
  113. * return the isSignedIn status.
  114. *
  115. * @return bool
  116. */
  117. public function isSignedIn(): bool {
  118. return $this->isSignedIn;
  119. }
  120. /**
  121. * Log out the current loaded user
  122. *
  123. * @return boolean
  124. */
  125. public function logOut (): bool {
  126. $ret = false;
  127. if($this->_checkAgainstSessionTable() === true) {
  128. $this->_destroySession();
  129. $ret = true;
  130. }
  131. return $ret;
  132. }
  133. /**
  134. * check if the loaded user is in this group
  135. * if the user is in ADMIN_GROUP_ID, the he is automatically "in" every group
  136. *
  137. * @param integer $groupID
  138. * @return bool
  139. */
  140. public function isInGroup(int $groupID): bool {
  141. $ret = false;
  142. if($this->userData['isRoot'] === true) {
  143. $ret = true;
  144. }
  145. elseif(in_array($groupID, array_keys($this->userData['group']))) {
  146. $ret = true;
  147. }
  148. return $ret;
  149. }
  150. /**
  151. * Authenticate the user. Create session and db entries
  152. *
  153. * @param string $username
  154. * @param string $password
  155. * @return boolean
  156. */
  157. public function authenticate(string $username, string $password): bool {
  158. $ret = false;
  159. if(!empty($username) && !empty($password)) {
  160. $do = $this->_checkAgainstUserTable($username);
  161. if($do === true) {
  162. # valid user now load the user data and compare password etc.
  163. $this->_loadUser();
  164. if(password_verify($password,$this->userData['password'])) {
  165. # everything ok
  166. # create the session info
  167. $tokenInfo = $this->_createToken();
  168. $_SESSION[SESSION_NAME]['bibliothecatoken'] = $tokenInfo['token'];
  169. $queryStr = "INSERT INTO `".DB_PREFIX."_userSession`
  170. SET `token` = '".$this->_DB->real_escape_string($tokenInfo['token'])."',
  171. `loginTime` = NOW(),
  172. `area` = '".$this->_DB->real_escape_string(SESSION_NAME)."',
  173. `fk_user_id` = '".$this->_DB->real_escape_string($this->userID)."',
  174. `salt` = '".$this->_DB->real_escape_string($tokenInfo['salt'])."'
  175. ON DUPLICATE KEY UPDATE
  176. `token` = '".$this->_DB->real_escape_string($tokenInfo['token'])."',
  177. `salt` = '".$this->_DB->real_escape_string($tokenInfo['salt'])."',
  178. `loginTime` = NOW()";
  179. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  180. try {
  181. $this->_DB->query($queryStr);
  182. # do some actions
  183. $this->_loginActions();
  184. }
  185. catch (Exception $e) {
  186. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  187. }
  188. $ret = true;
  189. }
  190. }
  191. }
  192. return $ret;
  193. }
  194. /**
  195. * Use the user identified by apitoken
  196. *
  197. * @param string $token
  198. * @return void
  199. */
  200. public function authByApiToken(string $token): void {
  201. if(!empty($token)) {
  202. $queryStr = "SELECT `id`
  203. FROM `".DB_PREFIX."_user`
  204. WHERE `apiToken` = '".$this->_DB->real_escape_string($token)."'
  205. AND `apiTokenValidDate` > NOW()";
  206. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  207. try {
  208. $query = $this->_DB->query($queryStr);
  209. if ($query !== false && $query->num_rows > 0) {
  210. $result = $query->fetch_assoc();
  211. $this->userID = $result['id'];
  212. $this->isSignedIn = true;
  213. $this->_loadUser();
  214. $this->_loginActions();
  215. }
  216. }
  217. catch (Exception $e) {
  218. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  219. }
  220. }
  221. }
  222. /**
  223. * create the sql string for rights sql
  224. *
  225. * @param string $mode
  226. * @param string $tableName
  227. * @return string
  228. */
  229. public function getSQLRightsString(string $mode = "read", string $tableName = ''): string {
  230. $str = '';
  231. $prefix = '';
  232. if(!empty($tableName)) {
  233. $prefix = "`".$tableName."`.";
  234. }
  235. if(isset($this->_rightsArray['user'][$mode]) && isset($this->_rightsArray['group'][$mode]) && isset($this->_rightsArray['world'][$mode])) {
  236. $uid = $this->userID;
  237. $gids = implode("','", array_keys($this->userData['groups']));
  238. if($this->userData['isRoot'] === true) {
  239. $str = "( ($prefix`rights` LIKE '".$this->_rightsArray['user'][$mode]."') ";
  240. $str .= "OR ($prefix`rights` LIKE '".$this->_rightsArray['group'][$mode]."') ";
  241. $str .= "OR ($prefix`rights` LIKE '".$this->_rightsArray['world'][$mode]."') )";
  242. }
  243. else {
  244. $str = "( ($prefix`owner` = ".$uid." AND $prefix`rights` LIKE '".$this->_rightsArray['user'][$mode]."') ";
  245. $str .= "OR ($prefix`group` IN ('".$gids."') AND $prefix`rights` LIKE '".$this->_rightsArray['group'][$mode]."') ";
  246. $str .= "OR ($prefix`rights` LIKE '".$this->_rightsArray['world'][$mode]."') )";
  247. }
  248. }
  249. else {
  250. Summoner::sysLog("[ERROR] ".__METHOD__." invalid rights string: ".Summoner::cleanForLog($this->_rightsArray));
  251. }
  252. return $str;
  253. }
  254. /**
  255. * check if we can use session
  256. * we only use session if we can use cookies with the session
  257. * THIS DOES NOT CHECK IF THE USER HAS COOKIES ACTIVATED !
  258. *
  259. * @return bool
  260. */
  261. protected function _checkSession(): bool {
  262. if(ini_set('session.use_only_cookies',true) === false ||
  263. ini_set('session.cookie_httponly',true) === false ||
  264. ini_set('session.use_cookies',true) === false) {
  265. return false;
  266. }
  267. $garbage_timeout = SESSION_LIFETIME + 300;
  268. ini_set('session.gc_maxlifetime', $garbage_timeout);
  269. # the % rate how often the session.gc is run
  270. # http://de.php.net/manual/en/session.configuration.php#ini.session.gc-probability
  271. ini_set('session.gc_probability',10); // 100 = everytime = 100%
  272. session_save_path(SESSION_SAVE_PATH);
  273. session_set_cookie_params(SESSION_LIFETIME);
  274. session_name(SESSION_NAME);
  275. session_start();
  276. # produce problems
  277. # multiple request at once will confuse the script and loose session information
  278. #session_regenerate_id(true);
  279. if(isset($_SESSION[SESSION_NAME]['bibliothecatoken']) && !empty($_SESSION[SESSION_NAME]['bibliothecatoken'])) {
  280. return $this->_checkAgainstSessionTable();
  281. }
  282. return false;
  283. }
  284. /**
  285. * we have session data available. Now check if those data is valid
  286. *
  287. * @return bool
  288. */
  289. protected function _checkAgainstSessionTable(): bool {
  290. $ret = false;
  291. $timeframe = date("Y-m-d H:i:s",time()-SESSION_LIFETIME);
  292. $queryStr = "SELECT s.fk_user_id, s.salt, s.token FROM `".DB_PREFIX."_userSession` AS s
  293. INNER JOIN `".DB_PREFIX."_user` AS u ON s.fk_user_id = u.id
  294. WHERE s.token = '".$this->_DB->real_escape_string($_SESSION[SESSION_NAME]['bibliothecatoken'])."'
  295. AND s.salt <> ''
  296. AND s.loginTime >= '".$timeframe."'";
  297. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  298. try {
  299. $query = $this->_DB->query($queryStr);
  300. if ($query !== false && $query->num_rows > 0) {
  301. # existing session info
  302. $result = $query->fetch_assoc();
  303. # validate the token
  304. $_check = $this->_createToken($result['salt']);
  305. if (!empty($_check) && $result['token'] === $_check['token']) {
  306. $this->userID = $result['fk_user_id'];
  307. $ret = true;
  308. }
  309. else {
  310. Summoner::sysLog("[ERROR] ".__METHOD__." mismatched token.");
  311. if(isset($result['fk_user_id']) && !empty($result['fk_user_id'])) {
  312. $this->userID = $result['fk_user_id'];
  313. }
  314. $this->_destroySession();
  315. }
  316. }
  317. }
  318. catch (Exception $e) {
  319. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  320. }
  321. return $ret;
  322. }
  323. /**
  324. * check if the given username is set in user table
  325. * if so load the user data
  326. *
  327. * @param string $u
  328. * @return bool
  329. */
  330. protected function _checkAgainstUserTable(string $u): bool {
  331. $ret = false;
  332. if(!empty($u)) {
  333. $queryStr = "SELECT `id`
  334. FROM `".DB_PREFIX."_user`
  335. WHERE `login` = '". $this->_DB->real_escape_string($u)."'
  336. AND `active` = '1'";
  337. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  338. try {
  339. $query = $this->_DB->query($queryStr);
  340. if ($query !== false && $query->num_rows > 0) {
  341. $result = $query->fetch_assoc();
  342. $this->userID = $result['id'];
  343. $ret = true;
  344. }
  345. }
  346. catch (Exception $e) {
  347. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  348. }
  349. }
  350. return $ret;
  351. }
  352. /**
  353. * if we have to run some at login
  354. *
  355. * @return void
  356. */
  357. protected function _loginActions(): void {
  358. # clean old sessions on session table
  359. $timeframe = date("Y-m-d H:i:s",time()-SESSION_LIFETIME);
  360. $queryStr = "DELETE FROM `".DB_PREFIX."_userSession`
  361. WHERE `loginTime` <= '".$timeframe."'";
  362. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  363. try {
  364. $this->_DB->query($queryStr);
  365. }
  366. catch (Exception $e) {
  367. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  368. }
  369. }
  370. /**
  371. * load the user and groups and fill $this->userData
  372. *
  373. * @return void
  374. */
  375. protected function _loadUser(): void {
  376. if(!empty($this->userID)) {
  377. $queryStr = "SELECT u.`id`, u.`baseGroupId`,u.`protected`,u.`password`,u.`login`,u.`name`,
  378. u.`apiToken`,u.`apiTokenValidDate`,
  379. g.name AS groupName, g.description AS groupDescription, g.id AS groupId
  380. FROM `".DB_PREFIX."_user` AS u
  381. LEFT JOIN `".DB_PREFIX."_user2group` AS u2g ON u2g.fk_user_id = u.id
  382. LEFT JOIN `".DB_PREFIX."_group` AS g ON g.id= u2g.fk_group_id
  383. WHERE u.`id` = '".$this->_DB->real_escape_string($this->userID)."'";
  384. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  385. try {
  386. $query = $this->_DB->query($queryStr);
  387. if($query !== false && $query->num_rows > 0) {
  388. while(($result = $query->fetch_assoc()) != false) {
  389. $this->userData['id'] = $result['id'];
  390. $this->userData['baseGroupId'] = $result['baseGroupId'];
  391. $this->userData['protected'] = $result['protected'];
  392. $this->userData['password'] = $result['password'];
  393. $this->userData['login'] = $result['login'];
  394. $this->userData['name'] = $result['name'];
  395. $this->userData['apiToken'] = $result['apiToken'];
  396. $this->userData['apiTokenValidDate'] = $result['apiTokenValidDate'];
  397. $this->userData['groups'][$result['groupId']] = array(
  398. 'groupName' => $result['groupName'],
  399. 'groupDescription' => $result['groupDescription']
  400. );
  401. }
  402. $this->userData['baseGroupName'] = $this->userData['groups'][$this->userData['baseGroupId']]['groupName'];
  403. $this->userData['isRoot'] = false;
  404. $grIds = array_keys($this->userData['groups']);
  405. if(in_array(ADMIN_GROUP_ID,$grIds)) {
  406. $this->userData['isRoot'] = true;
  407. }
  408. }
  409. }
  410. catch (Exception $e) {
  411. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  412. }
  413. }
  414. }
  415. /**
  416. * destroy and remove the current session from SESSION and session table
  417. *
  418. * @return bool
  419. */
  420. protected function _destroySession(): bool {
  421. $timeframe = date("Y-m-d H:i:s",time()-SESSION_LIFETIME);
  422. $queryStr = "DELETE FROM `".DB_PREFIX."_userSession`
  423. WHERE `fk_user_id` = '".$this->_DB->real_escape_string($this->userID)."'
  424. OR `loginTime` <= '".$timeframe."'";
  425. if(QUERY_DEBUG) Summoner::sysLog("[QUERY] ".__METHOD__." query: ".Summoner::cleanForLog($queryStr));
  426. try {
  427. $this->_DB->query($queryStr);
  428. }
  429. catch (Exception $e) {
  430. Summoner::sysLog("[ERROR] ".__METHOD__." mysql catch: ".$e->getMessage());
  431. }
  432. unset($_SESSION);
  433. unset($_COOKIE);
  434. session_destroy();
  435. return true;
  436. }
  437. /**
  438. * create the usertoken based on the $_SERVER information:
  439. * HTTP_USER_AGENT, REMOTE_ADDR, HTTP_DNT, HTTP_VIA, PATH, SHELL, SESSION_MANAGER, USER
  440. * and a salt
  441. *
  442. * @param string $salt
  443. * @return array
  444. */
  445. protected function _createToken(string $salt = ''): array {
  446. $ret = array();
  447. if(empty($salt)) {
  448. # 8 chars
  449. $salt = bin2hex(openssl_random_pseudo_bytes(4));
  450. }
  451. if(!isset($_SERVER['HTTP_USER_AGENT'])) $_SERVER['HTTP_USER_AGENT'] = $salt;
  452. if(!isset($_SERVER['REMOTE_ADDR'])) $_SERVER['REMOTE_ADDR'] = $salt;
  453. if(!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $salt;
  454. if(!isset($_SERVER['HTTP_VIA'])) $_SERVER['HTTP_VIA'] = $salt;
  455. if(!isset($_SERVER['HTTP_DNT'])) $_SERVER['HTTP_DNT'] = $salt;
  456. // cli info
  457. if(!isset($_SERVER['PATH'])) $_SERVER['PATH'] = $salt;
  458. if(!isset($_SERVER['SHELL'])) $_SERVER['SHELL'] = $salt;
  459. if(!isset($_SERVER['SESSION_MANAGER'])) $_SERVER['SESSION_MANAGER'] = $salt;
  460. if(!isset($_SERVER['USER'])) $_SERVER['USER'] = $salt;
  461. $finalString = $_SERVER['HTTP_USER_AGENT']
  462. .$_SERVER['REMOTE_ADDR']
  463. .$_SERVER['HTTP_ACCEPT_LANGUAGE']
  464. .$_SERVER['HTTP_DNT']
  465. .$_SERVER['HTTP_VIA']
  466. .$_SERVER['PATH']
  467. .$_SERVER['SHELL']
  468. .$_SERVER['SESSION_MANAGER']
  469. .$_SERVER['USER'];
  470. $ret['token'] = sha1($finalString.$salt);
  471. $ret['salt'] = $salt;
  472. return $ret;
  473. }
  474. }