You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
516 lines
15 KiB
516 lines
15 KiB
<?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:
|
|
**********************************************************************/
|
|
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';
|
|
|
|
protected $user;
|
|
protected $_guest = false;
|
|
|
|
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));
|
|
|
|
return $rv ?: false;
|
|
}
|
|
|
|
// 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'),
|
|
);
|
|
}
|
|
|
|
function getVar($tag) {
|
|
switch (strtolower($tag)) {
|
|
case 'ticket_link':
|
|
$ticket = $this->getTicket();
|
|
return $this->getTicketLink(($ticket &&
|
|
!$ticket->getNumCollaborators()));
|
|
break;
|
|
}
|
|
}
|
|
|
|
function getTicketLink($authtoken=true) {
|
|
global $cfg;
|
|
|
|
$ticket = $this->getTicket();
|
|
if ($authtoken
|
|
&& $ticket
|
|
&& $cfg->isAuthTokenEnabled()) {
|
|
$qstr = array();
|
|
$qstr['auth'] = $ticket->getAuthToken($this);
|
|
return sprintf('%s/view.php?%s',
|
|
$cfg->getBaseUrl(),
|
|
Http::build_query($qstr, false)
|
|
);
|
|
}
|
|
|
|
return sprintf('%s/view.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))));
|
|
|
|
$user = null;
|
|
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;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (!$user
|
|
|| !$user instanceof ITicketUser
|
|
|| strcasecmp($ticket->getAuthToken($user, $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 instanceof TicketOwner;
|
|
}
|
|
|
|
function flagGuest() {
|
|
$this->_guest = true;
|
|
}
|
|
|
|
function isGuest() {
|
|
return $this->_guest;
|
|
}
|
|
|
|
function getUserId() {
|
|
return $this->user->getId();
|
|
}
|
|
|
|
abstract function getTicketId();
|
|
abstract function getTicket();
|
|
}
|
|
|
|
class TicketOwner extends TicketUser {
|
|
|
|
protected $ticket;
|
|
|
|
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 $_account = false;
|
|
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
|
|
while (isset($u->user)) {
|
|
$u = $u->user;
|
|
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();
|
|
}
|
|
|
|
function getUserType() {
|
|
return $this->isOwner() ? 'owner' : 'collaborator';
|
|
}
|
|
|
|
function getAuthBackend() {
|
|
list($authkey,) = explode(':', $this->getAuthKey());
|
|
return UserAuthenticationBackend::getBackend($authkey);
|
|
}
|
|
|
|
function get2FABackend() {
|
|
//TODO: support 2FA on client portal
|
|
return null;
|
|
}
|
|
|
|
function getTicketStats() {
|
|
if (!isset($this->_stats))
|
|
$this->_stats = $this->getStats();
|
|
|
|
return $this->_stats;
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
function getAccount() {
|
|
if ($this->_account === false)
|
|
$this->_account =
|
|
ClientAccount::lookup(array('user_id'=>$this->getId()));
|
|
|
|
return $this->_account;
|
|
}
|
|
|
|
function getUser() {
|
|
if ($this->user === false)
|
|
$this->user = User::lookup($this->getId());
|
|
|
|
return $this->user;
|
|
}
|
|
|
|
function getLanguage($flags=false) {
|
|
if ($acct = $this->getAccount())
|
|
return $acct->getLanguage($flags);
|
|
}
|
|
|
|
private function getStats() {
|
|
global $cfg;
|
|
$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
|
|
$mine = clone $basic;
|
|
$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 = clone $basic;
|
|
$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 check_passwd($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->check_passwd($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();
|
|
}
|
|
|
|
function update($vars, &$errors) {
|
|
global $cfg;
|
|
|
|
// FIXME: Updates by agents should go through UserAccount::update()
|
|
global $thisstaff, $thisclient;
|
|
if ($thisstaff)
|
|
return parent::update($vars, $errors);
|
|
|
|
$rtoken = $_SESSION['_client']['reset-token'];
|
|
|
|
|
|
if ($rtoken) {
|
|
$_config = new Config('pwreset');
|
|
if ($_config->get($rtoken) != 'c'.$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 ($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd']) {
|
|
|
|
if (!$vars['passwd1'])
|
|
$errors['passwd1']=__('New password is required');
|
|
elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
|
|
$errors['passwd2']=__('Passwords do not match');
|
|
elseif ($this->get('passwd')) {
|
|
if (!$vars['cpasswd'])
|
|
$errors['cpasswd']=__('Current password is required');
|
|
elseif (!$this->hasCurrentPassword($vars['cpasswd']))
|
|
$errors['cpasswd']=__('Invalid current password!');
|
|
}
|
|
|
|
// Check password policies
|
|
if (!$errors) {
|
|
try {
|
|
UserAccount::checkPassword($vars['passwd1'], @$vars['cpasswd']);
|
|
} catch (BadPassword $ex) {
|
|
$errors['passwd1'] = $ex->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Timezone selection is not required. System default is a valid
|
|
// fallback
|
|
|
|
if ($errors) return false;
|
|
|
|
$this->set('timezone', $vars['timezone']);
|
|
// 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->cancelResetTokens();
|
|
$this->clearStatus(UserAccountStatus::REQUIRE_PASSWD_RESET);
|
|
// Clean sessions
|
|
Signal::send('auth.clean', $this->getUser(), $thisclient);
|
|
}
|
|
|
|
return $this->save();
|
|
}
|
|
}
|
|
|
|
|
|
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();
|
|
*/
|
|
}
|
|
?>
|