/*************************************************
* users.class.php --
* - Active user interface, cannot be substituted as generic comparison objects.
* Built in controls to maintain, restrict, check & even alter access, automatically
* syncronizing the database in the process
*
* Copyright(c) 2008 and beyond, Vanguard Technical and Media Services
* - Author Bryan Steimel (04.02.08)
*************************************************/
// depends, db.class.php(db.abstract.php) - db.tables.php
define('USERS_TABLE_USER', 'ueas_user');
define('UACCOUNT_ATTEMPTS_LIMIT', 10);
define('UACCOUNT_ATTEMPTS_LOCK', 60*60*6); // SECONDS! 6 hours.. failed attempts count reset after this same time period..
define('UACCOUNT_PWDMIN', 8);
define('UACCOUNT_DEFAULT_FLAGS', 0);
define('UACCOUNT_DEFAULT_FLAGS', 0);
define('UACCOUNT_COOKIE_SSL', false);
define('UACCOUNT_COOKIE_DOMAIN', 'test.europeanautosource.com');
define('UACCOUNT_COOKIE_PATH', '/admin/');
define('UACCOUNT_COOKIE_CAP', '__');
define('UACCOUNT_COOKIE_INACTIVE_EXPIRY', 0);
define('UACCOUNT_AUTH_OFFSET', 2);
define('UACCOUNT_AUTH_LEN', 30);
// register users table;
new dbt(USERS_TABLE_USER, array(
username => array(DBT::TEXT, 32),
hash => array(DBT::TEXT, 32),
authcode => array(DBT::TEXT, 30),
attempts_failure => array(DBT::INT, 2),
attempts_last => array(DBT::INT, 11),
lock_expiry => array(DBT::INT, 11),
account_status => array(DBT::INT, 3, 1),
logon_last => array(DBT::INT, 11),
id => DBT::DENY
)
);
// fuck it, I'm gonna throw exceptions as opposed to implementing an event handler..
class UExcept extends Exception {
const INTERNAL = 2;
const GENERIC = 0;
const WARNING = 1;
const LOCK = 100;
public $messages = array();
public $codes = array();
public $code = self::INTERNAL;
public function __construct($message = '', $code = self::INTERNAL) {
$this->message = $message; $this->message = $this->message();
parent::__construct($this->format(), $code);
// parent::__construct($message, $code);
}
public function message() {
if(is_numeric($this->message) && array_key_exists($this->message, $GLOBALS['UE_MESSAGES']))
return $GLOBALS['UE_MESSAGES'][$this->message];
elseif(is_array($this->message)) {
$this->codes = array_values($this->message);
foreach($this->message as $val)
array_push($this->messages, ($GLOBALS['UE_MESSAGES'][$val]
? $GLOBALS['UE_MESSAGES'][$val]
: 'An unknown error has occured')
);
return (array)$this->messages;
}
else
return parent::getmessage();
}
public function format($format = ' - %s
') {
$txt = '';
if(is_array($t = $this->messages ? $this->messages : $this->getmessage()))
foreach($t as $v) $txt .= sprintf($format, $v) . "\n";
else
$txt = $t;
return $txt;
}
public function register(array $list) { foreach($list as $k => $v) $GLOBALS['UE_MESSAGES'][$k] = $list[$k]; }
public function uexit($message=null) {
if($message)
print '' . $message . '<\b>' . "\n";
exit(2);
}
}
$dbd = $GLOBALS['DBD'];
//non-method functions, getting too cluttered down there is all..
class user_tools {
const U_OK = true,
ERR_EXISTS = 101,
ERR_INVALID_USER = 111,
ERR_INVALID_PWD = 121,
ERR_INVALID_EMAIL = 131;
static function retrieve($user, $id = false) { // $user/$id not interchangable! this will insure, its deliberate
global $dbd;
if(empty($user) && (!$id && is_null($user)))
throw(new UExcept(__CLASS__ . '::retrieve() No user supplied'));
$res = $dbd->query(
$b = "SELECT username, hash, authcode, authsess, email, request_last, account_status, logon_last, lock_expiry, attempts_last, attempts_failure, id " .
"FROM " . USERS_TABLE_USER . " WHERE " .
($id
? "id = '" . dbd::sanitize($user) . "'"
: "lcase(" . (!strpos($user, '@') ? 'username' : 'email') . ") = '" . dbd::sanitize($user) . "'"
)
);
return is_object($res) && $res->rows()
? $res : false;
}
static public function add($username, $email, $password, $verify = true) {
global $dbd;
$e = array();
if(($res = self::retrieve($username)) !== false)
throw(new UExcept(self::ERR_EXISTS, UExcept::WARNING));
if(!preg_match('/^[^0-9][a-z0-9\.\-\_]+$/i', $username))
$e[] = self::ERR_INVALID_USER; /* do this second, as to go ahead and account for any possibility in the db */
if(strlen($password) < UACCOUNT_PWDMIN)
$e[] = self::ERR_INVALID_PWD;
if(!preg_match('/^(?:(?!@).*)@.*\.[a-z]{2,}$/', $email))
$e[] = self::ERR_INVALID_EMAIL;
if(!empty($e))
throw(new UExcept($e, UExcept::WARNING));
$dbd->insert(USERS_TABLE_USER, array(
'account_status' => UACCOUNT_DEFAULT_FLAGS,
'username' => $username,
'email' => $email,
'authcode' => self::gen_auth(),
'hash' => self::crypt($password, $username)
)
);
return $dbd->affected()
? true : false;
}
static protected function gen_auth($chars = 30) {
$rchrs = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQURSTUVWXYZ0123456789';
for($rstr = '', $i = 1, $len = strlen($rchrs) - 1; $i <= $chars; $i++)
$rstr .= substr($rchrs, mt_rand(0, $len), 1);
return $rstr;
}
static public function crypt($text, $salt) { return md5(md5($text . strtolower($salt))); }
const ID = 1, SESS = 0;
static function cookiejar() {
foreach($_COOKIE as $k => $v) {
if(preg_match('/^__[a-z0-9]+$/i', $k) && is_numeric($v) && (strlen($k) == (UACCOUNT_AUTH_LEN + 2))) {
return array($k, $v);
break;
}
}
return false;
}
}
UExcept::register(array(
user::ERR_EXISTS => 'The username you requested is already in use. Please, choose another.',
user::ERR_INVALID_USER => 'The username you requested contains invalid characters. Please choose another. (Acceptable characters include A-Z, 0-9, _, - and .)',
user::ERR_INVALID_PWD => 'Password must be atleast ' . UACCOUNT_PWDMIN . ' characters long.',
user::ERR_INVALID_EMAIL => 'The email address you supplied is invalid.'
));
class user extends user_tools {
public $data = array();
public $update = array();
public $error;
protected $synced = false;
protected $queried = false;
public $cookie;
public $id; // just to keep it outta the update array!
static $dbd;
const UFLG_ACCT_LOCK = 0x001,
UFLG_ACCT_ROOT = 0x002;
/* 0x004, 0x006, 0x008, 0x0010 */
public function __construct($user = null, $id = false) { // less flexibility more security...
if(is_null($user) && !$id) {
if($this->cookie())
return $this->__construct($this->cookie[parent::SESSION], true);
} else {
$this->data['username'] = $user;
$this->update = array();
}
if(!$this->sync($user, $id))
throw(new UExcept('', self::ERR_NOSUCHUSER));
}
public function sync($user = null, $id = false) {
if(($res = parent::retrieve(($user = is_null($user) ? $this->username : $user), $id))) {
$this->update = array();
if(($this->data = $res->get())) {
$this->id = $this->data['id'];
return ($this->synced = $this->queried = true);
}
}
throw(new UExcept('Unable to sync \'' . $this->username . '\'')); // security...
}
public function synced() { return $this->queried && $this->synced; } // this only exists incase I need to add/change conditions later..
public function locked() {
if($this->status(self::UFLG_ACCT_LOCK)) {
if(!$this->lock_expiry || $this->lock_expiry > time())
return true;
$this->ulock();
}
return false;
}
public function authenicate($pass, $auth = null) { // $pass is plaintext!
if(empty($pass) || !isset($this->username))
throw(new UExcept('authenicate(); \'username\' or \'password\' undefined'));
if(!$this->queried && !$this->sync())
return false; // this will never happen
echo " lock: " . ($this->locked() ? 'yes' : 'no') . "
";
if($this->locked())
throw(new UExcept('This account has been locked, please check your email or consult site administration.', UExcept::LOCK));
if(($result = (
strlen($this->username)
&& !empty($this->hash)
&& $this->synced()
&& ($this->hash === parent::crypt($pass, $this->username))
)
? true : false) === false
) {
$this->attempts_failure = $this->attempts_failure + 1;
echo "unf: " . $this->attempts_last . " expiry: " . ($this->attempts_last + UACCOUNT_ATTEMPTS_LOCK) . " > " . time() . "
";
// if(!$this->attempts_last) // do we have a record? if, no its the first.. if left to 0 it will always expire..
// $this->attempts_last = time();
if($this->attempts_last && ($this->attempts_last+UACCOUNT_ATTEMPTS_LOCK) > time()) { // has attempts time expired?
if($this->attempts_failure >= UACCOUNT_ATTEMPTS_LIMIT) {
$this->lock(UACCOUNT_ATTEMPTS_LOCK);
return $this->authenicate($pass, $auth); // throw an exception at em
}
}
elseif(!$this->attempts_last) { // it has expired, reset it
$this->attempts_last = time();
$this->attempts_failure = 1;
}
}
return $result;
}
public function active() { // do we have a good cookie!
if(!$this->cookie())
return false;
}
// http://i140.photobucket.com/albums/r8/oatmeal_el/abortionomelets.jpg
public function cookie($renew = false) {
if($renew) { // find it.
if(!$this->queried)
return false;
$this->logon_last = time();
if(!setcookie(
UACCOUNT_COOKIE_CAP . ($this->authsess = parent::gen_auth()),
$this->id,
0,
UACCOUNT_COOKIE_PATH,
UACCOUNT_COOKIE_DOMAIN,
UACCOUNT_COOKIE_SSL,
true)
) throw(new UExcept(__CLASS__ . '::cookie: Header output already began...'));
$this->commit();
} else {
if($cookie = $this->cookie ? $this->cookie : parent::cookiejar()) {
if(($this->request_last < (time() - UACCOUNT_COOKIE_INACTIVE_EXPIRY))
&& (substr($cookie[parent::SESSION], 2) === $this->authsess)
) $this->cookie = $cookie;
else
$this->remove_cookie($cookie[parent::SESSION]);
// setcookie($cookie[parent::SESSION], '', time() - (60*60*24));
}
echo substr($cookie[parent::SESSION], 2) . ' - ' . $this->authsess . '
';
$this->request_last = time();
}
return $this->cookie;
}
public function remove_cookie($key = null) {
if(!($key = ($key ? $key : $this->cookie[parent::SESSION])))
return false;
return @setcookie(
$key,
$this->id,
(time() - 60*60*24*30),
UACCOUNT_COOKIE_PATH,
UACCOUNT_COOKIE_DOMAIN,
UACCOUNT_COOKIE_SSL,
true
);
}
public function lock($expiry) { // do this as a method, then i can be updated with a commit() call, saving any additional queries aside from it
echo "LOCKING!";
$this->lock_expiry = time() + $expiry;
$this->authcode = parent::gen_auth();
$this->status(self::UFLG_ACCT_LOCK, '+');
$this->commit();
}
public function ulock() {
if(!$this->status(self::UFLG_ACCT_LOCK))
return false;
echo "UNLOCKING!";
$this->lock_expiry = 0;
$this->attempts_last = 0;
$this->attempts_failure = 0;
$this->status(self::UFLG_ACCT_LOCK, '-');
$this->commit();
}
protected function status($flag = null, $op = null) { // $this->flags(FLAG, +|-)
$stat = $this->account_status;
if(is_null($flag))
return $stat;
elseif($op == '-' && $this->status($flag))
$this->account_status = $stat - $flag;
elseif($op == '+' && !$this->status($flag))
$this->account_status = $stat + $flag;
return ($flag & $this->account_status);
}
// signal/modify/report more *normal* actions taken, those not affecting flow control
public function __set($n, $v) {
// if(isset($this->data[$n]) && $this->data[$n] === $v)
// return;
// echo " -- Setting data " . $n . " to '" . $v . "'
\n";
$this->synced = false;
$this->update[$n] = 1;
$this->data[$n] = $v;
}
public function __get($n) {
if(isset($this->data[$n])) {
// echo "Serving data: " . $n . " = " . $this->data[$n] . "
\n";
return $this->data[$n];
}
throw(new UExcept('Requested user data, element \'' . $n .'\' undefined.')); // this will be annoying, but will force care to be taken with data - which is always a fine, fine idea
}
public function __isset($n) { return isset($this->data[$n]); }
public function __unset($n) { $this->$n = null; } // dont do anything in this instance..
public function commit() {
echo "COMMITING!";
global $dbd;
if(empty($this->update) || !is_array($this->update))
return false;
$bastard = array();
try {
foreach($this->update as $k => $v) $bastard[$k] = $this->$k;
} catch(UExcept $ux) {
throw(new UExcept('It seems someone went and attached irrelevent data to the user object, stop that.. (' . $ux->getmessage() . ')' ));
}
$b = ;
$this->update = array();
if(!empty($bastard))
return $dbd->exec(
"UPDATE " . USERS_TABLE_USER . " " .
"SET " . dbd::create_dstruct(dbd::UPDATE, $bastard) . " " .
"WHERE id = '" . dbd::sanitize($this->id) . "'"
);
}
public function __destruct() { }
public function logout() {
$this->remove_cookie();
$this->authsess = '';
$this->__destruct();
}
}
class admin extends user {
}
?>