Hash($Password, $Salt) == $StoredHash;
}
function GetSalt(): string
{
mt_srand(intval(microtime(true)) * 100000 + memory_get_usage(true));
return sha1(uniqid(mt_rand(), true));
}
}
// TODO: Make User class more general without dependencies on System, Mail, Log
class User extends Model
{
public array $Roles = array();
public array $User = array();
public int $OnlineStateTimeout;
public array $PermissionCache = array();
public array $PermissionGroupCache = array();
public array $PermissionGroupCacheOp = array();
public PasswordHash $PasswordHash;
function __construct(System $System)
{
parent::__construct($System);
$this->OnlineStateTimeout = 600; // in seconds
$this->PasswordHash = new PasswordHash();
$this->User = array('Id' => null);
}
static function GetModelDesc(): ModelDesc
{
$Desc = new ModelDesc(self::GetClassName());
$Column = $Desc->AddString('Login');
$Column->Unique = true;
$Column = $Desc->AddString('Name');
$Column->Unique = true;
$Desc->AddString('Password');
$Desc->AddString('Salt');
$Desc->AddString('Email');
$Column = $Desc->AddString('LastIpAddress');
$Column->HasDefault = true;
$Column->Nullable = true;
$Column = $Desc->AddDateTime('LastLoginTime');
$Column->Nullable = true;
$Column->HasDefault = true;
$Desc->AddDateTime('RegistrationTime');
$Desc->AddBoolean('Locked');
$Column = $Desc->AddString('InitPassword');
$Column->Nullable = true;
$Column->HasDefault = true;
return $Desc;
}
function Check(): void
{
$SID = session_id();
// Lookup user record
$Query = $this->Database->select('UserOnline', '*', 'SessionId="'.$SID.'"');
if ($Query->num_rows > 0)
{
// Refresh time of last access
$this->Database->update('UserOnline', '`SessionId`="'.$SID.'"', array('ActivityTime' => 'NOW()'));
} else $this->Database->insert('UserOnline', array('SessionId' => $SID,
'User' => null, 'LoginTime' => 'NOW()', 'ActivityTime' => 'NOW()',
'IpAddress' => GetRemoteAddress(), 'HostName' => gethostbyaddr(GetRemoteAddress()),
'ScriptName' => $_SERVER['PHP_SELF'], 'StayLogged' => 0, 'StayLoggedHash' => ''));
// Logged permanently?
if (array_key_exists('LoginHash', $_COOKIE))
{
$DbResult = $this->Database->query('SELECT * FROM `UserOnline` WHERE `User`='.$_COOKIE['LoginUserId'].
' AND `StayLogged`=1 AND SessionId!="'.$SID.'"');
if ($DbResult->num_rows > 0)
{
$DbRow = $DbResult->fetch_assoc();
if (sha1($_COOKIE['LoginUserId'].$DbRow['StayLoggedHash']) == $_COOKIE['LoginHash'])
{
$this->Database->query('DELETE FROM `UserOnline` WHERE `SessionId`="'.$SID.'"');
$this->Database->query('UPDATE `UserOnline` SET `SessionId`="'.$SID.'" WHERE `Id`='.$DbRow['Id']);
}
}
}
// Check login
$Query = $this->Database->select('UserOnline', '*', '`SessionId`="'.$SID.'"');
$Row = $Query->fetch_assoc();
if ($Row['User'] != '')
{
$Query = $this->Database->query('SELECT `User`.* FROM `User` WHERE `User`.`Id`='.$Row['User']);
$this->User = $Query->fetch_assoc();
$Result = USER_LOGGED;
} else
{
$Query = $this->Database->select('User', '*', 'Id IS NULL');
$this->User = array('Id' => null);
$Result = USER_NOT_LOGGED;
}
// Remove nonactive users
$DbResult = $this->Database->select('UserOnline', '`Id`, `User`', '(`ActivityTime` < DATE_SUB(NOW(), INTERVAL '.$this->OnlineStateTimeout.' SECOND)) AND (`StayLogged` = 0)');
while ($DbRow = $DbResult->fetch_array())
{
$this->Database->delete('UserOnline', 'Id='.$DbRow['Id']);
if ($DbRow['User'] != null) ModuleLog::Cast($this->System->GetModule('Log'))->NewRecord('User', 'Logout');
}
//$this->LoadPermission($this->User['Role']);
// Role and permission
//$this->LoadRoles();
}
function Register(string $Login, string $Password, string $Password2, string $Email, string $Name): string
{
if (($Email == '') || ($Login == '') || ($Password == '') || ($Password2 == '') || ($Name == '')) $Result = DATA_MISSING;
else if ($Password != $Password2) $Result = PASSWORDS_UNMATCHED;
else
{
// Is user registred yet?
$Query = $this->Database->select('User', '*', 'Login = "'.$Login.'"');
if ($Query->num_rows > 0) $Result = LOGIN_USED;
else
{
$Query = $this->Database->select('User', '*', 'Name = "'.$Name.'"');
if ($Query->num_rows > 0) $Result = NAME_USED;
else
{
$Query = $this->Database->select('User', '*', 'Email = "'.$Email.'"');
if ($Query->num_rows > 0) $Result = EMAIL_USED;
else
{
$PasswordHash = new PasswordHash();
$Salt = $PasswordHash->GetSalt();
$this->Database->insert('User', array('Name' => $Name, 'Login' => $Login,
'Password' => $PasswordHash->Hash($Password, $Salt), 'Salt' => $Salt,
'Email' => $Email, 'RegistrationTime' => 'NOW()',
'Locked' => 1));
$UserId = $this->Database->insert_id;
$PermissionGroup = new PermissionGroup($this->System);
$this->Database->insert('PermissionUserAssignment', array('User' => $UserId,
'AssignedGroup' => $PermissionGroup->GetItemBySysName('registered-users')));
$NewPassword = substr(sha1(strtoupper($Login)), 0, 7);
// Send activation mail to user email
$ServerURL = 'https://'.$this->System->Config['Web']['Host'].$this->System->Config['Web']['RootFolder'];
$Mail = new Mail();
$Mail->Subject = 'Registrace nového účtu';
$Mail->AddBody('Provedli jste registraci nového účtu na serveru '.$ServerURL.'".'.
'
\nPokud jste tak neučinili, měli by jste tento email ignorovat.
\n\n'.
'Váš účet je: '.$Login."\n
Pro dokončení registrace klikněte na tento odkaz: ".''.$ServerURL.'/?Action=UserRegisterConfirm&User='.
$UserId.'&H='.$NewPassword.'.'."\n
\n\n'.
'
Na tento email neodpovídejte.", 'text/html');
$Mail->AddTo($Email, $Name);
$Mail->From = $this->System->Config['Web']['Title'].' ';
$Mail->Send();
$Result = USER_REGISTRATED;
ModuleLog::Cast($this->System->GetModule('Log'))->NewRecord('User', 'NewRegistration', $Login);
}
}
}
}
return $Result;
}
function RegisterConfirm(string $Id, string $Hash): string
{
$DbResult = $this->Database->select('User', 'Id, Login, Password', 'Id = '.$Id);
if ($DbResult->num_rows > 0)
{
$Row = $DbResult->fetch_array();
$NewPassword = substr(sha1(strtoupper($Row['Login'])), 0, 7);
if ($Hash == $NewPassword)
{
$this->Database->update('User', 'Id='.$Row['Id'], array('Locked' => 0));
$Output = USER_REGISTRATION_CONFIRMED;
ModuleLog::Cast($this->System->GetModule('Log'))->NewRecord('User', 'RegisterConfirm', 'Login='.
$Row['Login'].', Id='.$Row['Id']);
} else $Output = PASSWORDS_UNMATCHED;
} else $Output = USER_NOT_FOUND;
return $Output;
}
function Login(string $Login, string $Password, bool $StayLogged = false): string
{
if ($StayLogged) $StayLogged = 1; else $StayLogged = 0;
$SID = session_id();
$Query = $this->Database->select('User', '*', 'Login="'.$Login.'"');
if ($Query->num_rows > 0)
{
$Row = $Query->fetch_assoc();
$PasswordHash = new PasswordHash();
if (!$PasswordHash->Verify($Password, $Row['Salt'], $Row['Password'])) $Result = BAD_PASSWORD;
else if ($Row['Locked'] == 1) $Result = ACCOUNT_LOCKED;
else
{
$this->Database->update('User', 'Id='.$Row['Id'], array('LastLoginTime' => 'NOW()',
'LastIpAddress' => GetRemoteAddress()));
$Hash = new PasswordHash();
$StayLoggedSalt = $Hash->GetSalt();
$this->Database->update('UserOnline', 'SessionId="'.$SID.'"', array(
'User' => $Row['Id'], 'StayLogged' => $StayLogged, 'StayLoggedHash' => $StayLoggedSalt));
if ($StayLogged)
{
setcookie('LoginUserId', $Row['Id'], time()+365*24*60*60, $this->System->Link('/'));
setcookie('LoginHash', sha1($Row['Id'].$StayLoggedSalt), time()+365*24*60*60, $this->System->Link('/'));
} else
{
setcookie('LoginUserId', '', time() - 3600, $this->System->Link('/'));
setcookie('LoginHash', '', time() - 3600, $this->System->Link('/'));
}
$Result = USER_LOGGED_IN;
$this->Check();
ModuleLog::Cast($this->System->GetModule('Log'))->NewRecord('User', 'Login', 'Login='.$Login.',Host='.gethostbyaddr(GetRemoteAddress()));
}
} else $Result = USER_NOT_REGISTRED;
// Wait some minimal time if not able to log in to avoid brute forcing passwords
if ($Result != USER_LOGGED_IN) sleep(1);
return $Result;
}
function Logout(): string
{
$SID = session_id();
$this->Database->update('UserOnline', 'SessionId="'.$SID.'"', array('User' => null));
ModuleLog::Cast($this->System->GetModule('Log'))->NewRecord('User', 'Logout', $this->User['Login']);
$this->Check();
return USER_LOGGED_OUT;
}
function LoadRoles()
{
$this->Roles = array();
$DbResult = $this->Database->select('UserRole', '*');
while ($DbRow = $DbResult->fetch_array())
{
$this->Roles[] = $DbRow;
}
}
function LoadPermission($Role)
{
$this->User['Permission'] = array();
$DbResult = $this->Database->query('SELECT `UserRolePermission`.*, `PermissionOperation`.`Description` FROM `UserRolePermission` JOIN `PermissionOperation` ON `PermissionOperation`.`Id` = `UserRolePermission`.`Operation` WHERE `UserRolePermission`.`Role` = '.$Role);
if ($DbResult->num_rows > 0)
while ($DbRow = $DbResult->fetch_array())
{
$this->User['Permission'][$DbRow['Operation']] = $DbRow;
}
}
function PermissionMatrix(): array
{
$Result = array();
$DbResult = $this->Database->query('SELECT `UserRolePermission`.*, `PermissionOperation`.`Description`, `UserRole`.`Title` FROM `UserRolePermission` LEFT JOIN `PermissionOperation` ON `PermissionOperation`.`Id` = `UserRolePermission`.`Operation` LEFT JOIN `UserRole` ON `UserRole`.`Id` = `UserRolePermission`.`Role`');
while ($DbRow = $DbResult->fetch_array())
{
$Value = '';
if ($DbRow['Read']) $Value .= 'R';
if ($DbRow['Write']) $Value .= 'W';
$Result[$DbRow['Description']][$DbRow['Title']] = $Value;
}
return $Result;
}
function CheckGroupPermission(string $GroupId, string $OperationId): bool
{
$PermissionExists = false;
// First try to check cache group-group relation
if (array_key_exists($GroupId, $this->PermissionGroupCache))
{
$PermissionExists = true;
} else
{
$this->PermissionGroupCache[$GroupId] = array();
// If no permission combination exists in cache, do new check of database items
$DbResult = $this->Database->select('PermissionGroupAssignment', '*', '(`Group`="'.$GroupId.
'") AND (`AssignedGroup` IS NOT NULL)');
while ($DbRow = $DbResult->fetch_array())
{
$this->PermissionGroupCache[$GroupId][] = $DbRow;
}
$PermissionExists = true;
}
if ($PermissionExists)
{
foreach ($this->PermissionGroupCache[$GroupId] as $DbRow)
{
if ($DbRow['AssignedGroup'] != '')
{
if ($this->CheckGroupPermission($DbRow['AssignedGroup'], $OperationId) == true) return true;
}
}
}
// Check group-operation relation
if (array_key_exists($GroupId.','.$OperationId, $this->PermissionGroupCacheOp))
{
$PermissionExists = true;
} else
{
// If no permission combination exists in cache, do new check of database items
$DbResult = $this->Database->select('PermissionGroupAssignment', '*', '`Group`="'.$GroupId.'" AND `AssignedOperation`="'.$OperationId.'"');
if ($DbResult->num_rows > 0) $this->PermissionGroupCacheOp[$GroupId.','.$OperationId] = true;
else $this->PermissionGroupCacheOp[$GroupId.','.$OperationId] = false;
$PermissionExists = true;
}
if ($PermissionExists)
{
return $this->PermissionGroupCacheOp[$GroupId.','.$OperationId];
}
return false;
}
function CheckPermission(string $Module, string $Operation, string $ItemType = '', int $ItemIndex = 0): bool
{
// Get module id
$DbResult = $this->Database->select('Module', 'Id', '`Name`="'.$Module.'"');
if ($DbResult->num_rows > 0)
{
$DbRow = $DbResult->fetch_assoc();
$ModuleId = $DbRow['Id'];
} else return false;
// First try to check cache
if (in_array(array($Module, $Operation, $ItemType, $ItemType), $this->PermissionCache))
{
$OperationId = array_search(array($Module, $Operation, $ItemType, $ItemIndex), $this->PermissionCache);
$PermissionExists = is_numeric($OperationId);
} else
{
// If no permission combination exists in cache, do new check of database items
$DbResult = $this->Database->select('PermissionOperation', 'Id', '(`Module`="'.$ModuleId.
'") AND (`Item`="'.$ItemType.'") AND (`ItemId`='.$ItemIndex.') AND (`Operation`="'.$Operation.'")');
if ($DbResult->num_rows > 0)
{
$DbRow = $DbResult->fetch_array();
$OperationId = $DbRow['Id'];
$this->PermissionCache[$DbRow['Id']] = array($Module, $Operation, $ItemType, $ItemIndex);
$PermissionExists = true;
} else
{
$this->PermissionCache[count($this->PermissionCache).'_'] = array($Module, $Operation, $ItemType, $ItemIndex);
$PermissionExists = false;
}
}
if ($PermissionExists)
{
if ($this->User == null or $this->User['Id'] == null) $UserCondition = '(`User` IS NULL)';
else $UserCondition = '(`User`="'.$this->User['Id'].'")';
// Check user-operation relation
$DbResult = $this->Database->select('PermissionUserAssignment', '*', $UserCondition.' AND (`AssignedOperation`="'.$OperationId.'")');
if ($DbResult->num_rows > 0) return true;
// Check user-group relation
$DbResult = $this->Database->select('PermissionUserAssignment', 'AssignedGroup', '(`AssignedGroup` IS NOT NULL) AND '.$UserCondition);
while ($DbRow = $DbResult->fetch_array())
{
if ($this->CheckGroupPermission($DbRow['AssignedGroup'], $OperationId) == true) return true;
}
return false;
} else return false;
}
function PasswordRecoveryRequest(string $Login, string $Email): string
{
$DbResult = $this->Database->select('User', 'Login, Name, Id, Email, Password', '`Login`="'.$Login.'" AND `Email`="'.$Email.'"');
if ($DbResult->num_rows > 0)
{
$Row = $DbResult->fetch_array();
$NewPassword = substr(sha1(strtoupper($Row['Login'])), 0, 7);
$ServerURL = 'https://'.$this->System->Config['Web']['Host'].$this->System->Config['Web']['RootFolder'];
$Mail = new Mail();
$Mail->Subject = 'Obnova hesla';
$Mail->From = $this->System->Config['Web']['Title'].' ';
$Mail->AddTo($Row['Email'], $Row['Name']);
$Mail->AddBody('Požádali jste o zaslání nového hesla na serveru '.$ServerURL.'".
\n'.
"Pokud jste tak neučinili, měli by jste tento email ignorovat.
\n\nVaše nové heslo k účtu ".
$Row['Login'].' je: '.$NewPassword."\n
".
'Pro aktivaci tohoto hesla klikněte na tento odkaz.'."\n
".
"Po přihlášení si prosím změňte heslo na nové.\n\n
Na tento email neodpovídejte.", 'text/html');
$Mail->Send();
$Output = USER_PASSWORD_RECOVERY_SUCCESS;
ModuleLog::Cast($this->System->GetModule('Log'))->NewRecord('User', 'PasswordRecoveryRequest', 'Login='.$Login.',Email='.$Email);
} else $Output = USER_PASSWORD_RECOVERY_FAIL;
return $Output;
}
function PasswordRecoveryConfirm(string $Id, string $Hash, string $NewPassword): string
{
$DbResult = $this->Database->select('User', 'Id, Login, Password', 'Id = '.$Id);
if ($DbResult->num_rows > 0)
{
$Row = $DbResult->fetch_array();
$NewPassword2 = substr(sha1(strtoupper($Row['Login'])), 0, 7);
if (($NewPassword == $NewPassword2) and ($Hash == $Row['Password']))
{
$PasswordHash = new PasswordHash();
$Salt = $PasswordHash->GetSalt();
$this->Database->update('User', 'Id='.$Row['Id'], array('Password' => $PasswordHash->Hash($NewPassword, $Salt),
'Salt' => $Salt, 'Locked' => 0));
$Output = USER_PASSWORD_RECOVERY_CONFIRMED;
ModuleLog::Cast($this->System->GetModule('Log'))->NewRecord('User', 'PasswordRecoveryConfirm', 'Login='.$Row['Login']);
} else $Output = PASSWORDS_UNMATCHED;
} else $Output = USER_NOT_FOUND;
return $Output;
}
function CheckToken(string $Module, string $Operation, string $Token): bool
{
$DbResult = $this->Database->select('APIToken', 'User', '`Token`="'.$Token.'"');
if ($DbResult->num_rows > 0)
{
$DbRow = $DbResult->fetch_assoc();
$User = new User($this->System);
$User->User = array('Id' => $DbRow['User']);
return $User->CheckPermission($Module, $Operation);
} else return false;
}
}
class UserOnline extends Model
{
static function GetModelDesc(): ModelDesc
{
$Desc = new ModelDesc(self::GetClassName());
$Desc->Memory = true;
$Desc->AddReference('User', User::GetClassName(), true);
$Desc->AddDateTime('ActivityTime');
$Desc->AddDateTime('LoginTime');
$Desc->AddString('SessionId');
$Desc->AddString('IpAddress');
$Desc->AddString('HostName');
$Desc->AddString('ScriptName');
$Desc->AddBoolean('StayLogged');
$Desc->AddString('StayLoggedHash');
return $Desc;
}
}
class PermissionGroup extends Model
{
static function GetModelDesc(): ModelDesc
{
$Desc = new ModelDesc(self::GetClassName());
$Desc->AddString('Description');
$Desc->AddString('SysName');
$Desc->DefaultValuesMethod = 'GetDefaultValues';
return $Desc;
}
static function GetDefaultValues(): array
{
return array(
array('Id' => 1, 'Description' => 'Ostatní uživatelé', 'SysName' => 'other-users'),
array('Id' => 2, 'Description' => 'Registrovaní uživatelé', 'SysName' => 'registered-users'),
array('Id' => 3, 'Description' => 'Správci', 'SysName' => 'admins'),
);
}
function GetItemBySysName(string $Name): int
{
$DbResult = $this->Database->select('PermissionGroup', 'Id', '`SysName`="'.$Name.'"');
if ($DbResult->num_rows > 0)
{
$DbRow = $DbResult->fetch_assoc();
return $DbRow['Id'];
} else return 0;
}
}
class PermissionGroupAssignment extends Model
{
static function GetModelDesc(): ModelDesc
{
$Desc = new ModelDesc(self::GetClassName());
$Desc->AddReference('Group', PermissionGroup::GetClassName());
$Desc->AddReference('AssignedGroup', PermissionGroup::GetClassName(), true);
$Desc->AddReference('AssignedOperation', PermissionOperation::GetClassName(), true);
return $Desc;
}
}
class PermissionOperation extends Model
{
static function GetModelDesc(): ModelDesc
{
$Desc = new ModelDesc(self::GetClassName());
$Desc->AddReference('Module', Module::GetClassName());
$Desc->AddString('Operation');
$Desc->AddString('Item');
$Desc->AddInteger('ItemId');
$Desc->Indices = array('Operation', 'Item', 'ItemId');
return $Desc;
}
}
class PermissionUserAssignment extends Model
{
static function GetModelDesc(): ModelDesc
{
$Desc = new ModelDesc(self::GetClassName());
$Desc->AddReference('User', User::GetClassName());
$Desc->AddReference('AssignedGroup', PermissionGroup::GetClassName(), true);
$Desc->AddReference('AssignedOperation', PermissionOperation::GetClassName(), true);
return $Desc;
}
}