Newer
Older
<?php
/*********************************************************************
class.client.php
http://www.osticket.com
Released under the GNU General Public License WITHOUT ANY WARRANTY.
See LICENSE.TXT for details.
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
require_once INCLUDE_DIR.'class.user.php';
abstract class TicketUser
implements EmailContact {
static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i';
function __construct($user) {
$this->user = $user;
function __call($name, $args) {
global $cfg;
$rv = null;
if($this->user && is_callable(array($this->user, $name)))
$rv = $args
? call_user_func_array(array($this->user, $name), $args)
: call_user_func(array($this->user, $name));
if ($rv) return $rv;
$tag = substr($name, 3);
switch (strtolower($tag)) {
case 'ticket_link':
Http::build_query(
array('auth' => $this->getAuthToken()),
false
)
);
function getId() { return ($this->user) ? $this->user->getId() : null; }
function getEmail() { return ($this->user) ? $this->user->getEmail() : null; }
function sendAccessLink() {
global $ost;
if (!($ticket = $this->getTicket())
|| !($email = $ost->getConfig()->getDefaultEmail())
|| !($content = Page::lookupByType('access-link')))
return;
$vars = array(
'url' => $ost->getConfig()->getBaseUrl(),
'ticket' => $this->getTicket(),
$lang = false;
if (is_callable(array($this, 'getLanguage')))
$lang = $this->getLanguage(UserAccount::LANG_MAILOUTS);
$msg = $ost->replaceTemplateVariables(array(
'subj' => $content->getLocalName($lang),
'body' => $content->getLocalBody($lang),
), $vars);
$email->send($this->getEmail(), Format::striptags($msg['subj']),
$msg['body']);
protected function getAuthToken($algo=1) {
//Format: // <user type><algo id used>x<pack of uid & tid><hash of the algo>
$authtoken = sprintf('%s%dx%s',
($this->isOwner() ? 'o' : 'c'),
$algo,
Base32::encode(pack('VV',$this->getId(), $this->getTicketId())));
switch($algo) {
$authtoken .= substr(base64_encode(
md5($this->getId().$this->getTicket()->getCreateDate().$this->getTicketId().SECRET_SALT, true)), 8);
default:
return null;
return $authtoken;
}
static function lookupByToken($token) {
//Expecting well formatted token see getAuthToken routine for details.
$matches = array();
if (!preg_match(static::$token_regex, $token, $matches))
return null;
//Unpack the user and ticket ids
$matches +=unpack('Vuid/Vtid',
Base32::decode(strtolower(substr($matches['hash'], 0, 13))));
$user = null;
switch ($matches['type']) {
case 'c': //Collaborator c
if (($user = Collaborator::lookup($matches['uid']))
&& $user->getTicketId() != $matches['tid'])
$user = null;
break;
case 'o': //Ticket owner
if (($ticket = Ticket::lookup($matches['tid']))) {
if (($user = $ticket->getOwner())
&& $user->getId() != $matches['uid'])
if (!$user
|| !$user instanceof TicketUser
|| strcasecmp($user->getAuthToken($matches['algo']), $token))
return false;
static function lookupByEmail($email) {
if (!($user=User::lookup(array('emails__address' => $email))))
return null;
return new EndUser($user);
}
return $this instanceof TicketOwner;
function flagGuest() {
$this->_guest = true;
}
function isGuest() {
return $this->_guest;
}
function getUserId() {
return $this->user->getId();
}
abstract function getTicketId();
abstract function getTicket();
}
function __construct($user, $ticket) {
parent::__construct($user);
$this->ticket = $ticket;
function getTicket() {
return $this->ticket;
function getTicketId() {
return $this->ticket->getId();
}
/*
* Decorator class for authenticated user
*
*/
protected $user;
function __construct($user) {
$this->user = $user;
}
/*
* Delegate calls to the user
*/
function __call($name, $args) {
if(!$this->user
|| !is_callable(array($this->user, $name)))
return $this->getVar(substr($name, 3));
return $args
? call_user_func_array(array($this->user, $name), $args)
: call_user_func(array($this->user, $name));
}
function getVar($tag) {
$u = $this;
// Traverse the $user properties of all nested user objects to get
// to the User instance with the custom data
while (isset($u->user))
$u = $u->user;
if (method_exists($u, 'getVar'))
return $u->getVar($tag);
}
function getId() {
//We ONLY care about user ID at the ticket level
if ($this->user instanceof Collaborator)
return $this->user->getUserId();
elseif ($this->user)
return $this->user->getId();
return false;
function getUserName() {
//XXX: Revisit when real usernames are introduced or when email
// requirement is removed.
return $this->user->getEmail();
}
return $this->isOwner() ? 'owner' : 'collaborator';
}
function getAuthBackend() {
list($authkey,) = explode(':', $this->getAuthKey());
return UserAuthenticationBackend::getBackend($authkey);
function getTicketStats() {
if (!isset($this->ht['stats']))
$this->ht['stats'] = $this->getStats();
return $this->ht['stats'];
}
function getNumTickets() {
if (!($stats=$this->getTicketStats()))
return 0;
}
function getNumOpenTickets() {
return ($stats=$this->getTicketStats())?$stats['open']:0;
}
function getNumClosedTickets() {
return ($stats=$this->getTicketStats())?$stats['closed']:0;
}
if ($this->_account === false)
$this->_account =
ClientAccount::lookup(array('user_id'=>$this->getId()));
return $this->_account;
function getLanguage($flags=false) {
if ($acct = $this->getAccount())
return $acct->getLanguage($flags);
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
$where = ' WHERE ticket.user_id = '.db_input($this->getId())
.' OR collab.user_id = '.db_input($this->getId()).' ';
$join = 'LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
ON (collab.ticket_id=ticket.ticket_id
AND collab.user_id = '.db_input($this->getId()).' ) ';
$sql = 'SELECT \'open\', count( ticket.ticket_id ) AS tickets '
.'FROM ' . TICKET_TABLE . ' ticket '
.'INNER JOIN '.TICKET_STATUS_TABLE. ' status
ON (ticket.status_id=status.id
AND status.state=\'open\') '
. $join
. $where
.'UNION SELECT \'closed\', count( ticket.ticket_id ) AS tickets '
.'FROM ' . TICKET_TABLE . ' ticket '
.'INNER JOIN '.TICKET_STATUS_TABLE. ' status
ON (ticket.status_id=status.id
AND status.state=\'closed\' ) '
. $join
. $where;
$res = db_query($sql);
$stats = array();
while($row = db_fetch_row($res)) {
$stats[$row[0]] = $row[1];
}
return $stats;
function onLogin($bk) {
if ($account = $this->getAccount())
$account->onLogin($bk);
}
class ClientAccount extends UserAccount {
function checkPassword($password, $autoupdate=true) {
/*bcrypt based password match*/
if(Passwd::cmp($password, $this->get('passwd')))
return true;
//Fall back to MD5
if(!$password || strcmp($this->get('passwd'), MD5($password)))
return false;
//Password is a MD5 hash: rehash it (if enabled) otherwise force passwd change.
if ($autoupdate)
$this->set('passwd', Passwd::hash($password));
if (!$autoupdate || !$this->save())
$this->forcePasswdReset();
return true;
}
function hasCurrentPassword($password) {
return $this->checkPassword($password, false);
}
function cancelResetTokens() {
// TODO: Drop password-reset tokens from the config table for
// this user id
$sql = 'DELETE FROM '.CONFIG_TABLE.' WHERE `namespace`="pwreset"
AND `key`='.db_input($this->getUserId());
if (!db_query($sql, false))
return false;
unset($_SESSION['_client']['reset-token']);
}
function onLogin($bk) {
$this->setExtraAttr('browser_lang',
Internationalization::getCurrentLanguage());
$this->save();
}
$rtoken = $_SESSION['_client']['reset-token'];
if ($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd'] || $rtoken) {
$errors['passwd1']=__('New password is required');
elseif ($vars['passwd1'] && strlen($vars['passwd1'])<6)
$errors['passwd1']=__('Password must be at least 6 characters');
elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
$errors['passwd2']=__('Passwords do not match');
if ($_config->get($rtoken) != $this->getUserId())
__('Invalid reset token. Logout and try again');
elseif (!($ts = $_config->lastModified($rtoken))
&& ($cfg->getPwResetWindow() < (time() - strtotime($ts))))
$errors['err'] =
__('Invalid reset token. Logout and try again');
elseif ($this->get('passwd')) {
if (!$vars['cpasswd'])
$errors['cpasswd']=__('Current password is required');
elseif (!$this->hasCurrentPassword($vars['cpasswd']))
$errors['cpasswd']=__('Invalid current password!');
elseif (!strcasecmp($vars['passwd1'], $vars['cpasswd']))
$errors['passwd1']=__('New password MUST be different from the current password!');
// Timezone selection is not required. System default is a valid
// fallback
$this->set('dst', isset($vars['dst']) ? 1 : 0);
// Change language
$this->set('lang', $vars['lang'] ?: null);
Internationalization::setCurrentLanguage(null);
TextDomain::configureForUser($this);
if ($vars['backend']) {
$this->set('backend', $vars['backend']);
if ($vars['username'])
$this->set('username', $vars['username']);
}
if ($vars['passwd1']) {
$this->set('passwd', Passwd::hash($vars['passwd1']));
$info = array('password' => $vars['passwd1']);
Signal::send('auth.pwchange', $this->getUser(), $info);
$this->clearStatus(UserAccountStatus::REQUIRE_PASSWD_RESET);
// Used by the email system
interface EmailContact {
}