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, ITicketUser, TemplateVariable {
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));
// Required for Internationalization::getCurrentLanguage() in templates
function getLanguage() {
return $this->user->getLanguage();
}
static function getVarScope() {
return array(
'email' => __('Email Address'),
'name' => array('class' => 'PersonsName', 'desc' => __('Full Name')),
'ticket_link' => __('Link to view the ticket'),
switch (strtolower($tag)) {
case 'ticket_link':
$qstr = array();
&& $ticket
&& !$ticket->getNumCollaborators()) {
$qstr['auth'] = $ticket->getAuthToken($this);
return sprintf('%s/view.php?%s',
$cfg->getBaseUrl(),
Http::build_query($qstr, false)
);
} else {
return sprintf('%s/tickets.php?id=%s',
$cfg->getBaseUrl(),
$ticket ? $ticket->getId() : 0
);
}
function getId() { return ($this->user) ? $this->user->getId() : null; }
function getEmail() { return ($this->user) ? $this->user->getEmail() : null; }
function getName() {
return ($this->user) ? $this->user->getName() : null;
}
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))));
if (!($ticket = Ticket::lookup($matches['tid'])))
// Require a ticket for now
return 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 (($user = $ticket->getOwner())
&& $user->getId() != $matches['uid']) {
$user = null;
|| !$user instanceof ITicketUser
|| strcasecmp($ticket->getAuthToken($user, $matches['algo']), $token))
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 __toString() {
return (string) $this->getName();
}
function getTicket() {
return $this->ticket;
function getTicketId() {
return $this->ticket->getId();
}
/*
* Decorator class for authenticated user
*
*/
class EndUser extends BaseAuthenticatedUser {
protected $user;
protected $_stats;
protected $topic_stats;
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
if (method_exists($u, 'getVar')) {
if ($rv = $u->getVar($tag))
return $rv;
}
}
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);
if (!isset($this->_stats))
$this->_stats = $this->getStats();
function getNumTickets($forMyOrg=false, $state=false) {
$stats = $this->getTicketStats();
$count = 0;
$section = $forMyOrg ? 'myorg' : 'mine';
foreach ($stats[$section] as $row) {
if ($state && $row['status__state'] != $state)
continue;
$count += $row['count'];
}
return $count;
function getNumOpenTickets($forMyOrg=false) {
return $this->getNumTickets($forMyOrg, 'open') ?: 0;
function getNumClosedTickets($forMyOrg=false) {
return $this->getNumTickets($forMyOrg, 'closed') ?: 0;
function getNumTopicTickets($topic_id, $forMyOrg=false) {
$stats = $this->getTicketStats();
$section = $forMyOrg ? 'myorg' : 'mine';
if (!isset($this->topic_stats)) {
$this->topic_stats = array();
foreach ($stats[$section] as $row) {
$this->topic_stats[$row['topic_id']] += $row['count'];
}
}
return $this->topic_stats[$topic_id];
function getNumTopicTicketsInState($topic_id, $state=false, $forMyOrg=false) {
$stats = $this->getTicketStats();
$count = 0;
$section = $forMyOrg ? 'myorg' : 'mine';
foreach ($stats[$section] as $row) {
if ($topic_id != $row['topic_id'])
continue;
if ($state && $state != $row['status__state'])
continue;
$count += $row['count'];
}
return $count;
}
function getNumOrganizationTickets() {
return $this->getNumTickets(true);
}
function getNumOpenOrganizationTickets() {
return $this->getNumTickets(true, 'open');
}
function getNumClosedOrganizationTickets() {
return $this->getNumTickets(true, 'closed');
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);
$basic = Ticket::objects()
->annotate(array('count' => SqlAggregate::COUNT('ticket_id')))
->values('status__state', 'topic_id')
->distinct('status_id', 'topic_id');
// Share tickets among the organization for owners only
$collab = clone $basic;
$mine->filter(array(
'user_id' => $this->getId(),
// Also add collaborator tickets to the list. This may seem ugly;
// but the general rule for SQL is that a single query can only use
// one index. Therefore, to scan two indexes (by user_id and
// thread.collaborators.user_id), we need two queries. A union will
// help out with that.
if ($cfg->collaboratorTicketsVisibility())
$mine->union($collab->filter(array(
'thread__collaborators__user_id' => $this->getId(),
Q::not(array('user_id' => $this->getId()))
)));
if ($orgid = $this->getOrgId()) {
// Also generate a separate query for all the tickets owned by
// either my organization or ones that I'm collaborating on
// which are not part of the organization.
$myorg->values('user__org_id');
$collab = clone $myorg;
$myorg->filter(array('user__org_id' => $orgid));
$myorg->union($collab->filter(array(
'thread__collaborators__user_id' => $this->getId(),
Q::not(array('user__org_id' => $orgid))
)));
return array('mine' => $mine, 'myorg' => $myorg);
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 `value`='.db_input('c'.$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();
}
// FIXME: Updates by agents should go through UserAccount::update()
if ($thisstaff)
return parent::update($vars, $errors);
$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) != 'c'.$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
// 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);
// Clean sessions
Signal::send('auth.clean', $this->getUser(), $thisclient);
interface ITicketUser {
/* PHP 5.3 < 5.3.8 will crash with some abstract inheritance issue
* ------------------------------------------------------------
function isOwner();
function flagGuest();
function isGuest();
function getUserId();
function getTicketId();
function getTicket();