lib/Ajde/User.php
<?php
abstract class Ajde_User extends Ajde_Model
{
protected $_autoloadParents = false;
protected $_displayField = 'fullname';
public $usernameField = 'username';
public $passwordField = 'password';
const USERGROUP_USERS = 1;
const USERGROUP_ADMINS = 2;
const USERGROUP_CLIENTS = 3;
const USERGROUP_EMPLOYEES = 4;
public $defaultUserGroup = self::USERGROUP_USERS;
protected $cookieLifetime = 30; // in days
private static $_user;
/**
* @return UserModel
*/
public static function getLoggedIn()
{
if (!isset(self::$_user)) {
$session = new Ajde_Session('user');
if ($session->has('model')) {
$user = $session->getModel('model');
self::$_user = $user;
} else {
self::$_user = false;
}
}
return self::$_user;
}
public static function isAdmin()
{
return ($user = self::getLoggedIn()) && (string) self::getLoggedIn()->getUsergroup() == self::USERGROUP_ADMINS;
}
public static function isDebugger()
{
return ($user = self::getLoggedIn()) && $user->getDebug();
}
public static function isTester()
{
return ($user = self::getLoggedIn()) && $user->getTester();
}
public function hasPassword()
{
return !$this->verifyHash('');
}
public function loadByCredentials($username, $password)
{
if (empty($username) || empty($password)) {
return false;
}
$sql = 'SELECT * FROM '.$this->_table.' WHERE '.$this->usernameField.' = ? LIMIT 1';
$values = [$username];
$user = $this->_load($sql, $values);
if ($user === false) {
return false;
}
return $this->verifyHash($password) ? $user : false;
}
/**
* @return UsergroupModel:
*/
public function getUsergroup()
{
$this->loadParent('usergroup');
return $this->get('usergroup');
}
public function createHash($password)
{
// @see http://net.tutsplus.com/tutorials/php/understanding-hash-functions-and-keeping-passwords-safe/
if (CRYPT_BLOWFISH !== 1) {
Ajde_Dump::warn('BLOWFISH algorithm not available for hashing, using MD5 instead');
// Use MD5
$algo = '$1';
$cost = '';
$unique_salt = $this->generateSecret(12);
} else {
// Use BLOWFISH
$algo = '$2a';
$cost = '$10';
$unique_salt = $this->generateSecret(22);
}
$hash = crypt($password, $algo.$cost.'$'.$unique_salt);
if (empty($hash)) {
// TODO:
throw new Ajde_Exception('crypt() algorithm failed');
}
return $hash;
}
public function verifyHash($password)
{
$hash = $this->get($this->passwordField);
if (empty($hash)) {
return false;
}
if (CRYPT_BLOWFISH !== 1) {
// Use MD5
$full_salt = substr($hash, 0, 15);
} else {
// Use BLOWFISH
$full_salt = substr($hash, 0, 29);
}
$new_hash = crypt($password, $full_salt);
return $hash == $new_hash;
}
public function login()
{
if (empty($this->_data)) {
// TODO:
throw new Ajde_Exception('Invalid user object');
}
$session = new Ajde_Session('user');
$session->setModel('model', $this);
self::$_user = $this;
}
public function logout()
{
// First destroy current session
// TODO: overhead to call session_regenerate_id? is it not required??
//session_regenerate_id();
$session = new Ajde_Session('user');
$session->destroy();
$cookie = new Ajde_Cookie(config('app.id').'_user');
$cookie->destroy();
self::$_user = null;
}
public function refresh()
{
$this->loadByPK($this->getPK());
}
public function generateSecret($length = 255)
{
return substr(sha1(mt_rand()), 0, $length);
}
public function add($username, $password)
{
$hash = $this->createHash($password);
$this->populate([
$this->usernameField => $username,
$this->passwordField => $hash,
'usergroup' => $this->defaultUserGroup,
'secret' => $this->generateSecret(),
]);
return $this->insert();
}
public function storeCookie($includeDomain = true)
{
$hash = $this->getCookieHash($includeDomain);
$cookieValue = $this->getPK().':'.$hash;
$cookie = new Ajde_Cookie(config('app.id').'_user', true);
$cookie->setLifetime($this->cookieLifetime);
$cookie->set('auth', $cookieValue);
return true;
}
public function getCookieHash($includeDomain = true)
{
if (empty($this->_data)) {
// TODO:
throw new Ajde_Exception('Invalid user object');
}
if (!in_array('sha256', hash_algos())) {
// TODO:
throw new Ajde_Exception('SHA-256 algorithm not available for hashing');
}
$userSecret = $this->get('secret');
$appSecret = config('security.secret');
if ($includeDomain) {
$hash = hash('sha256', $userSecret.$appSecret.$_SERVER['REMOTE_ADDR'].$_SERVER['HTTP_USER_AGENT']);
} else {
$hash = hash('sha256', $userSecret.$appSecret);
}
if (empty($hash)) {
// TODO:
throw new Ajde_Exception('SHA-256 algorithm failed');
}
return $hash;
}
public function verifyCookie($includeDomain = true)
{
$cookie = new Ajde_Cookie(config('app.id').'_user', true);
if (!$cookie->has('auth')) {
return false;
}
$auth = $cookie->get('auth');
list($uid, $hash) = explode(':', $auth);
if (!$this->loadByPK($uid)) {
return false;
}
if ($this->getCookieHash($includeDomain) === $hash) {
$this->login();
Ajde_Session_Flash::alert(sprintf(trans('Welcome back %s'), $this->getFullname()));
Ajde_Cache::getInstance()->disable();
} else {
return false;
}
}
public function canChangeEmailTo($newEmail)
{
if ($this->isFieldEncrypted('email')) {
$newEmail = $this->doEncrypt($newEmail);
}
$values = [$newEmail, $this->getPK()];
$sql = 'SELECT * FROM '.$this->_table.' WHERE email = ? AND id != ? LIMIT 1';
return !$this->_load($sql, $values, false);
}
public function canChangeUsernameTo($newUsername)
{
if ($this->isFieldEncrypted($this->usernameField)) {
$newUsername = $this->doEncrypt($newUsername);
}
$values = [$newUsername, $this->getPK()];
$sql = 'SELECT * FROM '.$this->_table.' WHERE '.$this->usernameField.' = ? AND id != ? LIMIT 1';
return !$this->_load($sql, $values, false);
}
public function resetUser()
{
if (!$this->hasNotEmpty('email')) {
return false;
}
$resetHash = $this->getResetHash();
$this->set('reset_hash', $resetHash);
$this->save();
$this->sendResetMail($resetHash);
return $resetHash;
}
public function sendResetMail($hash)
{
// @todo exception
throw new Ajde_Exception('Please implement sendResetMail in UserModel');
}
public function getResetHash()
{
if (empty($this->_data)) {
// TODO:
throw new Ajde_Exception('Invalid user object');
}
if (!in_array('sha256', hash_algos())) {
// TODO:
throw new Ajde_Exception('SHA-256 algorithm not available for hashing');
}
$userSecret = $this->get('secret');
$appSecret = config('security.secret');
$hash = strtotime('+1 month').':'.hash('sha256', $userSecret.$appSecret.microtime().rand());
if (empty($hash)) {
// TODO:
throw new Ajde_Exception('SHA-256 algorithm failed');
}
return $hash;
}
}