This commit is contained in:
+198
-74
@@ -11,14 +11,17 @@ class User extends PhpObject {
|
||||
const USER_TABLE = 'users';
|
||||
|
||||
//Clearance Levels
|
||||
const USER_ACTIVE = 1;
|
||||
const USER_INACTIVE = 0;
|
||||
const CLEARANCE_USER = 0;
|
||||
const CLEARANCE_ADMIN = 9;
|
||||
const CLEARANCES = array('user'=>self::CLEARANCE_USER, 'admin'=>self::CLEARANCE_ADMIN);
|
||||
|
||||
//Cookie
|
||||
const COOKIE_ID_USER = 'subscriber';
|
||||
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(
|
||||
@@ -28,7 +31,7 @@ class User extends PhpObject {
|
||||
'email' => '',
|
||||
'language' => '',
|
||||
'timezone' => '',
|
||||
'active' => self::USER_INACTIVE,
|
||||
'subscribed'=> self::USER_UNSUBSCRIBED,
|
||||
'clearance' => self::CLEARANCE_USER
|
||||
);
|
||||
|
||||
@@ -46,8 +49,7 @@ class User extends PhpObject {
|
||||
parent::__construct(__CLASS__);
|
||||
$this->oDb = &$oDb;
|
||||
$this->setUserId(0);
|
||||
$this->asUserInfo = self::DEFAULT_USER;
|
||||
$this->checkUserCookie();
|
||||
$this->checkSession();
|
||||
}
|
||||
|
||||
public function getUserId() {
|
||||
@@ -56,9 +58,10 @@ class User extends PhpObject {
|
||||
|
||||
public function setUserId($iUserId) {
|
||||
$this->iUserId = 0;
|
||||
$this->asUserInfo = self::DEFAULT_USER;
|
||||
|
||||
if($iUserId > 0) {
|
||||
$asUser = $this->getActiveUserInfo($iUserId);
|
||||
$asUser = $this->getUserById($iUserId);
|
||||
if(!empty($asUser)) {
|
||||
$this->iUserId = $iUserId;
|
||||
$this->asUserInfo = $asUser;
|
||||
@@ -70,27 +73,23 @@ class User extends PhpObject {
|
||||
return $this->asUserInfo;
|
||||
}
|
||||
|
||||
public function getActiveUserInfo($iUserId) {
|
||||
public function getUserById($iUserId) {
|
||||
$asUsersInfo = array();
|
||||
if($iUserId > 0) $asUsersInfo = $this->getActiveUsersInfo($iUserId);
|
||||
if($iUserId > 0) $asUsersInfo = $this->getUsersInfo($iUserId);
|
||||
return empty($asUsersInfo)?array():array_shift($asUsersInfo);
|
||||
}
|
||||
|
||||
public function getActiveUsersInfo($iUserId=-1) {
|
||||
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";
|
||||
|
||||
//Non-admin cannot access clearance info
|
||||
if(!$this->checkUserClearance(self::CLEARANCE_ADMIN)) unset($asSelect['clearance']);
|
||||
|
||||
$asInfo = array(
|
||||
'select' => $asSelect,
|
||||
'from' => self::USER_TABLE,
|
||||
'constraint'=> array('active'=>self::USER_ACTIVE)
|
||||
'from' => self::USER_TABLE
|
||||
);
|
||||
if($iUserId != -1) $asInfo['constraint'][Db::getId(self::USER_TABLE)] = $iUserId;
|
||||
if($iUserId != -1) $asInfo['constraint'] = array(Db::getId(self::USER_TABLE) => $iUserId);
|
||||
|
||||
return $this->oDb->selectRows($asInfo);
|
||||
}
|
||||
@@ -99,71 +98,116 @@ class User extends PhpObject {
|
||||
return $this->asUserInfo['language'];
|
||||
}
|
||||
|
||||
public function addUser($sEmail, $sLang, $sTimezone, $sNickName='') {
|
||||
private function addUser($sEmail, $sLang, $sTimezone, $sNickName='') {
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
$sEmail = trim($sEmail);
|
||||
|
||||
//Check Email availability
|
||||
$iUserId = $this->oDb->selectValue(self::USER_TABLE, Db::getId(self::USER_TABLE), array('email'=>$sEmail, 'active'=>self::USER_ACTIVE));
|
||||
$iUserId = $this->oDb->insertRow(
|
||||
self::USER_TABLE,
|
||||
array('email'=>$sEmail, 'language'=>$sLang, 'timezone'=>$sTimezone)
|
||||
);
|
||||
|
||||
if($iUserId > 0) {
|
||||
//Just log user in
|
||||
$sDesc = 'lang:newsletter.email_exists';
|
||||
$bSuccess = true;
|
||||
if($iUserId == 0) $sDesc = 'lang:error.commit_db';
|
||||
else $bSuccess = true;
|
||||
|
||||
//Extra optional values
|
||||
if($bSuccess) {
|
||||
$this->updateNickname($sNickName);
|
||||
$this->updateGravatar($iUserId, $sEmail);
|
||||
}
|
||||
|
||||
return Spot::getResult($bSuccess, $sDesc, [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 Spot::getResult(false, 'lang:error.commit_db');
|
||||
$this->asUserInfo['subscribed'] = $iSubscribed;
|
||||
return Spot::getResult(true, $iSubscribed?'lang:account.subscribed':'lang: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;
|
||||
$sDesc = '';
|
||||
$sEmail = strtolower(trim($sEmail));
|
||||
|
||||
//Check email value
|
||||
if(!filter_var($sEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
$sDesc = 'lang:account.invalid_email';
|
||||
}
|
||||
else {
|
||||
//Add/Reactivate user
|
||||
$iUserId = $this->oDb->insertUpdateRow(
|
||||
//Check Email presence in DB
|
||||
$asDBUser = $this->oDb->selectRow(
|
||||
self::USER_TABLE,
|
||||
array('email'=>$sEmail, 'language'=>$sLang, 'timezone'=>$sTimezone, 'active'=>self::USER_ACTIVE),
|
||||
array('email')
|
||||
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 === '') $sDesc = empty($asDBUser['password'])?'lang:account.set_password':'lang:account.password_required';
|
||||
|
||||
//Set password
|
||||
elseif(empty($asDBUser['password'])) {
|
||||
if(!$this->oDb->updateRow(self::USER_TABLE, $iUserId, array('password' => password_hash($sPassword, PASSWORD_DEFAULT)))) $sDesc = 'lang:error.commit_db';
|
||||
else {
|
||||
$sDesc = 'lang:account.password_set';
|
||||
$bSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
if($iUserId==0) $sDesc = 'lang:error.commit_db';
|
||||
else {
|
||||
$sDesc = 'lang:newsletter.subscribed';
|
||||
$bSuccess = true;
|
||||
//Check password
|
||||
elseif(password_verify($sPassword, $asDBUser['password'])) {
|
||||
$bSuccess = true;
|
||||
$sDesc = 'lang:account.logged_in';
|
||||
}
|
||||
else $sDesc = 'lang:account.invalid_credentials';
|
||||
|
||||
//die(print_r(['provided_pass'=>$sPassword, 'db_pass'=>$asDBUser['password']], true));
|
||||
}
|
||||
else $bSuccess = true;
|
||||
}
|
||||
else {
|
||||
//Unknown user, create it
|
||||
$asAddResult = $this->addUser($sEmail, $sLang, $sTimezone, $sNickName);
|
||||
$bSuccess = $asAddResult['result'];
|
||||
$sDesc = $bSuccess?'subscribe_user':$asAddResult['desc'];
|
||||
$iUserId = $asAddResult['data'][Db::getId(self::USER_TABLE)] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
if($bSuccess) {
|
||||
$this->setUserId($iUserId);
|
||||
|
||||
//Set Cookie (valid 1 year)
|
||||
$this->updateCookie(self::COOKIE_DURATION);
|
||||
|
||||
//Update Nickname if user has already posted
|
||||
$this->updateNickname($sNickName);
|
||||
|
||||
//Retrieve Gravatar image
|
||||
$this->updateGravatar($iUserId, $sEmail);
|
||||
$this->setSession();
|
||||
$this->setTokenCookie();
|
||||
}
|
||||
|
||||
return Spot::getResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
public function removeUser($iUserId=0) {
|
||||
$iUserId = ($iUserId > 0)?$iUserId:$this->getUserId();
|
||||
$bSelf = ($iUserId == $this->getUserId());
|
||||
$bSuccess = false;
|
||||
$sDesc = '';
|
||||
|
||||
if($bSelf || $this->checkUserClearance(self::CLEARANCE_ADMIN)) {
|
||||
if($this->getUserId() > 0) {
|
||||
$iUserId = $this->oDb->updateRow(self::USER_TABLE, $iUserId, array('active' => self::USER_INACTIVE));
|
||||
if($iUserId==0) $sDesc = 'lang:error.commit_db';
|
||||
else {
|
||||
$sDesc = 'lang:newsletter.unsubscribed';
|
||||
if($bSelf) $this->updateCookie(-60 * 60); //Set Cookie in the past, deleting it
|
||||
$bSuccess = true;
|
||||
}
|
||||
}
|
||||
else $sDesc = 'lang:newsletter.unknown_email';
|
||||
}
|
||||
else $sDesc = 'lang:error.no_auth';
|
||||
|
||||
return Spot::getResult($bSuccess, $sDesc);
|
||||
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 Spot::getResult(true, 'lang:account.logged_out');
|
||||
}
|
||||
|
||||
public function updateNickname($sNickname) {
|
||||
@@ -175,15 +219,6 @@ class User extends PhpObject {
|
||||
$this->oDb->updateRow(self::USER_TABLE, $iUserId, array('gravatar' => base64_encode($sImage)));
|
||||
}
|
||||
|
||||
private function checkUserCookie() {
|
||||
if(isset($_COOKIE[self::COOKIE_ID_USER])){
|
||||
$this->setUserId($_COOKIE[self::COOKIE_ID_USER]);
|
||||
|
||||
//Extend cookie life
|
||||
if($this->getUserId() > 0) $this->updateCookie(self::COOKIE_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkUserClearance($iClearance)
|
||||
{
|
||||
return ($this->asUserInfo['clearance'] >= $iClearance);
|
||||
@@ -206,7 +241,96 @@ class User extends PhpObject {
|
||||
return Spot::getResult($bSuccess, $sDesc);
|
||||
}
|
||||
|
||||
private function updateCookie($iDeltaTime) {
|
||||
setcookie(self::COOKIE_ID_USER, ($iDeltaTime < 0)?'':$this->getUserId(), array('samesite' => 'Lax', 'expires' => time() + $iDeltaTime));
|
||||
/* 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]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user