self::CLEARANCE_USER, 'admin'=>self::CLEARANCE_ADMIN); const USER_SUBSCRIBED = 1; const USER_UNSUBSCRIBED = 0; //Session & Cookie const SESSION_ID_USER = 'id_user'; const SESSION_ADMIN = 'admin_authenticated'; const COOKIE_TOKEN = 'login'; const COOKIE_DURATION = 60 * 60 * 24 * 365; //1 year const DEFAULT_USER = array( 'id' => 0, 'id_user' => 0, 'name' => '', 'email' => '', 'language' => '', 'timezone' => '', 'subscribed'=> self::USER_UNSUBSCRIBED, 'clearance' => self::CLEARANCE_USER ); /** * Database Handle * @var Db */ private $oDb; //User Info private $iUserId; private $asUserInfo; public function __construct(Db &$oDb) { parent::__construct(__CLASS__); $this->oDb = &$oDb; $this->setUserId(0); $this->checkSession(); } public function getUserId() { return $this->iUserId; } public function setUserId($iUserId) { $this->iUserId = 0; $this->asUserInfo = self::DEFAULT_USER; if($iUserId > 0) { $asUser = $this->getUserById($iUserId); if(!empty($asUser)) { $this->iUserId = $iUserId; $this->asUserInfo = $asUser; } } } public function getUserInfo() { return $this->asUserInfo; } public function getUserById($iUserId) { $asUsersInfo = array(); if($iUserId > 0) $asUsersInfo = $this->getUsersInfo($iUserId); return empty($asUsersInfo)?array():array_shift($asUsersInfo); } public function getUsersInfo($iUserId=-1) { //Mapping between user fields and DB fields $asSelect = array_keys($this->asUserInfo); $asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE)." AS id"; $asInfo = array( 'select' => $asSelect, 'from' => self::USER_TABLE ); if($iUserId != -1) $asInfo['constraint'] = array(Db::getId(self::USER_TABLE) => $iUserId); return $this->oDb->selectRows($asInfo); } public function getLang() { return $this->asUserInfo['language']; } private function addUser($sEmail, $sLang, $sTimezone, $sNickName='') { $bSuccess = false; $sLangId = ''; $iUserId = $this->oDb->insertRow( self::USER_TABLE, array('email'=>$sEmail, 'language'=>$sLang, 'timezone'=>$sTimezone) ); if($iUserId == 0) $sLangId = 'error.commit_db'; else $bSuccess = true; //Extra optional values if($bSuccess) { $this->updateNickname($sNickName); $this->updateGravatar($iUserId, $sEmail); } return Livetrail::getResult($bSuccess, $sLangId, [Db::getId(self::USER_TABLE) => $iUserId]); } public function setSubscription($bSubscribed) { if($this->getUserId() > 0) { $iSubscribed = $bSubscribed?1:0; $iUserId = $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('subscribed'=>$iSubscribed)); if(!$iUserId) return Livetrail::getResult(false, 'error.commit_db'); $this->asUserInfo['subscribed'] = $iSubscribed; return Livetrail::getResult(true, $iSubscribed?'account.subscribed':'account.unsubscribed'); } } public function getSubscribedUsersInfo() { $asSelect = array_keys($this->asUserInfo); $asSelect[array_search('id', $asSelect)] = Db::getId(self::USER_TABLE).' AS id'; return $this->oDb->selectRows(array( 'select'=>$asSelect, 'from'=>self::USER_TABLE, 'constraint'=>array('subscribed'=>self::USER_SUBSCRIBED) )); } public function login($sEmail, $sPassword, $sLang, $sTimezone, $sNickName='') { $bSuccess = false; $bSubscribe = false; $sLangId = ''; $sEmail = strtolower(trim($sEmail)); //Check email value if(!filter_var($sEmail, FILTER_VALIDATE_EMAIL)) { $sLangId = 'account.invalid_email'; } else { //Check Email presence in DB $asDBUser = $this->oDb->selectRow( self::USER_TABLE, array('email' => $sEmail), array(Db::getId(self::USER_TABLE), 'password', 'clearance') ); $iUserId = $asDBUser[Db::getId(self::USER_TABLE)] ?? 0; //User exists if($iUserId > 0) { //Is Admin if($asDBUser['clearance'] >= self::CLEARANCE_ADMIN) { //Request a password if($sPassword === '') $sLangId = empty($asDBUser['password'])?'account.set_password':'account.password_required'; //Set password elseif(empty($asDBUser['password'])) { if(!$this->oDb->updateRow(self::USER_TABLE, $iUserId, array('password' => password_hash($sPassword, PASSWORD_DEFAULT)))) $sLangId = 'error.commit_db'; else { $sLangId = 'account.password_set'; $bSuccess = true; } } //Check password elseif(password_verify($sPassword, $asDBUser['password'])) { $bSuccess = true; $sLangId = 'account.logged_in'; } else $sLangId = 'account.invalid_credentials'; } else { $bSuccess = true; $sLangId = 'account.logged_in'; } } else { //Unknown user, create it $asAddResult = $this->addUser($sEmail, $sLang, $sTimezone, $sNickName); $bSuccess = $asAddResult['result']; $bSubscribe = $bSuccess; $sLangId = $bSuccess?'':$asAddResult['desc_lang_id']; $iUserId = $asAddResult['data'][Db::getId(self::USER_TABLE)] ?? 0; } } if($bSuccess) { $this->setUserId($iUserId); $this->setSession(); $this->setTokenCookie(); } return Livetrail::getResult($bSuccess, $sLangId, array('subscribe'=>$bSubscribe)); } public function logout() { if($this->getUserId() > 0) $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('token' => '', 'token_exp' => '0000-00-00 00:00:00')); $this->clearSession(); $this->clearCookie(); $this->setUserId(0); return Livetrail::getResult(true, 'account.logged_out'); } public function updateNickname($sNickname) { if($this->getUserId() > 0 && $sNickname!='') $this->oDb->updateRow(self::USER_TABLE, $this->getUserId(), array('name'=>$sNickname)); } private function updateGravatar($iUserId, $sEmail) { $sImage = ($sEmail != '')?@file_get_contents('https://www.gravatar.com/avatar/'.md5($sEmail).'.png?d=404&s=24'):''; $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('gravatar' => base64_encode($sImage))); } public function checkUserClearance($iClearance) { return ($this->asUserInfo['clearance'] >= $iClearance); } public function setUserClearance($iUserId, $iClearance) { $bSuccess = false; $sLangId = ''; $asLangParams = array(); if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) $sLangId = 'error.no_auth'; else { if(!in_array($iClearance, self::CLEARANCES)) { $sLangId = 'error.impossible_value'; $asLangParams = array($iClearance, 'clearance'); } else { $iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('clearance'=>$iClearance)); if(!$iUserId) $sLangId = 'error.commit_db'; else $bSuccess = true; } } return Livetrail::getResult($bSuccess, $sLangId, array(), $asLangParams); } /* Session */ private function checkSession() { $iUserId = (int) ($_SESSION[self::SESSION_ID_USER] ?? 0); if($iUserId > 0) { $this->setUserId($iUserId); if($this->checkUserClearance(self::CLEARANCE_ADMIN) && empty($_SESSION[self::SESSION_ADMIN])) $this->logout(); } else $this->checkTokenCookie(); } private function setSession() { session_regenerate_id(true); $_SESSION[self::SESSION_ID_USER] = $this->getUserId(); $_SESSION[self::SESSION_ADMIN] = $this->checkUserClearance(self::CLEARANCE_ADMIN); } private function clearSession() { unset($_SESSION[self::SESSION_ID_USER], $_SESSION[self::SESSION_ADMIN]); } /* Cookie */ private function checkTokenCookie() { $sCookieValue = $this->getCookie(); //Check cookie structure $asToken = explode(':', $sCookieValue, 3); $iUserId = $asToken[0]; if(count($asToken) !== 3 || !ctype_digit($iUserId) || !ctype_digit($asToken[1]) || !preg_match('/^[a-f0-9]{64}$/', $asToken[2])) { if($sCookieValue !== '') $this->clearCookie(); return; } //Get user info $asUser = $this->oDb->selectRow( self::USER_TABLE, $iUserId, array('clearance', 'token', 'token_exp') ); //Check token value if(!empty($asUser) && $asUser['clearance'] == $asToken[1] && time() <= strtotime($asUser['token_exp']) && hash_equals($asUser['token'], hash('sha256', $sCookieValue)) ) { $this->setUserId($iUserId); $this->setSession(); } else $this->clearCookie(); } private function setTokenCookie() { $sToken = bin2hex(random_bytes(32)); $iClearance = $this->asUserInfo['clearance']; $sCookieValue = implode(':', [$this->getUserId(), $iClearance, $sToken]); $this->oDb->updateRow( self::USER_TABLE, $this->getUserId(), array( 'token' => hash('sha256', $sCookieValue), 'token_exp' => date(Db::TIMESTAMP_FORMAT, time() + self::COOKIE_DURATION) ) ); $this->setCookie($sCookieValue, time() + self::COOKIE_DURATION); } private function getCookie() { return $_COOKIE[self::COOKIE_TOKEN] ?? ''; } private function setCookie($sValue, $iExpires) { setcookie( self::COOKIE_TOKEN, $sValue, array( 'expires' => $iExpires, 'path' => '/', 'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'), 'httponly' => true, 'samesite' => 'Lax' ) ); } private function clearCookie() { $this->setCookie('', time() - 3600); unset($_COOKIE[self::COOKIE_TOKEN]); } }