<?php /********************************************************************* class.client.php Handles everything about EndUser Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket 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: **********************************************************************/ abstract class TicketUser { static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i'; protected $user; 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': return sprintf('%s/view.php?auth=%s', $cfg->getBaseUrl(), urlencode($this->getAuthToken())); break; } return false; } function sendAccessLink() { global $ost; if (!($ticket = $this->getTicket()) || !($email = $ost->getConfig()->getDefaultEmail()) || !($content = Page::lookup(Page::getIdByType('access-link')))) return; $vars = array( 'url' => $ost->getConfig()->getBaseUrl(), 'ticket' => $this->getTicket(), 'recipient' => $this); $msg = $ost->replaceTemplateVariables(array( 'subj' => $content->getName(), 'body' => $content->getBody(), ), $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) { case 1: $authtoken .= substr(base64_encode( md5($this->getId().$this->getTicket()->getCreateDate().$this->getTicketId().SECRET_SALT, true)), 8); break; 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']) $user = null; } break; } if (!$user || !$user instanceof TicketUser || strcasecmp($user->getAuthToken($matches['algo']), $token)) return false; return $user; } static function lookupByEmail($email) { if (!($user=User::lookup(array('emails__address' => $email)))) return null; return new EndUser($user); } function isOwner() { return ($this->user && $this->user->getId() == $this->getTicket()->getOwnerId()); } abstract function getTicketId(); abstract function getTicket(); } class TicketOwner extends TicketUser { protected $ticket; 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 * */ class EndUser extends AuthenticatedUser { protected $user; protected $_account = false; 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 false; return $args ? call_user_func_array(array($this->user, $name), $args) : call_user_func(array($this->user, $name)); } 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(); } function getRole() { 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() { return ($stats=$this->getTicketStats())?($stats['open']+$stats['closed']):0; } function getNumOpenTickets() { return ($stats=$this->getTicketStats())?$stats['open']:0; } function getNumClosedTickets() { return ($stats=$this->getTicketStats())?$stats['closed']:0; } function getAccount() { if ($this->_account === false) $this->_account = ClientAccount::lookup(array('user_id'=>$this->getId())); return $this->_account; } private function getStats() { $sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed ' .' FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.TICKET_TABLE.' open ON (open.ticket_id=ticket.ticket_id AND open.status=\'open\') ' .' LEFT JOIN '.TICKET_TABLE.' closed ON (closed.ticket_id=ticket.ticket_id AND closed.status=\'closed\')' .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab ON (collab.ticket_id=ticket.ticket_id AND collab.user_id = '.db_input($this->getId()).' )' .' WHERE ticket.user_id = '.db_input($this->getId()) .' OR collab.user_id = '.db_input($this->getId()); return db_fetch_array(db_query($sql)); } } require_once INCLUDE_DIR.'class.orm.php'; class ClientAccountModel extends VerySimpleModel { static $meta = array( 'table' => USER_ACCOUNT_TABLE, 'pk' => array('id'), 'joins' => array( 'user' => array( 'null' => false, 'constraint' => array('user_id' => 'UserModel.id') ), ), ); } class ClientAccount extends ClientAccountModel { var $_options = null; var $timezone; const CONFIRMED = 0x0001; const LOCKED = 0x0002; const PASSWD_RESET_REQUIRED = 0x0004; function __onload() { if ($this->get('timezone_id')) { $this->timezone = Timezone::getOffsetById($this->ht['timezone_id']); $_SESSION['TZ_OFFSET'] = $this->timezone; $_SESSION['TZ_DST'] = $this->get('dst'); } } function getId() { return $this->get('id'); } function getUserId() { return $this->get('user_id'); } 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 hasPassword() { return (bool) $this->get('passwd'); } function sendResetEmail($template='pwreset-client') { global $ost, $cfg; $token = Misc::randCode(48); // 290-bits $email = $cfg->getDefaultEmail(); $content = Page::lookup(Page::getIdByType($template)); if (!$email || !$content) return new Error('Unable to retrieve password reset email template'); $vars = array( 'url' => $ost->getConfig()->getBaseUrl(), 'token' => $token, 'recipient' => $this->getUser(), 'link' => sprintf( "%s/pwreset.php?token=%s", $ost->getConfig()->getBaseUrl(), $token), ); $vars['reset_link'] = &$vars['link']; $info = array('email' => $email, 'vars' => &$vars, 'log'=>true); Signal::send('auth.pwreset.email', $this, $info); $msg = $ost->replaceTemplateVariables(array( 'subj' => $content->getName(), 'body' => $content->getBody(), ), $vars); $_config = new Config('pwreset'); $_config->set($vars['token'], $this->user->getId()); $email->send($this->user->default_email->get('address'), Format::striptags($msg['subj']), $msg['body']); } function confirm() { $this->_setStatus(self::CONFIRMED); return $this->save(); } function isConfirmed() { return $this->_getStatus(self::CONFIRMED); } function lock() { $this->_setStatus(self::LOCKED); $this->save(); } function isLocked() { return $this->_getStatus(self::LOCKED); } function forcePasswdReset() { $this->_setStatus(self::PASSWD_RESET_REQUIRED); return $this->save(); } function isPasswdResetForced() { return $this->_getStatus(self::PASSWD_RESET_REQUIRED); } function _getStatus($flag) { return 0 !== ($this->get('status') & $flag); } function _clearStatus($flag) { return $this->set('status', $this->get('status') & ~$flag); } function _setStatus($flag) { return $this->set('status', $this->get('status') | $flag); } 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 getInfo() { $base = $this->ht; $base['tz_offset'] = $this->timezone; return $base; } function getUser() { $user = User::lookup($this->get('user_id')); $user->set('account', $this); return $user; } function update($vars, &$errors) { $rtoken = $_SESSION['_client']['reset-token']; if ($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd'] || $rtoken) { if (!$vars['passwd1']) $errors['passwd1']='New password required'; elseif ($vars['passwd1'] && strlen($vars['passwd1'])<6) $errors['passwd1']='Must be at least 6 characters'; elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) $errors['passwd2']='Password(s) do not match'; if ($rtoken) { $_config = new Config('pwreset'); if ($_config->get($rtoken) != $this->getUserId()) $errors['err'] = '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 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!'; } } if (!$vars['timezone_id']) $errors['timezone_id']='Time zone required'; if ($errors) return false; $this->set('timezone_id', $vars['timezone_id']); $this->set('dst', isset($vars['dst']) ? 1 : 0); if ($vars['passwd1']) { $this->set('passwd', Passwd::hash($vars['passwd1'])); $info = array('password' => $vars['passwd1']); Signal::send('auth.pwchange', $this, $info); $this->cancelResetTokens(); $this->_clearStatus(self::PASSWD_RESET_REQUIRED); } return $this->save(); } static function createForUser($user) { return static::create(array('user_id'=>$user->getId())); } static function lookupByUsername($username) { if (strpos($username, '@') !== false) $user = self::lookup(array('user__emails__address'=>$username)); else $user = self::lookup(array('username'=>$username)); return $user; } } ClientAccount::_inspect(); ?>