diff --git a/attachment.php b/attachment.php index d780ae2da4187c41a0607afa30267504c7597f2c..1c0941d7d545d5bbdfaeaa50029d9900ba6e5b39 100644 --- a/attachment.php +++ b/attachment.php @@ -17,8 +17,8 @@ require('secure.inc.php'); require_once(INCLUDE_DIR.'class.attachment.php'); //Basic checks -if(!$thisclient - || !$_GET['id'] +if(!$thisclient + || !$_GET['id'] || !$_GET['h'] || !($attachment=Attachment::lookup($_GET['id'])) || !($file=$attachment->getFile())) @@ -26,9 +26,9 @@ if(!$thisclient //Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!! $vhash=md5($attachment->getFileId().session_id().$file->getHash()); -if(strcasecmp(trim($_GET['h']),$vhash) - || !($ticket=$attachment->getTicket()) - || !$ticket->checkClientAccess($thisclient)) +if(strcasecmp(trim($_GET['h']),$vhash) + || !($ticket=$attachment->getTicket()) + || !$ticket->checkUserAccess($thisclient)) die('Unknown or invalid attachment'); //Download the file.. $file->download(); diff --git a/client.inc.php b/client.inc.php index 2ab016d15c6a252af4816ef867748c991f8fd760..84eeaca1136446c977a6ce5e5ec26093b8244cc6 100644 --- a/client.inc.php +++ b/client.inc.php @@ -43,11 +43,9 @@ require_once(INCLUDE_DIR.'class.dept.php'); //clear some vars $errors=array(); $msg=''; -$thisclient=$nav=null; +$nav=null; //Make sure the user is valid..before doing anything else. -if($_SESSION['_client']['userID'] && $_SESSION['_client']['key']) - $thisclient = new ClientSession($_SESSION['_client']['userID'],$_SESSION['_client']['key']); - +$thisclient = UserAuthenticationBackend::getUser(); //is the user logged in? if($thisclient && $thisclient->getId() && $thisclient->isValid()){ $thisclient->refreshSession(); diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php index 6325950742f9dc68fb55aeb7d87142eb15c5f487..5137cbaabc670f7e15d0842df05fa127328aab83 100644 --- a/include/ajax.tickets.php +++ b/include/ajax.tickets.php @@ -31,11 +31,11 @@ class TicketsAjaxAPI extends AjaxController { $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25; $tickets=array(); - $sql='SELECT DISTINCT ticketID, email.address AS email' + $sql='SELECT DISTINCT `number`, email.address AS email' .' FROM '.TICKET_TABLE.' ticket' .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id' .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id' - .' WHERE ticketID LIKE \''.db_input($_REQUEST['q'], false).'%\''; + .' WHERE `number` LIKE \''.db_input($_REQUEST['q'], false).'%\''; $sql.=' AND ( staff_id='.db_input($thisstaff->getId()); @@ -464,7 +464,7 @@ class TicketsAjaxAPI extends AjaxController { Http::response(404, 'No such ticket'); elseif (!$bk || !$id) Http::response(422, 'Backend and user id required'); - elseif (!($backend = AuthenticationBackend::getBackend($bk))) + elseif (!($backend = StaffAuthenticationBackend::getBackend($bk))) Http::response(404, 'User not found'); $user_info = $backend->lookup($id); diff --git a/include/ajax.users.php b/include/ajax.users.php index 7be4874ec1f64e528fafcc0676ac6e60498c1d0d..698f2f49de10ffd767b7cfce62a2fb32f05b1a6e 100644 --- a/include/ajax.users.php +++ b/include/ajax.users.php @@ -51,7 +51,7 @@ class UsersAjaxAPI extends AjaxController { } } - foreach (AuthenticationBackend::searchUsers($_REQUEST['q']) as $u) { + foreach (StaffAuthenticationBackend::searchUsers($_REQUEST['q']) as $u) { $name = "{$u['first']} {$u['last']}"; $users[] = array('email' => $u['email'], 'name'=>$name, 'info' => "{$u['email']} - $name (remote)", @@ -122,7 +122,7 @@ class UsersAjaxAPI extends AjaxController { Http::response(403, 'Login Required'); elseif (!$bk || !$id) Http::response(422, 'Backend and user id required'); - elseif (!($backend = AuthenticationBackend::getBackend($bk))) + elseif (!($backend = StaffAuthenticationBackend::getBackend($bk))) Http::response(404, 'User not found'); $user_info = $backend->lookup($id); @@ -177,7 +177,7 @@ class UsersAjaxAPI extends AjaxController { Http::response(400, 'Query argument is required'); $users = array(); - foreach (AuthenticationBackend::allRegistered() as $ab) { + foreach (StaffAuthenticationBackend::allRegistered() as $ab) { if (!$ab instanceof AuthDirectorySearch) continue; diff --git a/include/api.tickets.php b/include/api.tickets.php index daa7ffe431a17d43d39227347f4b3efc0ac8053b..fb12e623c03328eada3e02bfddba0178c39860c9 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -97,7 +97,7 @@ class TicketApiController extends ApiController { if(!$ticket) return $this->exerr(500, "Unable to create new ticket: unknown error"); - $this->response(201, $ticket->getExtId()); + $this->response(201, $ticket->getNumber()); } /* private helper functions */ diff --git a/include/class.auth.php b/include/class.auth.php index 08760ed9d910ec0d1f1f41213a2aca13b9cd94d2..e3bce85fe5b83f8e5c11c626ac6115845a185a18 100644 --- a/include/class.auth.php +++ b/include/class.auth.php @@ -2,13 +2,36 @@ require(INCLUDE_DIR.'class.ostsession.php'); require(INCLUDE_DIR.'class.usersession.php'); -class AuthenticatedUser { - // How the user was authenticated - var $backend; + +abstract class AuthenticatedUser { + //Authorization key returned by the backend used to authorize the user + private $authkey; // Get basic information - function getId() {} - function getUsername() {} + abstract function getId(); + abstract function getUsername(); + abstract function getRole(); + + //Backend used to authenticate the user + abstract function getAuthBackend(); + + //Authentication key + function setAuthKey($key) { + $this->authkey = $key; + } + + function getAuthKey() { + return $this->authkey; + } + + // logOut the user + function logOut() { + + if ($bk = $this->getAuthBackend()) + return $bk->signOut($this); + + return false; + } } interface AuthDirectorySearch { @@ -42,15 +65,25 @@ interface AuthDirectorySearch { * receives a username and optional password. If the authentication * succeeds, an instance deriving from <User> should be returned. */ -class AuthenticationBackend { - static private $registry = array(); +abstract class AuthenticationBackend { + static protected $registry = array(); static $name; static $id; + /* static */ static function register($class) { - if (is_string($class)) + if (is_string($class) && class_exists($class)) $class = new $class(); + + if (!is_object($class) + || !($class instanceof AuthenticationBackend)) + return false; + + return static::_register($class); + } + + static function _register($class) { // XXX: Raise error if $class::id is already in the registry static::$registry[$class::$id] = $class; } @@ -60,55 +93,57 @@ class AuthenticationBackend { } static function getBackend($id) { - return static::$registry[$id]; + + if ($id + && ($backends = static::allRegistered()) + && isset($backends[$id])) + return $backends[$id]; } - /* static */ - function process($username, $password=null, &$errors) { + static function process($username, $password=null, &$errors) { + if (!$username) return false; - $backend = static::_getAllowedBackends($username); - - foreach (static::$registry as $bk) { - if ($backend && $bk->supportsAuthentication() && $bk::$id != $backend) + $backends = static::getAllowedBackends($username); + foreach (static::allRegistered() as $bk) { + if ($backends //Allowed backends + && $bk->supportsAuthentication() + && !in_array($bk::$id, $backends)) // User cannot be authenticated against this backend continue; + // All backends are queried here, even if they don't support // authentication so that extensions like lockouts and audits // can be supported. $result = $bk->authenticate($username, $password); - if ($result instanceof AuthenticatedUser) { - static::_login($result, $username, $bk); - $result->backend = $bk; + + if ($result instanceof AuthenticatedUser + && ($bk->login($result, $bk))) return $result; - } // TODO: Handle permission denied, for instance elseif ($result instanceof AccessDenied) { $errors['err'] = $result->reason; break; } } + $info = array('username'=>$username, 'password'=>$password); Signal::send('auth.login.failed', null, $info); } - function singleSignOn(&$errors) { - global $ost; + function processSignOn(&$errors) { - foreach (static::$registry as $bk) { + foreach (static::allRegistered() as $bk) { // All backends are queried here, even if they don't support // authentication so that extensions like lockouts and audits // can be supported. $result = $bk->signOn(); if ($result instanceof AuthenticatedUser) { - // Ensure staff members are allowed to be authenticated - // against this backend - if ($result instanceof Staff - && !static::_isBackendAllowed($result, $bk)) + //Perform further Object specific checks and the actual login + if (!$bk->login($result, $bk)) continue; - static::_login($result, $result->getUserName(), $bk); - $result->backend = $bk; + return $result; } // TODO: Handle permission denied, for instance @@ -121,7 +156,7 @@ class AuthenticationBackend { static function searchUsers($query) { $users = array(); - foreach (static::$registry as $bk) { + foreach (static::allRegistered() as $bk) { if ($bk instanceof AuthDirectorySearch) { $users += $bk->search($query); } @@ -129,42 +164,129 @@ class AuthenticationBackend { return $users; } - function _isBackendAllowed($staff, $bk) { - $sql = 'SELECT backend FROM '.STAFF_TABLE - .' WHERE staff_id='.db_input($staff->getId()); - $backend = db_result(db_query($sql)); - return !$backend || strcasecmp($bk::$id, $backend) === 0; + /** + * Fetches the friendly name of the backend + */ + function getName() { + return static::$name; + } + + /** + * Indicates if the backed supports authentication. Useful if the + * backend is used for logging or lockout only + */ + function supportsAuthentication() { + return true; + } + + /** + * Indicates if the backend supports changing a user's password. This + * would be done in two fashions. Either the currently-logged in user + * want to change its own password or a user requests to have their + * password reset. This requires an administrative privilege which this + * backend might not possess, so it's defined in supportsPasswordReset() + */ + function supportsPasswordChange() { + return false; + } + + function supportsPasswordReset() { + return false; + } + + function signOn() { + return null; + } + + protected function validate($auth) { + return null; + } + + abstract function authenticate($username, $password); + abstract function login($user, $bk); + abstract static function getUser(); //Validates authenticated users. + abstract function getAllowedBackends($userid); + abstract protected function getAuthKey($user); + abstract static function signOut($user); +} + +class RemoteAuthenticationBackend { + var $create_unknown_user = false; +} + +abstract class StaffAuthenticationBackend extends AuthenticationBackend { + + static private $_registry = array(); + + static function _register($class) { + static::$_registry[$class::$id] = $class; } - function _getAllowedBackends($username) { - $username = trim($_POST['userid']); + static function allRegistered() { + return array_merge(self::$_registry, parent::allRegistered()); + } + + function isBackendAllowed($staff, $bk) { + + if (!($backends=self::getAllowedBackends($staff->getId()))) + return true; //No restrictions + + return in_array($bk::$id, array_map('strtolower', $backends)); + } + + function getAllowedBackends($userid) { + + $backends =array(); + //XXX: Only one backend can be specified at the moment. $sql = 'SELECT backend FROM '.STAFF_TABLE - .' WHERE username='.db_input($username) - .' OR email='.db_input($username); - return db_result(db_query($sql)); + .' WHERE backend IS NOT NULL '; + if (is_numeric($userid)) + $sql.= ' AND staff_id='.db_input($userid); + else { + $sql.= ' AND (username='.db_input($userid) .' OR email='.db_input($userid).')'; + } + + if (($res=db_query($sql)) && db_num_rows($res)) + $backends[] = db_result($res); + + return array_filter($backends); } - function _login($user, $username, $bk) { + function login($staff, $bk) { global $ost; - if ($user instanceof Staff) { - //Log debug info. - $ost->logDebug('Staff login', - sprintf("%s logged in [%s], via %s", $user->getUserName(), - $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug. + if (!$bk || !($staff instanceof Staff)) + return false; - $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ' - .' WHERE staff_id='.db_input($user->getId()); - db_query($sql); - //Now set session crap and lets roll baby! - $_SESSION['_staff'] = array(); //clear. - $_SESSION['_staff']['userID'] = $username; + // Ensure staff is allowed for realz to be authenticated via the backend. + if (!static::isBackendAllowed($staff, $bk) + || !($authkey=$bk->getAuthKey($staff))) + return false; - $user->refreshSession(); //set the hash. + //Log debug info. + $ost->logDebug('Staff login', + sprintf("%s logged in [%s], via %s", $staff->getUserName(), + $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug. - $_SESSION['TZ_OFFSET'] = $user->getTZoffset(); - $_SESSION['TZ_DST'] = $user->observeDaylight(); - } + $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() ' + .' WHERE staff_id='.db_input($staff->getId()); + db_query($sql); + + //Tag the authkey. + $authkey = $bk::$id.':'.$authkey; + + //Now set session crap and lets roll baby! + $authsession = &$_SESSION['_auth']['staff']; + + $authsession = array(); //clear. + $authsession['id'] = $staff->getId(); + $authsession['key'] = $authkey; + + $staff->setAuthKey($authkey); + $staff->refreshSession(); //set the hash. + + $_SESSION['TZ_OFFSET'] = $staff->getTZoffset(); + $_SESSION['TZ_DST'] = $staff->observeDaylight(); //Regenerate session id. $sid = session_id(); //Current id @@ -175,54 +297,168 @@ class AuthenticationBackend { && $sid!=session_id()) $session->destroy($sid); - Signal::send('auth.login.succeeded', $user); + Signal::send('auth.login.succeeded', $staff); + + $staff->cancelResetTokens(); - $user->cancelResetTokens(); + return true; } - /** - * Fetches the friendly name of the backend + /* Base signOut + * + * Backend should extend the signout and perform any additional signout + * it requires. */ - function getName() { - return static::$name; + + static function signOut($staff) { + global $ost; + + $_SESSION['_auth']['staff'] = array(); + $ost->logDebug('Staff logout', + sprintf("%s logged out [%s]", + $staff->getUserName(), + $_SERVER['REMOTE_ADDR'])); //Debug. + + Signal::send('auth.logout', $staff); } - /** - * Indicates if the backed supports authentication. Useful if the - * backend is used for logging or lockout only - */ - function supportsAuthentication() { - return true; + // Called to get authenticated user (if any) + static function getUser() { + + if (!isset($_SESSION['_auth']['staff']) + || !$_SESSION['_auth']['staff']['key']) + return null; + + list($id, $auth) = explode(':', $_SESSION['_auth']['staff']['key']); + + if (!($bk=static::getBackend($id)) //get the backend + || !$bk->supportsAuthentication() //Make sure it can authenticate + || !($staff = $bk->validate($auth)) //Get AuthicatedUser + || !($staff instanceof Staff) + || $staff->getId() != $_SESSION['_auth']['staff']['id'] // check ID + ) + return null; + + $staff->setAuthKey($_SESSION['_auth']['staff']['key']); + + + return $staff; } - /** - * Indicates if the backend supports changing a user's password. This - * would be done in two fashions. Either the currently-logged in user - * want to change its own password or a user requests to have their - * password reset. This requires an administrative privilege which this - * backend might not possess, so it's defined in supportsPasswordReset() - */ - function supportsPasswordChange() { + function authenticate($username, $password) { return false; } - function supportsPasswordReset() { - return false; + // Generic authentication key for staff's backend is the username + protected function getAuthKey($staff) { + + if(!($staff instanceof Staff)) + return null; + + return $staff->getUsername(); + } + + protected function validate($authkey) { + + if (($staff = new StaffSession($authkey)) && $staff->getId()) + return $staff; + } +} + +abstract class UserAuthenticationBackend extends AuthenticationBackend { + + static private $_registry = array(); + + static function _register($class) { + static::$_registry[$class::$id] = $class; + } + + static function allRegistered() { + return array_merge(self::$_registry, parent::allRegistered()); + } + + function getAllowedBackends($userid) { + // White listing backends for specific user not supported. + return array(); + } + + function login($user, $bk) { + global $ost; + + if (!$user || !$bk + || !$bk::$id //Must have ID + || !($authkey = $bk->getAuthKey($user))) + return false; + + //Tag the authkey. + $authkey = $bk::$id.':'.$authkey; + + //Set the session goodies + $authsession = &$_SESSION['_auth']['user']; + + $authsession = array(); //clear. + $authsession['id'] = $user->getId(); + $authsession['key'] = $authkey; + $_SESSION['TZ_OFFSET'] = $ost->getConfig()->getTZoffset(); + $_SESSION['TZ_DST'] = $ost->getConfig()->observeDaylightSaving(); + + //The backend used decides the format of the auth key. + // XXX: encrypt to hide the bk?? + $user->setAuthKey($authkey); + + $user->refreshSession(); //set the hash. + + //Log login info... + $msg=sprintf('%s (%s) logged in [%s]', + $user->getUserName(), $user->getId(), $_SERVER['REMOTE_ADDR']); + $ost->logDebug('User login', $msg); + + //Regenerate session ID. + $sid=session_id(); //Current session id. + session_regenerate_id(TRUE); //get new ID. + if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id()) + $session->destroy($sid); + + return true; } - /* abstract */ function authenticate($username, $password) { return false; } - /* abstract */ - function signOn() { - return false; + static function signOut($user) { + global $ost; + + $_SESSION['_auth']['user'] = array(); + $ost->logDebug('User logout', + sprintf("%s logged out [%s]", + $user->getUserName(), $_SERVER['REMOTE_ADDR'])); } -} -class RemoteAuthenticationBackend { - var $create_unknown_user = false; + protected function getAuthKey($user) { + return $user->getUsername(); + } + + static function getUser() { + + if (!isset($_SESSION['_auth']['user']) + || !$_SESSION['_auth']['user']['key']) + return null; + + list($id, $auth) = explode(':', $_SESSION['_auth']['user']['key']); + + if (!($bk=static::getBackend($id)) //get the backend + || !$bk->supportsAuthentication() //Make sure it can authenticate + || !($user=$bk->validate($auth)) //Get AuthicatedUser + || !($user instanceof AuthenticatedUser) // Make sure it user + || $user->getId() != $_SESSION['_auth']['user']['id'] // check ID + ) + return null; + + $user->setAuthKey($_SESSION['_auth']['user']['key']); + + return $user; + } } /** @@ -241,52 +477,137 @@ class AccessDenied { * Simple authentication backend which will lock the login form after a * configurable number of attempts */ -class AuthLockoutBackend extends AuthenticationBackend { +abstract class AuthStrikeBackend extends AuthenticationBackend { function authenticate($username, $password=null) { - global $cfg, $ost; + return static::authStrike($username, $password); + } - if($_SESSION['_staff']['laststrike']) { - if((time()-$_SESSION['_staff']['laststrike'])<$cfg->getStaffLoginTimeout()) { - $_SESSION['_staff']['laststrike'] = time(); //reset timer. + function signOn() { + return static::authStrike('Unknown'); + } + + static function signOut($user) { + return false; + } + + + function login($user, $bk) { + return false; + } + + static function getUser() { + return null; + } + + function supportsAuthentication() { + return false; + } + + function getAllowedBackends($userid) { + return array(); + } + + function getAuthKey($user) { + return null; + } + + abstract function authStrike($username, $password=null); +} + +/* + * Backend to monitor staff's failed login attempts + */ +class StaffAuthStrikeBackend extends AuthStrikeBackend { + + function authstrike($username, $password=null) { + global $ost; + + $cfg = $ost->getConfig(); + + $authsession = &$_SESSION['_auth']['staff']; + + if($authsession['laststrike']) { + if((time()-$authsession['laststrike'])<$cfg->getStaffLoginTimeout()) { + $authsession['laststrike'] = time(); //reset timer. return new AccessDenied('Max. failed login attempts reached'); } else { //Timeout is over. //Reset the counter for next round of attempts after the timeout. - $_SESSION['_staff']['laststrike']=null; - $_SESSION['_staff']['strikes']=0; + $authsession['laststrike']=null; + $authsession['strikes']=0; } } - $_SESSION['_staff']['strikes']+=1; - if($_SESSION['_staff']['strikes']>$cfg->getStaffMaxLogins()) { - $_SESSION['_staff']['laststrike']=time(); + $authsession['strikes']+=1; + if($authsession['strikes']>$cfg->getStaffMaxLogins()) { + $authsession['laststrike']=time(); $alert='Excessive login attempts by a staff member?'."\n". 'Username: '.$username."\n" .'IP: '.$_SERVER['REMOTE_ADDR']."\n" .'TIME: '.date('M j, Y, g:i a T')."\n\n" - .'Attempts #'.$_SESSION['_staff']['strikes']."\n" + .'Attempts #'.$authsession['strikes']."\n" .'Timeout: '.($cfg->getStaffLoginTimeout()/60)." minutes \n\n"; $ost->logWarning('Excessive login attempts ('.$username.')', $alert, $cfg->alertONLoginError()); return new AccessDenied('Forgot your login info? Contact Admin.'); //Log every other failed login attempt as a warning. - } elseif($_SESSION['_staff']['strikes']%2==0) { + } elseif($authsession['strikes']%2==0) { $alert='Username: '.$username."\n" .'IP: '.$_SERVER['REMOTE_ADDR']."\n" .'TIME: '.date('M j, Y, g:i a T')."\n\n" - .'Attempts #'.$_SESSION['_staff']['strikes']; + .'Attempts #'.$authsession['strikes']; $ost->logWarning('Failed staff login attempt ('.$username.')', $alert, false); } } +} +StaffAuthenticationBackend::register(StaffAuthStrikeBackend); + +/* + * Backend to monitor user's failed login attempts + */ +class UserAuthStrikeBackend extends AuthStrikeBackend { + + function authstrike($username, $password=null) { + global $ost; + + $cfg = $ost->getConfig(); + + $authsession = &$_SESSION['_auth']['user']; + + //Check time for last max failed login attempt strike. + if($authsession['laststrike']) { + if((time()-$authsession['laststrike'])<$cfg->getClientLoginTimeout()) { + $authsession['laststrike'] = time(); //renew the strike. + return new AccessDenied('You\'ve reached maximum failed login attempts allowed.'); + } else { //Timeout is over. + //Reset the counter for next round of attempts after the timeout. + $authsession['laststrike'] = null; + $authsession['strikes'] = 0; + } + } + + $authsession['strikes']+=1; + if($authsession['strikes']>$cfg->getClientMaxLogins()) { + $authsession['laststrike'] = time(); + $alert='Excessive login attempts by a user.'."\n". + 'Login: '.$username.': '.$password."\n". + 'IP: '.$_SERVER['REMOTE_ADDR']."\n".'Time:'.date('M j, Y, g:i a T')."\n\n". + 'Attempts #'.$authsession['strikes']; + $ost->logError('Excessive login attempts (user)', $alert, ($cfg->alertONLoginError())); + return new AccessDenied('Access Denied'); + } elseif($authsession['strikes']%2==0) { //Log every other failed login attempt as a warning. + $alert='Login: '.$username.': '.$password."\n".'IP: '.$_SERVER['REMOTE_ADDR']. + "\n".'TIME: '.date('M j, Y, g:i a T')."\n\n".'Attempts #'.$authsession['strikes']; + $ost->logWarning('Failed login attempt (user)', $alert); + } - function supportsAuthentication() { - return false; } } -AuthenticationBackend::register(AuthLockoutBackend); +UserAuthenticationBackend::register(UserAuthStrikeBackend); -class osTicketAuthentication extends AuthenticationBackend { - static $name = "Local Authenication"; + +class osTicketAuthentication extends StaffAuthenticationBackend { + static $name = "Local Authentication"; static $id = "local"; function authenticate($username, $password) { @@ -303,6 +624,129 @@ class osTicketAuthentication extends AuthenticationBackend { return $user; } } + +} +StaffAuthenticationBackend::register(osTicketAuthentication); + +class PasswordResetTokenBackend extends StaffAuthenticationBackend { + static $id = "pwreset.staff"; + + function supportsAuthentication() { + return false; + } + + function signOn($errors=array()) { + if (!isset($_POST['userid']) || !isset($_POST['token'])) + return false; + elseif (!($_config = new Config('pwreset'))) + return false; + elseif (($staff = new StaffSession($_POST['userid'])) && + !$staff->getId()) + $errors['msg'] = 'Invalid user-id given'; + elseif (!($id = $_config->get($_POST['token'])) + || $id != $staff->getId()) + $errors['msg'] = 'Invalid reset token'; + elseif (!($ts = $_config->lastModified($_POST['token'])) + && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts)))) + $errors['msg'] = 'Invalid reset token'; + elseif (!$staff->forcePasswdRest()) + $errors['msg'] = 'Unable to reset password'; + else + return $staff; + } + + function login($staff, $bk) { + $_SESSION['_staff']['reset-token'] = $_POST['token']; + Signal::send('auth.pwreset.login', $staff); + return parent::login($staff, $bk); + } } -AuthenticationBackend::register(osTicketAuthentication); +StaffAuthenticationBackend::register(PasswordResetTokenBackend); + +/* + * AuthToken Authentication Backend + * + * Provides auto-login facility for end users with valid link + * + * Ticket used to loggin is tracked durring the session this is + * important in the future when auto-logins will be + * limited to single ticket view. + */ +class AuthTokenAuthentication extends UserAuthenticationBackend { + static $name = "Auth Token Authentication"; + static $id = "authtoken"; + + + function signOn() { + + $user = null; + if ($_GET['auth']) { + if (($u = TicketUser::lookupByToken($_GET['auth']))) + $user = new ClientSession($u); + } + // Support old ticket based tokens. + elseif ($_GET['t'] && $_GET['e'] && $_GET['a']) { + if (($ticket = Ticket::lookupByNumber($_GET['t'], $_GET['e'])) + // Using old ticket auth code algo - hardcoded here because it + // will be removed in ticket class in the upcoming rewrite + && !strcasecmp($_GET['a'], md5($ticket->getId() . $_GET['e'] . SECRET_SALT)) + && ($owner = $ticket->getOwner())) + $user = new ClientSession($owner); + } + + return $user; + } + + + protected function getAuthKey($user) { + + if (!$this->supportsAuthentication() || !$user) + return null; + + //Generate authkey based the type of ticket user + // It's required to validate users going forward. + $authkey = sprintf('%s%dt%dh%s', //XXX: Placeholder + ($user->isOwner() ? 'o':'c'), + $user->getId(), + $user->getTicketId(), + md5($user->getId().$this->id)); + + return $authkey; + } + + protected function validate($authkey) { + + $regex = '/^(?P<type>\w{1})(?P<id>\d+)t(?P<tid>\d+)h(?P<hash>.*)$/i'; + $matches = array(); + if (!preg_match($regex, $authkey, $matches)) + return false; + + $user = null; + switch ($matches['type']) { + case 'c': //Collaborator + $criteria = array( 'userId' => $matches['id'], + 'ticketId' => $matches['tid']); + if (($c = Collaborator::lookup($criteria)) + && ($c->getTicketId() == $matches['tid'])) + $user = new ClientSession($c); + break; + case 'o': //Ticket owner + if (($ticket = Ticket::lookup($matches['tid'])) + && ($o = $ticket->getOwner()) + && ($o->getId() == $matches['id'])) + $user = new ClientSession($o); + break; + } + + //Make sure the authkey matches. + if (!$user || strcmp($this->getAuthKey($user), $authkey)) + return null; + + + return $user; + } + +} +UserAuthenticationBackend::register(AuthTokenAuthentication); + ?> diff --git a/include/class.base32.php b/include/class.base32.php new file mode 100755 index 0000000000000000000000000000000000000000..ce83828f873d208f72fb3cdce35ab990d6aab673 --- /dev/null +++ b/include/class.base32.php @@ -0,0 +1,121 @@ +<?php +/* + * Base32 encoder/decoder + * + * Jared Hancock <jared@osticket.com> + * Copyright (c) osTicket.com + */ + + +class Base32 { + + /** + * encode a binary string + * + * @param $inString Binary string to base32 encode + * @return $outString Base32 encoded $inString + * + * Original code from + * http://www.phpkode.com/source/p/moodle/moodle/lib/base32.php. Optimized + * to double performance + */ + + function encode($inString) + { + $outString = ""; + $compBits = ""; + static $BASE32_TABLE = array( + '00000' => 'a', '00001' => 'b', '00010' => 'c', '00011' => 'd', + '00100' => 'e', '00101' => 'f', '00110' => 'g', '00111' => 'h', + '01000' => 'i', '01001' => 'j', '01010' => 'k', '01011' => 'l', + '01100' => 'm', '01101' => 'n', '01110' => 'o', '01111' => 'p', + '10000' => 'q', '10001' => 'r', '10010' => 's', '10011' => 't', + '10100' => 'u', '10101' => 'v', '10110' => 'w', '10111' => 'x', + '11000' => 'y', '11001' => 'z', '11010' => '0', '11011' => '1', + '11100' => '2', '11101' => '3', '11110' => '4', '11111' => '5'); + + /* Turn the compressed string into a string that represents the bits as 0 and 1. */ + for ($i = 0, $k = strlen($inString); $i < $k; $i++) { + $compBits .= str_pad(decbin(ord($inString[$i])), 8, '0', STR_PAD_LEFT); + } + + /* Pad the value with enough 0's to make it a multiple of 5 */ + if ((($len = strlen($compBits)) % 5) != 0) { + $compBits = str_pad($compBits, $len+(5-($len % 5)), '0', STR_PAD_RIGHT); + } + + /* Create an array by chunking it every 5 chars */ + $fiveBitsArray = str_split($compBits, 5); + + /* Look-up each chunk and add it to $outstring */ + foreach ($fiveBitsArray as $fiveBitsString) { + $outString .= $BASE32_TABLE[$fiveBitsString]; + } + + return $outString; + } + + + + /** + * decode to a binary string + * + * @param $inString String to base32 decode + * + * @return $outString Base32 decoded $inString + * + * @access private + * + */ + + function decode($inString) { + /* declaration */ + $deCompBits = ''; + $outString = ''; + + static $BASE32_TABLE = array( + 'a' => '00000', 'b' => '00001', 'c' => '00010', 'd' => '00011', + 'e' => '00100', 'f' => '00101', 'g' => '00110', 'h' => '00111', + 'i' => '01000', 'j' => '01001', 'k' => '01010', 'l' => '01011', + 'm' => '01100', 'n' => '01101', 'o' => '01110', 'p' => '01111', + 'q' => '10000', 'r' => '10001', 's' => '10010', 't' => '10011', + 'u' => '10100', 'v' => '10101', 'w' => '10110', 'x' => '10111', + 'y' => '11000', 'z' => '11001', '0' => '11010', '1' => '11011', + '2' => '11100', '3' => '11101', '4' => '11110', '5' => '11111'); + + /* Step 1 */ + $inputCheck = strlen($inString) % 8; + if(($inputCheck == 1)||($inputCheck == 3)||($inputCheck == 6)) { + trigger_error('input to Base32Decode was a bad mod length: '.$inputCheck); + return false; + } + + /* $deCompBits is a string that represents the bits as 0 and 1.*/ + for ($i = 0, $k = strlen($inString); $i < $k; $i++) { + $inChar = $inString[$i]; + if(isset($BASE32_TABLE[$inChar])) { + $deCompBits .= $BASE32_TABLE[$inChar]; + } else { + trigger_error('input to Base32Decode had a bad character: '.$inChar); + return false; + } + } + + /* Break the decompressed string into octets for returning */ + foreach (str_split($deCompBits, 8) as $chunk) { + if (strlen($chunk) != 8) { + // Ensure correct padding + if (substr_count($chunk, '1')>0) { + trigger_error('found non-zero padding in Base32Decode'); + return false; + } + break; + } + $outString .= chr(bindec($chunk)); + } + + return $outString; + } +} + +?> diff --git a/include/class.client.php b/include/class.client.php index 7464b841ca8ab7bfb5217d2965760ea99ff28fb9..93aa1b07f0b978f6a11fdfa64eb25b3b62de7808 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -2,10 +2,7 @@ /********************************************************************* class.client.php - Handles everything about client - - XXX: Please note that osTicket uses email address and ticket ID to authenticate the user*! - Client is modeled on the info of the ticket used to login . + Handles everything about EndUser Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket @@ -16,106 +13,183 @@ 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'; -class Client { + protected $user; - var $id; - var $fullname; - var $username; - var $email; + function __construct($user) { + $this->user = $user; + } - var $_answers; + function __call($name, $args) { + global $cfg; - var $ticket_id; - var $ticketID; + if($this->user && is_callable(array($this->user, $name))) + return $args + ? call_user_func_array(array($this->user, $name), $args) + : call_user_func(array($this->user, $name)); - var $ht; + $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 Client($id, $email=null) { - $this->id =0; - $this->load($id,$email); } - function load($id=0, $email=null) { + protected function getAuthToken($algo=1) { - if(!$id && !($id=$this->getId())) - return false; + //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()))); - $sql='SELECT ticket.ticket_id, ticketID, email.address as email ' - .' FROM '.TICKET_TABLE.' ticket ' - .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id' - .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id' - .' WHERE ticketID='.db_input($id); + switch($algo) { + case 1: + $authtoken .= substr(base64_encode( + md5($this->getId().$this->getTicket()->getCreateDate().$this->getTicketId().SECRET_SALT, true)), 8); + break; + default: + return null; + } - if($email) - $sql.=' AND email.address = '.db_input($email); + 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(!($res=db_query($sql)) || !db_num_rows($res)) - return NULL; + if (!$user + || !$user instanceof TicketUser + || strcasecmp($user->getAuthToken($matches['algo']), $token)) + return false; - $this->ht = db_fetch_array($res); - $this->id = $this->ht['ticketID']; //placeholder - $this->ticket_id = $this->ht['ticket_id']; - $this->ticketID = $this->ht['ticketID']; + return $user; + } - $user = User::lookup(array('emails__address'=>$this->ht['email'])); - $this->fullname = $user->getFullName(); + static function lookupByEmail($email) { - $this->username = $this->ht['email']; - $this->email = $this->ht['email']; + if (!($user=User::lookup(array('emails__address' => $email)))) + return null; - $this->stats = array(); + return new EndUser($user); + } - return($this->id); + function isOwner() { + return ($this->user + && $this->user->getId() == $this->getTicket()->getOwnerId()); } - function loadDynamicData() { - $this->_answers = array(); - foreach (DynamicFormEntry::forClient($this->getId()) as $form) - foreach ($form->getAnswers() as $answer) - $this->_answers[$answer->getField()->get('name')] = - $answer->getValue(); + abstract function getTicketId(); + abstract function getTicket(); +} + +class TicketOwner extends TicketUser { + + protected $ticket; + + function __construct($user, $ticket) { + parent::__construct($user); + $this->ticket = $ticket; } - function reload() { - return $this->load(); + function getTicket() { + return $this->ticket; } - function isClient() { - return TRUE; + function getTicketId() { + return $this->ticket->getId(); } +} - function getId() { - return $this->id; +/* + * Decorator class for authenticated user + * + */ + +class EndUser extends AuthenticatedUser { + + protected $user; + + function __construct($user) { + $this->user = $user; } - function getEmail() { - return $this->email; + /* + * 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 getUserName() { - return $this->username; + function getId() { + //We ONLY care about user ID at the ticket level + if ($this->user instanceof Collaborator) + return $this->user->getUserId(); + + return $this->user->getId(); } - function getName() { - return $this->fullname; + function getUserName() { + //XXX: Revisit when real usernames are introduced or when email + // requirement is removed. + return $this->user->getEmail(); } - function getPhone() { - return $this->_answers['phone']; + function getRole() { + return $this->isOwner() ? 'owner' : 'collaborator'; } - function getTicketID() { - return $this->ticketID; + function getAuthBackend() { + list($authkey,) = explode(':', $this->getAuthKey()); + return UserAuthenticationBackend::getBackend($authkey); } function getTicketStats() { - if(!$this->stats['tickets']) - $this->stats['tickets'] = Ticket::getClientStats($this->getEmail()); + if (!isset($this->ht['stats'])) + $this->ht['stats'] = $this->getStats(); - return $this->stats['tickets']; + return $this->ht['stats']; } function getNumTickets() { @@ -130,114 +204,23 @@ class Client { return ($stats=$this->getTicketStats())?$stats['closed']:0; } - /* ------------- Static ---------------*/ - function getLastTicketIdByEmail($email) { - $sql='SELECT ticket.ticketID FROM '.TICKET_TABLE.' ticket ' - .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id' - .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id' - .' WHERE email.address = '.db_input($email) - .' ORDER BY ticket.created ' - .' LIMIT 1'; - if(($res=db_query($sql)) && db_num_rows($res)) - list($tid) = db_fetch_row($res); + private function getStats() { - return $tid; - } - - function lookup($id, $email=null) { - return ($id && is_numeric($id) && ($c=new Client($id,$email)) && $c->getId()==$id)?$c:null; - } + $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()); - function lookupByEmail($email) { - return (($id=self::getLastTicketIdByEmail($email)))?self::lookup($id, $email):null; + return db_fetch_array(db_query($sql)); } - /* static */ function login($ticketID, $email, $auth=null, &$errors=array()) { - global $ost; - - $cfg = $ost->getConfig(); - $auth = trim($auth); - $email = trim($email); - $ticketID = trim($ticketID); - - # Only consider auth token for GET requests, and for GET requests, - # REQUIRE the auth token - $auto_login = ($_SERVER['REQUEST_METHOD'] == 'GET'); - - //Check time for last max failed login attempt strike. - if($_SESSION['_client']['laststrike']) { - if((time()-$_SESSION['_client']['laststrike'])<$cfg->getClientLoginTimeout()) { - $errors['login'] = 'Excessive failed login attempts'; - $errors['err'] = 'You\'ve reached maximum failed login attempts allowed. Try again later or <a href="open.php">open a new ticket</a>'; - $_SESSION['_client']['laststrike'] = time(); //renew the strike. - } else { //Timeout is over. - //Reset the counter for next round of attempts after the timeout. - $_SESSION['_client']['laststrike'] = null; - $_SESSION['_client']['strikes'] = 0; - } - } - - if($auto_login && !$auth) - $errors['login'] = 'Invalid method'; - elseif(!$ticketID || !Validator::is_email($email)) - $errors['login'] = 'Valid email and ticket number required'; - - //Bail out on error. - if($errors) return false; - - //See if we can fetch local ticket id associated with the ID given - if(($ticket=Ticket::lookupByExtId($ticketID, $email)) && $ticket->getId()) { - //At this point we know the ticket ID is valid. - //TODO: 1) Check how old the ticket is...3 months max?? 2) Must be the latest 5 tickets?? - //Check the email given. - - # Require auth token for automatic logins (GET METHOD). - if (!strcasecmp($ticket->getEmail(), $email) && (!$auto_login || $auth === $ticket->getAuthToken())) { - - //valid match...create session goodies for the client. - $user = new ClientSession($email,$ticket->getExtId()); - $_SESSION['_client'] = array(); //clear. - $_SESSION['_client']['userID'] = $ticket->getEmail(); //Email - $_SESSION['_client']['key'] = $ticket->getExtId(); //Ticket ID --acts as password when used with email. See above. - $_SESSION['_client']['token'] = $user->getSessionToken(); - $_SESSION['TZ_OFFSET'] = $cfg->getTZoffset(); - $_SESSION['TZ_DST'] = $cfg->observeDaylightSaving(); - $user->refreshSession(); //set the hash. - //Log login info... - $msg=sprintf('%s/%s logged in [%s]', $ticket->getEmail(), $ticket->getExtId(), $_SERVER['REMOTE_ADDR']); - $ost->logDebug('User login', $msg); - - //Regenerate session ID. - $sid=session_id(); //Current session id. - session_regenerate_id(TRUE); //get new ID. - if(($session=$ost->getSession()) && is_object($session) && $sid!=session_id()) - $session->destroy($sid); - - return $user; - - } - } - - //If we get to this point we know the login failed. - $errors['login'] = 'Invalid login'; - $_SESSION['_client']['strikes']+=1; - if(!$errors && $_SESSION['_client']['strikes']>$cfg->getClientMaxLogins()) { - $errors['login'] = 'Access Denied'; - $errors['err'] = 'Forgot your login info? Please <a href="open.php">open a new ticket</a>.'; - $_SESSION['_client']['laststrike'] = time(); - $alert='Excessive login attempts by a user.'."\n". - 'Email: '.$email."\n".'Ticket#: '.$ticketID."\n". - 'IP: '.$_SERVER['REMOTE_ADDR']."\n".'Time:'.date('M j, Y, g:i a T')."\n\n". - 'Attempts #'.$_SESSION['_client']['strikes']; - $ost->logError('Excessive login attempts (user)', $alert, ($cfg->alertONLoginError())); - } elseif($_SESSION['_client']['strikes']%2==0) { //Log every other failed login attempt as a warning. - $alert='Email: '.$email."\n".'Ticket #: '.$ticketID."\n".'IP: '.$_SERVER['REMOTE_ADDR']. - "\n".'TIME: '.date('M j, Y, g:i a T')."\n\n".'Attempts #'.$_SESSION['_client']['strikes']; - $ost->logWarning('Failed login attempt (user)', $alert); - } - - return false; - } } ?> diff --git a/include/class.collaborator.php b/include/class.collaborator.php index bf823dacdb366279ec88b5de9657843283f72ba9..b8adb93262d1dba483457b7decd82b8af7dd6078 100644 --- a/include/class.collaborator.php +++ b/include/class.collaborator.php @@ -14,8 +14,9 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ require_once(INCLUDE_DIR . 'class.user.php'); +require_once(INCLUDE_DIR . 'class.client.php'); -class Collaborator { +class Collaborator extends TicketUser { var $ht; @@ -23,8 +24,8 @@ class Collaborator { var $ticket; function __construct($id) { - $this->load($id); + parent::__construct($this->getUser()); } function load($id) { @@ -36,24 +37,13 @@ class Collaborator { .' WHERE id='.db_input($id); $this->ht = db_fetch_array(db_query($sql)); - $this->ticket = $this->user = null; + $this->ticket = null; } function reload() { return $this->load(); } - function __call($name, $args) { - - if(!($user=$this->getUser()) || !method_exists($user, $name)) - return null; - - if($args) - return call_user_func_array(array($user, $name), $args); - - return call_user_func(array($user, $name)); - } - function __toString() { return Format::htmlchars(sprintf('%s <%s>', $this->getName(), $this->getEmail())); @@ -67,6 +57,10 @@ class Collaborator { return ($this->ht['isactive']); } + function getCreateDate() { + return $this->ht['created']; + } + function getTicketId() { return $this->ht['ticket_id']; } @@ -148,12 +142,11 @@ class Collaborator { .' WHERE ticket_id='.db_input($info['ticketId']) .' AND user_id='.db_input($info['userId']); - list($id) = db_fetch_row(db_query($sql)); - - return $id; + return db_result(db_query($sql)); } static function lookup($criteria) { + $id = is_numeric($criteria) ? $criteria : self::getIdByInfo($criteria); diff --git a/include/class.crypto.php b/include/class.crypto.php index 92ab1e953f1b3786cac39e25e34acc7abfd59f9b..a9aebcd2f87b6e5e66b67a0c70ba0c6855ddd0cf 100644 --- a/include/class.crypto.php +++ b/include/class.crypto.php @@ -26,6 +26,8 @@ define('CRYPT_PHPSECLIB', 3); define('CRYPT_IS_WINDOWS', !strncasecmp(PHP_OS, 'WIN', 3)); + +require_once INCLUDE_DIR.'class.base32.php'; require_once PEAR_DIR.'Crypt/Hash.php'; require_once PEAR_DIR.'Crypt/AES.php'; diff --git a/include/class.export.php b/include/class.export.php index b5c5a44396eb07aec4c6a20c87d34a0a7c9eeff5..b95a7d16d587541188d40d4cc6f45097b00b757f 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -57,7 +57,7 @@ class Export { $sql = str_replace(' FROM ', ',' . implode(',', $select) . ' FROM ', $sql); return self::dumpQuery($sql, array( - 'ticketID' => 'Ticket Id', + 'number' => 'Ticket Number', 'created' => 'Date', 'subject' => 'Subject', 'name' => 'From', diff --git a/include/class.nav.php b/include/class.nav.php index 799c76a8b4dc0a78b74374c2b2aa673cf74c5132..1648332948ec1a32e5ac9a12aba75827d3087a15 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -282,12 +282,12 @@ class UserNav { $navs['new']=array('desc'=>'Open New Ticket','href'=>'open.php','title'=>''); if($user && $user->isValid()) { if($cfg && $cfg->showRelatedTickets()) { - $navs['tickets']=array('desc'=>sprintf('My Tickets (%d)',$user->getNumTickets()), + $navs['tickets']=array('desc'=>sprintf('Tickets (%d)',$user->getNumTickets()), 'href'=>'tickets.php', 'title'=>'Show all tickets'); } else { $navs['tickets']=array('desc'=>'View Ticket Thread', - 'href'=>sprintf('tickets.php?id=%d',$user->getTicketID()), + 'href'=>sprintf('tickets.php?id=%d',$user->getTicketId()), 'title'=>'View ticket status'); } } else { diff --git a/include/class.signal.php b/include/class.signal.php index 928c15c4d2392ae767a0c465b6730d859f3dc85f..424ccccc9665a36db41bb5a1ff271150d946ec82 100644 --- a/include/class.signal.php +++ b/include/class.signal.php @@ -25,6 +25,8 @@ * the codebase there exists a Signal::send() for the same named signal. */ class Signal { + static private $subscribers = array(); + /** * Subscribe to a signal. * @@ -51,10 +53,9 @@ class Signal { * signal handler. The function will receive the signal data and should * return true if the signal handler should be called. */ - /*static*/ function connect($signal, $callable, $object=null, + static function connect($signal, $callable, $object=null, $check=null) { - global $_subscribers; - if (!isset($_subscribers[$signal])) $_subscribers[$signal] = array(); + if (!isset(self::$subscribers[$signal])) self::$subscribers[$signal] = array(); // XXX: Ensure $object if set is a class if ($object && !is_string($object)) trigger_error("Invalid object: $object: Expected class"); @@ -62,7 +63,7 @@ class Signal { trigger_error("Invalid check function: Must be callable"); $check = null; } - $_subscribers[$signal][] = array($object, $callable, $check); + self::$subscribers[$signal][] = array($object, $callable, $check); } /** @@ -85,11 +86,10 @@ class Signal { * possible to propogate changes in the signal handlers back to the * originating context. */ - /*static*/ function send($signal, $object, &$data=null) { - global $_subscribers; - if (!isset($_subscribers[$signal])) + static function send($signal, $object, &$data=null) { + if (!isset(self::$subscribers[$signal])) return; - foreach ($_subscribers[$signal] as $sub) { + foreach (self::$subscribers[$signal] as $sub) { list($s, $callable, $check) = $sub; if ($s && !is_a($object, $s)) continue; @@ -99,6 +99,4 @@ class Signal { } } } - -$_subscribers = array(); ?> diff --git a/include/class.staff.php b/include/class.staff.php index efd4f341c03f40cb3fa79756cc038533b7c86c7f..e353c5cd908c6ce9a1e316f03b3098fe3cc94556 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -83,8 +83,12 @@ class Staff extends AuthenticatedUser { return $this->load(); } + function __toString() { + return (string) $this->getName(); + } + function asVar() { - return $this->getName(); + return $this->__toString(); } function getHastable() { @@ -95,6 +99,17 @@ class Staff extends AuthenticatedUser { return $this->config->getInfo() + $this->getHastable(); } + // AuthenticatedUser implementation... + // TODO: Move to an abstract class that extends Staff + function getRole() { + return 'staff'; + } + + function getAuthBackend() { + list($authkey, ) = explode(':', $this->getAuthKey()); + return StaffAuthenticationBackend::getBackend($authkey); + } + /*compares user password*/ function check_passwd($password, $autoupdate=true) { diff --git a/include/class.thread.php b/include/class.thread.php index 975a3a47294705ba3341851fd4dc53b2eb6d64a8..6fa0d9c13cc74e85dd8e57dce0c4f90411c1166f 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -118,8 +118,7 @@ class Thread { .' FROM '.TICKET_THREAD_TABLE.' thread ' .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (thread.ticket_id=attach.ticket_id - AND thread.id=attach.ref_id - AND thread.thread_type=attach.ref_type) ' + AND thread.id=attach.ref_id) ' .' WHERE thread.ticket_id='.db_input($this->getTicketId()); if($type && is_array($type)) @@ -161,6 +160,7 @@ class Thread { function addResponse($vars, &$errors) { $vars['ticketId'] = $this->getTicketId(); + $vars['userId'] = 0; return Response::create($vars, $errors); } @@ -181,7 +181,7 @@ class Thread { /* XXX: Leave this out until TICKET_EMAIL_INFO_TABLE has a primary * key $sql = 'DELETE mid.* FROM '.TICKET_EMAIL_INFO_TABLE.' mid - INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.message_id) + INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.thread_id) WHERE thread.ticket_id = '.db_input($this->getTicketId()); db_query($sql); */ @@ -244,11 +244,10 @@ Class ThreadEntry { .' ,count(DISTINCT attach.attach_id) as attachments ' .' FROM '.TICKET_THREAD_TABLE.' thread ' .' LEFT JOIN '.TICKET_EMAIL_INFO_TABLE.' info - ON (thread.id=info.message_id) ' + ON (thread.id=info.thread_id) ' .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (thread.ticket_id=attach.ticket_id - AND thread.id=attach.ref_id - AND thread.thread_type=attach.ref_type) ' + AND thread.id=attach.ref_id) ' .' WHERE thread.id='.db_input($id); if($type) @@ -337,7 +336,7 @@ Class ThreadEntry { require_once(INCLUDE_DIR.'class.mailparse.php'); $sql = 'SELECT headers FROM '.TICKET_EMAIL_INFO_TABLE - .' WHERE message_id='.$this->getId(); + .' WHERE thread_id='.$this->getId(); $headers = db_result(db_query($sql)); return Mail_Parse::splitHeaders($headers); } @@ -352,6 +351,37 @@ Class ThreadEntry { return $this->_references; } + function getTaggedEmailReferences($prefix, $refId) { + + $ref = "+$prefix".Base32::encode(pack('VV', $this->getId(), $refId)); + + $mid = substr_replace($this->getEmailMessageId(), + $ref, strpos($this->getEmailMessageId(), '@'), 0); + + //TODO: Confirm how references are ordered on reply - we want the tagged + // reference to be processed first. + + return sprintf('%s %s', $this->getEmailReferences(), $mid); + } + + function getEmailReferencesForUser($user) { + return $this->getTaggedEmailReferences('u', $user->getId()); + } + + function getEmailReferencesForStaff($staff) { + return $this->getTaggedEmailReferences('s', $staff->getId()); + } + + function getUIDFromEmailReference($ref) { + + $info = unpack('Vtid/Vuid', + Base32::decode(strtolower(substr($ref, -13)))); + + if ($info && $info['tid'] == $this->getId()) + return $info['uid']; + + } + function getTicket() { if(!$this->ticket && $this->getTicketId()) @@ -372,6 +402,18 @@ Class ThreadEntry { return $this->staff; } + function getUserId() { + return $this->ht['user_id']; + } + + function getUser() { + + if (!isset($this->user)) + $this->user = User::lookup($this->getUserId()); + + return $this->user; + } + function getEmailHeader() { return $this->ht['headers']; } @@ -466,8 +508,7 @@ Class ThreadEntry { $sql ='INSERT IGNORE INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() ' .' ,file_id='.db_input($fileId) .' ,ticket_id='.db_input($this->getTicketId()) - .' ,ref_id='.db_input($this->getId()) - .' ,ref_type='.db_input($this->getType()); + .' ,ref_id='.db_input($this->getId()); return (db_query($sql) && ($id=db_insert_id()))?$id:0; } @@ -491,8 +532,7 @@ Class ThreadEntry { .' FROM '.FILE_TABLE.' f ' .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' .' WHERE a.ticket_id='.db_input($this->getTicketId()) - .' AND a.ref_id='.db_input($this->getId()) - .' AND a.ref_type='.db_input($this->getType()); + .' AND a.ref_id='.db_input($this->getId()); $this->attachments = array(); if(($res=db_query($sql)) && db_num_rows($res)) { @@ -585,14 +625,17 @@ Class ThreadEntry { // Disambiguate if the user happens also to be a staff member of the // system. The current ticket owner should _always_ post messages // instead of notes or responses - if (strcasecmp($mailinfo['email'], $ticket->getEmail()) == 0) { + if ($mailinfo['userId'] + || strcasecmp($mailinfo['email'], $ticket->getEmail()) == 0) { $vars['message'] = $body; + $vars['userId'] = $mailinfo['userId'] ? $mailinfo['userId'] : $ticket->getUserId(); return $ticket->postMessage($vars, 'Email'); } // XXX: Consider collaborator role - elseif ($staff_id = Staff::getIdByEmail($mailinfo['email'])) { - $vars['staffId'] = $staff_id; - $poster = Staff::lookup($staff_id); + elseif ($mailinfo['staffId'] + || ($mailinfo['staffId'] = Staff::getIdByEmail($mailinfo['email']))) { + $vars['staffId'] = $mailinfo['staffId']; + $poster = Staff::lookup($mailinfo['staffId']); $errors = array(); $vars['note'] = $body; return $ticket->postNote($vars, $errors, $poster); @@ -603,8 +646,11 @@ Class ThreadEntry { } // TODO: Consider security constraints else { + //XXX: Are we potentially leaking the email address to + // collaborators? $vars['message'] = sprintf("Received From: %s\n\n%s", $mailinfo['email'], $body); + $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner? return $ticket->postMessage($vars, 'Email'); } // Currently impossible, but indicate that this thread object could @@ -643,7 +689,7 @@ Class ThreadEntry { /* static */ function logEmailHeaders($id, $mid, $header=false) { $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE - .' SET message_id='.db_input($id) //TODO: change it to thread_id + .' SET thread_id='.db_input($id) .', email_mid='.db_input($mid); //TODO: change it to message_id. if ($header) $sql .= ', headers='.db_input($header); @@ -652,10 +698,14 @@ Class ThreadEntry { /* variables */ - function asVar() { + function __toString() { return $this->getBody(); } + function asVar() { + return (string) $this; + } + function getVar($tag) { global $cfg; @@ -705,11 +755,11 @@ Class ThreadEntry { * previously seen. This is useful if no thread-id is associated * with the email (if it was rejected for instance). */ - function lookupByEmailHeaders($mailinfo, &$seen=false) { + function lookupByEmailHeaders(&$mailinfo, &$seen=false) { // Search for messages using the References header, then the // in-reply-to header - $search = 'SELECT message_id, email_mid FROM '.TICKET_EMAIL_INFO_TABLE - . ' WHERE email_mid=%s ORDER BY message_id DESC'; + $search = 'SELECT thread_id, email_mid FROM '.TICKET_EMAIL_INFO_TABLE + . ' WHERE email_mid=%s ORDER BY thread_id DESC'; if (list($id, $mid) = db_fetch_row(db_query( sprintf($search, db_input($mailinfo['mid']))))) { @@ -732,10 +782,27 @@ Class ThreadEntry { // @see rfc 1036, section 2.2.5 // @see http://www.jwz.org/doc/threading.html foreach (array_reverse($matches[0]) as $mid) { + //Try to determine if it's a reply to a tagged email. + $ref = null; + if (strpos($mid, '+')) { + list($left, $right) = explode('@',$mid); + list($left, $ref) = explode('+', $left); + $mid = "$left@$right"; + } $res = db_query(sprintf($search, db_input($mid))); while (list($id) = db_fetch_row($res)) { - if ($t = ThreadEntry::lookup($id)) - return $t; + if (!($t = ThreadEntry::lookup($id))) continue; + + //We found a match - see if we can ID the user. + // XXX: Check access of ref is enough? + if ($ref && ($uid = $t->getUIDFromEmailReference($ref))) { + if ($ref[0] =='s') //staff + $mailinfo['staffId'] = $uid; + else //user or collaborator. + $mailinfo['userId'] = $uid; + } + + return $t; } } } @@ -745,12 +812,23 @@ Class ThreadEntry { // injection by third-party. $subject = $mailinfo['subject']; $match = array(); - if ($subject && $mailinfo['email'] + if ($subject + && $mailinfo['email'] && preg_match("/#(?:[\p{L}-]+)?([0-9]{1,10})/u", $subject, $match) - && ($tid = Ticket::getIdByExtId((int)$match[1], $mailinfo['email'])) - ) - // Return last message for the thread - return Message::lastByTicketId($tid); + //Lookup by ticket number + && ($ticket = Ticket::lookupByNumber((int)$match[1])) + //Lookup the user using the email address + && ($user = User::lookup(array('emails__address' => $mailinfo['email'])))) { + //We have a valid ticket and user + if ($ticket->getUserId() == $user->getId() //owner + || ($c = Collaborator::lookup( // check if collaborator + array('userId' => $user->getId(), + 'ticketId' => $ticket->getId())))) { + + $mailinfo['userId'] = $user->getId(); + return $ticket->getLastMessage(); + } + } return null; } @@ -798,7 +876,7 @@ Class ThreadEntry { $poster = $vars['poster']; if ($poster && is_object($poster)) - $poster = $poster->getName(); + $poster = (string) $poster; $sql=' INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() ' .' ,thread_type='.db_input($vars['type']) @@ -806,6 +884,7 @@ Class ThreadEntry { .' ,title='.db_input(Format::sanitize($vars['title'], true)) .' ,body='.db_input($vars['body']) .' ,staff_id='.db_input($vars['staffId']) + .' ,user_id='.db_input($vars['userId']) .' ,poster='.db_input($poster) .' ,source='.db_input($vars['source']); @@ -883,6 +962,11 @@ class Message extends ThreadEntry { $vars['type'] = 'M'; $vars['body'] = $vars['message']; + if (!$vars['poster'] + && $vars['userId'] + && ($user = User::lookup($vars['userId']))) + $vars['poster'] = (string) $user->getName(); + return ThreadEntry::add($vars); } @@ -949,6 +1033,11 @@ class Response extends ThreadEntry { if(!$vars['pid'] && $vars['msgId']) $vars['pid'] = $vars['msgId']; + if (!$vars['poster'] + && $vars['staffId'] + && ($staff = Staff::lookup($vars['staffId']))) + $vars['poster'] = (string) $staff->getName(); + return ThreadEntry::add($vars); } diff --git a/include/class.ticket.php b/include/class.ticket.php index 29ccc502e00545b1a4c856931dc5e53db2a36045..a815cb9328fdb385631407ffa9f09a64c23114e2 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -85,7 +85,7 @@ class Ticket { $this->ht = db_fetch_array($res); $this->id = $this->ht['ticket_id']; - $this->number = $this->ht['ticketID']; + $this->number = $this->ht['number']; $this->_answers = array(); $this->loadDynamicData(); @@ -156,17 +156,28 @@ class Ticket { || $staff->getId()==$this->getStaffId()); } - function checkClientAccess($client) { - global $cfg; + function checkUserAccess($user) { - if(!is_object($client) && !($client=Client::lookup($client))) + if (!$user || !($user instanceof EndUser)) return false; - if(!strcasecmp($client->getEmail(), $this->getEmail())) + //Ticket Owner + if ($user->getId() == $this->getUserId()) + return true; + + //Collaborator? + // 1) If the user was authorized via this ticket. + if ($user->getTicketId() == $this->getId() + && !strcasecmp($user->getRole(), 'collaborator')) + return true; + + // 2) Query the database to check for expanded access... + if (Collaborator::lookup(array( + 'userId' => $user->getId(), + 'ticketId' => $this->getId()))) return true; - return ($cfg && $cfg->showRelatedTickets() - && $client->getTicketId()==$this->getExtId()); + return false; } //Getters @@ -174,10 +185,6 @@ class Ticket { return $this->id; } - function getExtId() { - return $this->getNumber(); - } - function getNumber() { return $this->number; } @@ -187,9 +194,12 @@ class Ticket { } function getOwner() { - if (!isset($this->user)) - $this->user = User::lookup($this->getOwnerId()); - return $this->user; + + if (!isset($this->owner) + && ($u=User::lookup($this->getOwnerId()))) + $this->owner = new TicketOwner($u, $this); + + return $this->owner; } function getEmail(){ @@ -374,12 +384,16 @@ class Ticket { return $this->dept; } - function getClient() { + function getUserId() { + return $this->getOwnerId(); + } + + function getUser() { - if(!$this->client) - $this->client = Client::lookup($this->getExtId(), $this->getEmail()); + if(!isset($this->user) && $this->getOwner()) + $this->user = new EndUser($this->getOwner()); - return $this->client; + return $this->user; } function getStaffId() { @@ -583,6 +597,23 @@ class Ticket { return $this->collaborators; } + //UserList of recipients (owner + collaborators) + function getRecipients() { + + if (!isset($this->recipients)) { + $list = new UserList(); + $list->add($this->getOwner()); + if ($collabs = $this->getActiveCollaborators()) { + foreach ($collabs as $c) + $list->add($c); + } + $this->recipients = $list; + } + + return $this->recipients; + } + + function addCollaborator($user, $vars, &$errors) { if (!$user || $user->getId()==$this->getOwnerId()) @@ -595,6 +626,7 @@ class Ticket { return null; $this->collaborators = null; + $this->recipients = null; return $c; } @@ -852,6 +884,7 @@ class Ticket { $msg = $this->replaceVars($msg->asArray(), array('message' => $message, + 'recipient' => $this->getOwner(), 'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'') ); @@ -920,11 +953,11 @@ class Ticket { $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']); } - $client= $this->getClient(); + $user = $this->getOwner(); //Alert admin...this might be spammy (no option to disable)...but it is helpful..I think. $alert='Max. open tickets reached for '.$this->getEmail()."\n" - .'Open ticket: '.$client->getNumOpenTickets()."\n" + .'Open ticket: '.$user->getNumOpenTickets()."\n" .'Max Allowed: '.$cfg->getMaxOpenTickets()."\n\nNotice sent to the user."; $ost->alertAdmin('Overlimit Notice', $alert); @@ -937,41 +970,53 @@ class Ticket { $this->reload(); } - function activityNotice($vars) { + /* + * Notify collaborators on response or new message + * + */ + + function notifyCollaborators($entry) { global $cfg; - if (!$vars - || !$vars['variables'] - || !($collaborators=$this->getActiveCollaborators()) + if (!$entry instanceof ThreadEntry + || !($recipients=$this->getRecipients()) || !($dept=$this->getDept()) || !($tpl=$dept->getTemplate()) || !($msg=$tpl->getActivityNoticeMsgTemplate()) || !($email=$dept->getEmail())) return; + //Who posted the entry? + $uid = 0; + if ($entry instanceof Message) { + $poster = $entry->getUser(); + $uid = $entry->getUserId(); + } else + $poster = $entry->getStaff(); - $msg = $this->replaceVars($msg->asArray(), $vars['variables']); + $vars = array( + 'message' => (string) $entry, + 'poster' => $poster? $poster : 'A collaborator'); - if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) - $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; + $msg = $this->replaceVars($msg->asArray(), $vars); - $attachments = ($cfg->emailAttachments() && $vars['attachments'])?$vars['attachments']:array(); - $options = array(); - if($vars['inreplyto']) - $options['inreplyto'] = $vars['inreplyto']; - if($vars['references']) - $options['references'] = $vars['references']; + if ($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) + $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; - foreach($collaborators as $collaborator) { - $notice = $this->replaceVars($msg, array('recipient' => $collaborator)); - $email->send($collaborator->getEmail(), $notice['subj'], $notice['body'], $attachments, + $attachments = $cfg->emailAttachments()?$entry->getAttachments():array(); + $options = array('inreplyto' => $entry->getEmailMessageId()); + foreach ($recipients as $recipient) { + if ($uid == $recipient->getId()) continue; + $options['references'] = $entry->getEmailReferencesForUser($recipient); + $notice = $this->replaceVars($msg, array('recipient' => $recipient)); + $email->send($recipient->getEmail(), $notice['subj'], $notice['body'], $attachments, $options); } return; } - function onMessage($autorespond=true, $message=null) { + function onMessage($message, $autorespond=true) { global $cfg; db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastmessage=NOW() WHERE ticket_id='.db_input($this->getId())); @@ -988,13 +1033,18 @@ class Ticket { if($this->isClosed()) $this->reopen(); //reopen.. /********** double check auto-response ************/ - if($autorespond && (Email::getIdByEmail($this->getEmail()))) + if (!($user = $message->getUser())) + $autorespond=false; + elseif ($autorespond && (Email::getIdByEmail($user->getEmail()))) $autorespond=false; - elseif($autorespond && ($dept=$this->getDept())) + elseif ($autorespond && ($dept=$this->getDept())) $autorespond=$dept->autoRespONNewMessage(); - if(!$autorespond || !$cfg->autoRespONNewMessage()) return; //no autoresp or alerts. + if(!$autorespond + || !$cfg->autoRespONNewMessage() + || !$message) return; //no autoresp or alerts. + $this->reload(); $dept = $this->getDept(); $email = $dept->getAutoRespEmail(); @@ -1005,18 +1055,18 @@ class Ticket { && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) { $msg = $this->replaceVars($msg->asArray(), - array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')); + array( + 'recipient' => $user, + 'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')); //Reply separator tag. if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; - if (!$message) - $message = $this->getLastMessage(); $options = array( - 'inreplyto'=>$message->getEmailMessageId(), - 'references'=>$message->getEmailReferences()); - $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], + 'inreplyto' => $message->getEmailMessageId(), + 'references' => $message->getEmailReferencesForUser($user)); + $email->sendAutoReply($user->getEmail(), $msg['subj'], $msg['body'], null, $options); } } @@ -1456,9 +1506,10 @@ class Ticket { //Add email recipients as collaborators if ($vars['recipients']) { - //TODO: Disable collaborators added by other collaborator - // staff approval will be required. - $info = array('isactive' => 1); + //New collaborators added by other collaborators are disable -- + // requires staff approval. + $info = array( + 'isactive' => ($message->getUserId() == $this->getUserId())? 1: 0); $collabs = array(); foreach ($vars['recipients'] as $recipient) { if (($user=User::fromVars($recipient))) @@ -1482,7 +1533,7 @@ class Ticket { if ($autorespond && $message->isAutoResponse()) $autorespond=false; - $this->onMessage($autorespond, $message); //must be called b4 sending alerts to staff. + $this->onMessage($message, $autorespond); //must be called b4 sending alerts to staff. $dept = $this->getDept(); @@ -1526,12 +1577,7 @@ class Ticket { } } - unset($variables['message']); - $variables['message'] = $message->asVar(); - - $info = $options + array('variables' => $variables); - $this->activityNotice($info); - + $this->notifyCollaborators($message); return $message; } @@ -1636,7 +1682,8 @@ class Ticket { && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getReplyMsgTemplate())) { - $msg = $this->replaceVars($msg->asArray(), $variables); + $msg = $this->replaceVars($msg->asArray(), + $variables + array('recipient' => $this->getOwner())); if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) $msg['body'] = "<p style=\"display:none\">$tag<p>".$msg['body']; @@ -1645,12 +1692,8 @@ class Ticket { $options); } - if($vars['emailcollab']) { - unset($variables['response']); - $variables['message'] = $response->asVar(); - $info = $options + array('variables' => $variables); - $this->activityNotice($info); - } + if($vars['emailcollab']) + $this->notifyCollaborators($response); return $response; } @@ -1770,7 +1813,7 @@ class Ticket { function pdfExport($psize='Letter', $notes=false) { require_once(INCLUDE_DIR.'class.pdf.php'); $pdf = new Ticket2PDF($this, $psize, $notes); - $name='Ticket-'.$this->getExtId().'.pdf'; + $name='Ticket-'.$this->getNumber().'.pdf'; $pdf->Output($name, 'I'); //Remember what the user selected - for autoselect on the next print. $_SESSION['PAPER_SIZE'] = $psize; @@ -1869,15 +1912,15 @@ class Ticket { /*============== Static functions. Use Ticket::function(params); =============nolint*/ - function getIdByExtId($extId, $email=null) { + function getIdByNumber($number, $email=null) { - if(!$extId || !is_numeric($extId)) + if(!$number) return 0; $sql ='SELECT ticket.ticket_id FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id' .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id' - .' WHERE ticket.ticketID='.db_input($extId); + .' WHERE ticket.`number`='.db_input($number); if($email) $sql .= ' AND email.address = '.db_input($email); @@ -1898,20 +1941,20 @@ class Ticket { ?$ticket:null; } - function lookupByExtId($id, $email=null) { - return self::lookup(self:: getIdByExtId($id, $email)); + function lookupByNumber($number, $email=null) { + return self::lookup(self:: getIdByNumber($number, $email)); } - function genExtRandID() { - global $cfg; + function genRandTicketNumber($len = EXT_TICKET_ID_LEN) { - //We can allow collissions...extId and email must be unique ...so same id with diff emails is ok.. - // But for clarity...we are going to make sure it is unique. - $id=Misc::randNumber(EXT_TICKET_ID_LEN); - if(db_num_rows(db_query('SELECT ticket_id FROM '.TICKET_TABLE.' WHERE ticketID='.db_input($id)))) - return Ticket::genExtRandID(); + //We can allow collissions...number and email must be unique ...so + // same number with diff emails is ok.. But for clarity...we are going to make sure it is unique. + $number = Misc::randNumber($len); + if(db_num_rows(db_query('SELECT ticket_id FROM '.TICKET_TABLE.' + WHERE `number`='.db_input($number)))) + return Ticket::genRandTicketNumber($len); - return $id; + return $number; } function getIdByMessageId($mid, $email) { @@ -1992,11 +2035,8 @@ class Ticket { /* Quick client's tickets stats @email - valid email. */ - function getClientStats($email) { - - if(!$email || !Validator::is_email($email)) - return null; - if (!$user = User::lookup(array('emails__address'=>$email))) + function getUserStats($user) { + if(!$user || !($user instanceof EndUser)) return null; $sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed ' @@ -2046,8 +2086,8 @@ class Ticket { //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL) if($cfg->getMaxOpenTickets()>0 && strcasecmp($origin,'staff') - && ($client=Client::lookupByEmail($vars['email'])) - && ($openTickets=$client->getNumOpenTickets()) + && ($user=TicketUser::lookupByEmail($vars['email'])) + && ($openTickets=$user->getNumOpenTickets()) && ($openTickets>=$cfg->getMaxOpenTickets()) ) { $errors['err']="You've reached the maximum open tickets allowed."; @@ -2201,11 +2241,11 @@ class Ticket { $ipaddress=$vars['ip']?$vars['ip']:$_SERVER['REMOTE_ADDR']; //We are ready son...hold on to the rails. - $extId=Ticket::genExtRandID(); + $number = Ticket::genRandTicketNumber(); $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() ' .' ,lastmessage= NOW()' .' ,user_id='.db_input($user->id) - .' ,ticketID='.db_input($extId) + .' ,`number`='.db_input($number) .' ,dept_id='.db_input($deptId) .' ,topic_id='.db_input($topicId) .' ,ip_address='.db_input($ipaddress) @@ -2222,9 +2262,9 @@ class Ticket { /* -------------------- POST CREATE ------------------------ */ if(!$cfg->useRandomIds()) { - //Sequential ticketIDs support really..really suck arse. - $extId=$id; //To make things really easy we are going to use autoincrement ticket_id. - db_query('UPDATE '.TICKET_TABLE.' SET ticketID='.db_input($extId).' WHERE ticket_id='.$id.' LIMIT 1'); + //Sequential ticket number support really..really suck arse. + //To make things really easy we are going to use autoincrement ticket_id. + db_query('UPDATE '.TICKET_TABLE.' SET `number`='.db_input($id).' WHERE ticket_id='.$id.' LIMIT 1'); //TODO: RETHING what happens if this fails?? [At the moment on failure random ID is used...making stuff usable] } @@ -2238,6 +2278,7 @@ class Ticket { //post the message. unset($vars['cannedattachments']); //Ticket::open() might have it set as part of open & respond. $vars['title'] = $vars['subject']; //Use the initial subject as title of the post. + $vars['userId'] = $ticket->getUserId(); $message = $ticket->postMessage($vars , $origin, false); // Configure service-level-agreement for this ticket @@ -2283,8 +2324,8 @@ class Ticket { /************ check if the user JUST reached the max. open tickets limit **********/ if($cfg->getMaxOpenTickets()>0 - && ($client=$ticket->getClient()) - && ($client->getNumOpenTickets()==$cfg->getMaxOpenTickets())) { + && ($user=$ticket->getOwner()) + && ($user->getNumOpenTickets()==$cfg->getMaxOpenTickets())) { $ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff'))); } diff --git a/include/class.user.php b/include/class.user.php index 9102771c846e69f64d0d5442e5d7b8700c5b3c20..e74e922a56e65ebf82bccbcdc2d7393688fc3e7f 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -138,6 +138,10 @@ class User extends UserModel { return $this->updated; } + function getCreateDate() { + return $this->created; + } + function to_json() { $info = array( @@ -464,4 +468,55 @@ class UserEmail extends UserEmailModel { } } + +/* + * Generic user list. + */ +class UserList implements IteratorAggregate, ArrayAccess { + private $users; + + function __construct($list = array()) { + $this->users = $list; + } + + function add($user) { + $this->offsetSet(null, $user); + } + + function offsetSet($offset, $value) { + + if (is_null($offset)) + $this->users[] = $value; + else + $this->users[$offset] = $value; + } + + function offsetExists($offset) { + return isset($this->users[$offset]); + } + + function offsetUnset($offset) { + unset($this->users[$offset]); + } + + function offsetGet($offset) { + return isset($this->users[$offset]) ? $this->users[$offset] : null; + } + + function getIterator() { + return new ArrayIterator($this->users); + } + + function __toString() { + + $list = array(); + foreach($this->users as $user) { + if (is_object($user)) + $list [] = $user->getName(); + } + + return $list ? implode(', ', $list) : ''; + } +} + ?> diff --git a/include/class.usersession.php b/include/class.usersession.php index e77f65fac77f63853cb3c6a4dea99d93e51ea491..1eae3af81e058246f68d49c11c2660f489ce5d97 100644 --- a/include/class.usersession.php +++ b/include/class.usersession.php @@ -111,13 +111,13 @@ class UserSession { } -class ClientSession extends Client { +class ClientSession extends EndUser { var $session; - function ClientSession($email, $id){ - parent::Client($id, $email); - $this->session= new UserSession($email); + function __construct($user) { + parent::__construct($user); + $this->session= new UserSession($user->getUserName()); } function isValid(){ @@ -156,13 +156,13 @@ class StaffSession extends Staff { var $session; - function StaffSession($var){ - parent::Staff($var); + function __construct($var) { + parent::__construct($var); $this->session= new UserSession($this->getId()); } function isValid(){ - global $_SESSION,$cfg; + global $_SESSION, $cfg; if(!$this->getId() || $this->session->getSessionId()!=session_id()) return false; diff --git a/include/class.variable.php b/include/class.variable.php index 36d49e6a39accdcdee68aaa06cd8059779474e00..233c9fc18696886460991b35bf3dba18701762cc 100644 --- a/include/class.variable.php +++ b/include/class.variable.php @@ -63,11 +63,15 @@ class VariableReplacer { if(!$obj) return ""; - if(!$var && is_callable(array($obj, 'asVar'))) - return call_user_func(array($obj, 'asVar')); + if (!$var) { + if (method_exists($obj, 'asVar')) + return call_user_func(array($obj, 'asVar')); + elseif (method_exists($obj, '__toString')) + return (string) $obj; + } list($v, $part) = explode('.', $var, 2); - if($v && is_callable(array($obj, 'get'.ucfirst($v)))) { + if ($v && is_callable(array($obj, 'get'.ucfirst($v)))) { $rv = call_user_func(array($obj, 'get'.ucfirst($v))); if(!$rv || !is_object($rv)) return $rv; @@ -75,7 +79,7 @@ class VariableReplacer { return $this->getVar($rv, $part); } - if(!$var || !is_callable(array($obj, 'getVar'))) + if (!$var || !method_exists($obj, 'getVar')) return ""; $parts = explode('.', $var); diff --git a/include/client/header.inc.php b/include/client/header.inc.php index 9f20098b4b1b4fcfc2d891973717c42876296fde..4f1c93f88433f82744d328bbe7b70e0687ad6669 100644 --- a/include/client/header.inc.php +++ b/include/client/header.inc.php @@ -48,7 +48,7 @@ header("Content-Type: text/html; charset=UTF-8\r\n"); ?> <?php if($cfg->showRelatedTickets()) {?> - <a href="<?php echo ROOT_PATH; ?>tickets.php">My Tickets <b>(<?php echo $thisclient->getNumTickets(); ?>)</b></a> - + <a href="<?php echo ROOT_PATH; ?>tickets.php">Tickets <b>(<?php echo $thisclient->getNumTickets(); ?>)</b></a> - <?php } ?> <a href="<?php echo ROOT_PATH; ?>logout.php?auth=<?php echo $ost->getLinkToken(); ?>">Log Out</a> diff --git a/include/client/open.inc.php b/include/client/open.inc.php index 75211c0982b687475e41e2b9bc4e738aab1d1a7d..3f070c703d9c32b63fc43ca1f3001aff0207f546 100644 --- a/include/client/open.inc.php +++ b/include/client/open.inc.php @@ -4,7 +4,7 @@ $info=array(); if($thisclient && $thisclient->isValid()) { $info=array('name'=>$thisclient->getName(), 'email'=>$thisclient->getEmail(), - 'phone'=>$thisclient->getPhone()); + 'phone'=>$thisclient->getPhoneNumber()); } $info=($_POST && $errors)?Format::htmlchars($_POST):$info; diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 7c2c6e56436e5fcf454caf8c6dca5d117f4f6e72..1c227ba4cdec255b7e75607abfeab143e549d305 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -18,8 +18,8 @@ if(isset($_REQUEST['status'])) { //Query string status has nothing to do with th $status='open'; //Defaulting to open } -$sortOptions=array('id'=>'ticketID', 'name'=>'user.name', 'subject'=>'subject.value', - 'email'=>'email.address', 'status'=>'ticket.status', 'dept'=>'dept_name','date'=>'ticket.created'); +$sortOptions=array('id'=>'`number`', 'subject'=>'subject.value', + 'status'=>'ticket.status', 'dept'=>'dept_name','date'=>'ticket.created'); $orderWays=array('DESC'=>'DESC','ASC'=>'ASC'); //Sorting options... $order_by=$order=null; @@ -38,9 +38,8 @@ if($order_by && strpos($order_by,',')) $x=$sort.'_sort'; $$x=' class="'.strtolower($order).'" '; -$qselect='SELECT ticket.ticket_id,ticket.ticketID,ticket.dept_id,isanswered, ' - .'dept.ispublic, subject.value as subject, ' - .'user.name, email.address as email, ' +$qselect='SELECT ticket.ticket_id,ticket.`number`,ticket.dept_id,isanswered, ' + .'dept.ispublic, subject.value as subject,' .'dept_name,ticket. status, ticket.source, ticket.created '; $dynfields='(SELECT entry.object_id, value FROM '.FORM_ANSWER_TABLE.' ans '. @@ -51,11 +50,13 @@ $subject_sql = sprintf($dynfields, 'subject'); $qfrom='FROM '.TICKET_TABLE.' ticket ' .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) ' - .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id' - .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id' + .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab + ON (collab.ticket_id = ticket.ticket_id + AND collab.user_id ='.$thisclient->getId().' )' .' LEFT JOIN '.$subject_sql.' subject ON ticket.ticket_id = subject.object_id '; -$qwhere =' WHERE email.address='.db_input($thisclient->getEmail()); +$qwhere = sprintf(' WHERE ( ticket.user_id=%d OR collab.user_id=%d )', + $thisclient->getId(), $thisclient->getId()); if($status){ $qwhere.=' AND ticket.status='.db_input($status); @@ -65,7 +66,7 @@ $search=($_REQUEST['a']=='search' && $_REQUEST['q']); if($search) { $qstr.='&a='.urlencode($_REQUEST['a']).'&q='.urlencode($_REQUEST['q']); if(is_numeric($_REQUEST['q'])) { - $qwhere.=" AND ticket.ticketID LIKE '$queryterm%'"; + $qwhere.=" AND ticket.`number` LIKE '$queryterm%'"; } else {//Deep search! $queryterm=db_real_escape($_REQUEST['q'],false); //escape the term ONLY...no quotes. $qwhere.=' AND ( ' @@ -100,7 +101,7 @@ if($search) $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting ?> -<h1>My Tickets</h1> +<h1>Tickets</h1> <br> <form action="tickets.php" method="get" id="ticketSearchForm"> <input type="hidden" name="a" value="search"> @@ -152,24 +153,24 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting if($row['attachments']) $subject.=' <span class="Icon file"></span>'; - $ticketID=$row['ticketID']; + $ticketNumber=$row['number']; if($row['isanswered'] && !strcasecmp($row['status'],'open')) { $subject="<b>$subject</b>"; - $ticketID="<b>$ticketID</b>"; + $ticketNumber="<b>$ticketNumber</b>"; } $phone=Format::phone($row['phone']); if($row['phone_ext']) $phone.=' '.$row['phone_ext']; ?> - <tr id="<?php echo $row['ticketID']; ?>"> + <tr id="<?php echo $row['ticket_id']; ?>"> <td class="centered"> <a class="Icon <?php echo strtolower($row['source']); ?>Ticket" title="<?php echo $row['email']; ?>" - href="tickets.php?id=<?php echo $row['ticketID']; ?>"><?php echo $ticketID; ?></a> + href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $ticketNumber; ?></a> </td> <td> <?php echo Format::db_date($row['created']); ?></td> <td> <?php echo ucfirst($row['status']); ?></td> <td> - <a href="tickets.php?id=<?php echo $row['ticketID']; ?>"><?php echo $subject; ?></a> + <a href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a> </td> <td> <?php echo Format::truncate($dept,30); ?></td> <td><?php echo $phone; ?></td> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 910d021de0fd98940996d90b311fb54a848a134e..c36eaec96dd81d06804a5cd44d26fc986a6d2737 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -1,5 +1,5 @@ <?php -if(!defined('OSTCLIENTINC') || !$thisclient || !$ticket || !$ticket->checkClientAccess($thisclient)) die('Access Denied!'); +if(!defined('OSTCLIENTINC') || !$thisclient || !$ticket || !$ticket->checkUserAccess($thisclient)) die('Access Denied!'); $info=($_POST && $errors)?Format::htmlchars($_POST):array(); @@ -13,8 +13,8 @@ if(!$dept || !$dept->isPublic()) <tr> <td colspan="2" width="100%"> <h1> - Ticket #<?php echo $ticket->getExtId(); ?> - <a href="view.php?id=<?php echo $ticket->getExtId(); ?>" title="Reload"><span class="Icon refresh"> </span></a> + Ticket #<?php echo $ticket->getNumber(); ?> + <a href="view.php?id=<?php echo $ticket->getId(); ?>" title="Reload"><span class="Icon refresh"> </span></a> </h1> </td> </tr> @@ -124,10 +124,10 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { <?php }elseif($warn) { ?> <div id="msg_warning"><?php echo $warn; ?></div> <?php } ?> -<form id="reply" action="tickets.php?id=<?php echo $ticket->getExtId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data"> +<form id="reply" action="tickets.php?id=<?php echo $ticket->getId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data"> <?php csrf_token(); ?> <h2>Post a Reply</h2> - <input type="hidden" name="id" value="<?php echo $ticket->getExtId(); ?>"> + <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> <input type="hidden" name="a" value="reply"> <table border="0" cellspacing="0" cellpadding="3" style="width:100%"> <tr> @@ -143,7 +143,7 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { <br/> <textarea name="message" id="message" cols="50" rows="9" wrap="soft" data-draft-namespace="ticket.client" - data-draft-object-id="<?php echo $ticket->getExtId(); ?>" + data-draft-object-id="<?php echo $ticket->getId(); ?>" class="richtext ifhtml draft"><?php echo $info['message']; ?></textarea> </td> </tr> diff --git a/include/i18n/en_US/templates/email/message.autoresp.yaml b/include/i18n/en_US/templates/email/message.autoresp.yaml index 30c641de0f74048f297333de499197865c7a2dbd..61905d57f04f06f754f0c30a6f590f3eaca48292 100644 --- a/include/i18n/en_US/templates/email/message.autoresp.yaml +++ b/include/i18n/en_US/templates/email/message.autoresp.yaml @@ -14,9 +14,9 @@ notes: | subject: | Message Confirmation body: | - <h3><strong>Dear %{ticket.name.first},</strong></h3> + <h3><strong>Dear %{recipient.name.first},</strong></h3> Your reply to support request <a - href="%{ticket.client_link}">#%{ticket.number}</a> has been noted + href="%{recipient.ticket_link}">#%{ticket.number}</a> has been noted <br> <br> <div style="color: rgb(127, 127, 127); "> @@ -26,7 +26,7 @@ body: | <hr> <div style="color: rgb(127, 127, 127); font-size: small; text-align: center"><em>You can view the support request progress <a - href="%{ticket.client_link}">online here</a></em><br> + href="%{recipient.ticket_link}">online here</a></em><br> <a href="http://osticket.com/"><img src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc" alt="Powered by osTicket" width="126" height="19" style="width: 126px;"></a></div> diff --git a/include/i18n/en_US/templates/email/ticket.activity.notice.yaml b/include/i18n/en_US/templates/email/ticket.activity.notice.yaml index 4248931a815db98cd8cd3a7ce35a2048ee07da2c..ee3023175914465493c0e808456bee1f7f858ce9 100644 --- a/include/i18n/en_US/templates/email/ticket.activity.notice.yaml +++ b/include/i18n/en_US/templates/email/ticket.activity.notice.yaml @@ -27,9 +27,9 @@ body: | <hr> <div style="color: rgb(127, 127, 127); font-size: small; text-align: center;"> <em>You're getting this email because you are a collaborator - on ticket <a href="%{ticket.client_link}" style="color: rgb(84, 141, 212);" + on ticket <a href="%{recipient.ticket_link}" style="color: rgb(84, 141, 212);" >#%{ticket.number}</a>. To participate, simply reply to this email - or <a href="%{ticket.client_link}" style="color: rgb(84, 141, 212);" + or <a href="%{recipient.ticket_link}" style="color: rgb(84, 141, 212);" >click here</a> for a complete archive of the ticket thread.</em> </div> <div style="text-align: center;"> <a href="http://osticket.com/"><img diff --git a/include/i18n/en_US/templates/email/ticket.autoreply.yaml b/include/i18n/en_US/templates/email/ticket.autoreply.yaml index e50551566ac31ec72311dcd584fe70bde5bfc467..eb7a50c5a6e3852d636a89214a36c03967c327cd 100644 --- a/include/i18n/en_US/templates/email/ticket.autoreply.yaml +++ b/include/i18n/en_US/templates/email/ticket.autoreply.yaml @@ -21,11 +21,11 @@ body: | float: right; margin: 0px 0px 10px 10px;"> <br> <br> - <strong>Dear %{ticket.name.first},</strong> + <strong>Dear %{recipient.name.first},</strong> <br> <br> A request for support has been created and assigned ticket <a - href="%{ticket.client_link}">#%{ticket.number}</a> with the following + href="%{recipient.ticket_link}">#%{ticket.number}</a> with the following automatic reply <br> <br> @@ -43,7 +43,7 @@ body: | <div style="color: rgb(127, 127, 127); font-size: small; text-align: center;"><em>We hope this response has sufficiently answered your questions. If not, please do not send another email. Instead, reply to this email - or <a href="%{ticket.client_link}"><span style="color: rgb(84, 141, + or <a href="%{recipient.ticket_link}"><span style="color: rgb(84, 141, 212);" >login to your account</span></a> for a complete archive of all your support requests and responses.</em></div> <div style="text-align: center;"> diff --git a/include/i18n/en_US/templates/email/ticket.autoresp.yaml b/include/i18n/en_US/templates/email/ticket.autoresp.yaml index ebdfde6f106bed221ff5ebe44af66f4456dea58f..47e05b5995ab8e918f33ff58292e8eb67ef8631b 100644 --- a/include/i18n/en_US/templates/email/ticket.autoresp.yaml +++ b/include/i18n/en_US/templates/email/ticket.autoresp.yaml @@ -25,7 +25,7 @@ body: | </tr> <tr> <td style="width:67%; padding-top: 12pt; padding-right: 12pt"> - Dear %{ticket.name.first}, + Dear %{recipient.name.first}, <br> <br> We received your request and assigned ticket #%{ticket.number} @@ -39,7 +39,7 @@ body: | <br> <br> A representative will follow-up with you as soon as possible. - You can <a href="%{ticket.client_link}">view this ticket's + You can <a href="%{recipient.ticket_link}">view this ticket's progress online</a>. <br> <br> @@ -54,7 +54,7 @@ body: | <span style="color: rgb(127, 127, 127); "> If you wish to send additional comments or information regarding this issue, please don't open a new ticket. Simply</span> <a - href="%{ticket.client_link}"><span style="color: rgb(84, 141, 212);" + href="%{recipient.ticket_link}"><span style="color: rgb(84, 141, 212);" >login</span></a> <span style="color: rgb(127, 127, 127);" >and update the ticket.</span> <br> diff --git a/include/i18n/en_US/templates/email/ticket.notice.yaml b/include/i18n/en_US/templates/email/ticket.notice.yaml index 530f32a1f78fb33bab55b6b7b2942ffdaa798d73..55d0bb0d218acd01e22bffbd20f8a342ce496a06 100644 --- a/include/i18n/en_US/templates/email/ticket.notice.yaml +++ b/include/i18n/en_US/templates/email/ticket.notice.yaml @@ -19,11 +19,11 @@ body: | float: right; margin: 0px 0px 10px 10px;"> <br> <br> - <strong>Dear %{ticket.name.first},</strong> + <strong>Dear %{recipient.name.first},</strong> <br> <br> Our customer care team has created a ticket, <a - href="%{ticket.client_link}">#%{ticket.number}</a> on your behalf, with + href="%{recipient.ticket_link}">#%{ticket.number}</a> on your behalf, with the following details and summary: <br> <br> @@ -36,7 +36,7 @@ body: | <br> <br> If need be, a representative will follow-up with you as soon as - possible. You can also <a href="%{ticket.client_link}">view this + possible. You can also <a href="%{recipient.ticket_link}">view this ticket's progress online</a>. <br> <br> @@ -47,7 +47,7 @@ body: | <div style="color: rgb(127, 127, 127); font-size: small; "><em>If you wish to provide additional comments or information regarding the issue, please do not send another email. Instead, reply to this email or <a - href="%{ticket.client_link}"><span style="color: rgb(84, 141, 212);" + href="%{recipient.ticket_link}"><span style="color: rgb(84, 141, 212);" >login to your account</span></a> for a complete archive of all your support requests and responses.</em></div> <br> diff --git a/include/i18n/en_US/templates/email/ticket.reply.yaml b/include/i18n/en_US/templates/email/ticket.reply.yaml index c576a720945eb7b490132aa8d31c30d468da8a17..80774c737635171d069cb4f9fa9e3f4dac8b2989 100644 --- a/include/i18n/en_US/templates/email/ticket.reply.yaml +++ b/include/i18n/en_US/templates/email/ticket.reply.yaml @@ -15,7 +15,7 @@ body: | width="113" height="64" style="float: right; width: 113px; margin: 0px 0px 10px 10px;"> <h3><span style="color: rgb(127, 127, 127); font-weight: normal; font-family: Georgia; font-size: 30pt">We're Here For You</span></h3> - <strong>Dear %{ticket.name},</strong> + <strong>Dear %%{recipient.name},</strong> <br> <br> %{response} @@ -29,7 +29,7 @@ body: | <div style="color: rgb(127, 127, 127); font-size: small; text-align: center;" ><em>We hope this response has sufficiently answered your questions. If not, please do not send another email. Instead, reply to this email or - <a href="%{ticket.client_link}" style="color: rgb(84, 141, 212);" >login + <a href="%{recipient.ticket_link}" style="color: rgb(84, 141, 212);" >login to your account</a> for a complete archive of all your support requests and responses.</em></div> <div style="text-align: center;"> diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php index 667b462312b603a205c912584dafb35fe15ac06c..55351fbee7864581e1294a72e56d758d2d068c48 100644 --- a/include/staff/staff.inc.php +++ b/include/staff/staff.inc.php @@ -121,7 +121,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); $('#password-fields').show(); "> <option value="">— Use any available backend —</option> - <?php foreach (AuthenticationBackend::allRegistered() as $ab) { + <?php foreach (StaffAuthenticationBackend::allRegistered() as $ab) { if (!$ab->supportsAuthentication()) continue; ?> <option value="<?php echo $ab::$id; ?>" <?php if ($info['backend'] == $ab::$id) diff --git a/include/staff/ticket-edit.inc.php b/include/staff/ticket-edit.inc.php index cddb2ee3c6b36f248110fb85833cdb87760b9b9a..e60f275c2633c237a51f91eeab1b048bf0abf980 100644 --- a/include/staff/ticket-edit.inc.php +++ b/include/staff/ticket-edit.inc.php @@ -11,7 +11,7 @@ if ($_POST) <input type="hidden" name="do" value="update"> <input type="hidden" name="a" value="edit"> <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>"> - <h2>Update Ticket #<?php echo $ticket->getExtId(); ?></h2> + <h2>Update Ticket #<?php echo $ticket->getNumber(); ?></h2> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> <tbody> <tr> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 6fee37da63a4817fb462815677b2a0bf14370f87..a9b1c8edf41bb9f0169008239dd1b56599d84b05 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -15,6 +15,7 @@ if($cfg->getLockTime() && !$ticket->acquireLock($thisstaff->getId(),$cfg->getLoc //Get the goodies. $dept = $ticket->getDept(); //Dept $staff = $ticket->getStaff(); //Assigned or closed by.. +$user = $ticket->getOwner(); //Ticket User (EndUser) $team = $ticket->getTeam(); //Assigned team. $sla = $ticket->getSLA(); $lock = $ticket->getLock(); //Ticket lock obj @@ -40,7 +41,8 @@ if($ticket->isOverdue()) <table width="940" cellpadding="2" cellspacing="0" border="0"> <tr> <td width="50%" class="has_bottom_border"> - <h2><a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="Reload"><i class="icon-refresh"></i> Ticket #<?php echo $ticket->getExtId(); ?></a></h2> + <h2><a href="tickets.php?id=<?php echo $ticket->getId(); ?>" + title="Reload"><i class="icon-refresh"></i> Ticket #<?php echo $ticket->getNumber(); ?></a></h2> </td> <td width="50%" class="right_align has_bottom_border"> <?php @@ -161,19 +163,19 @@ if($ticket->isOverdue()) ><?php echo Format::htmlchars($ticket->getName()); ?></span></a> <?php - if(($client=$ticket->getClient())) { + if($user) { echo sprintf(' <a href="tickets.php?a=search&ownerId=%d" title="Related Tickets" data-dropdown="#action-dropdown-stats">(<b>%d</b>)</a>', - urlencode($ticket->getOwnerId()), $client->getNumTickets()); + urlencode($user->getId()), $user->getNumTickets()); ?> <div id="action-dropdown-stats" class="action-dropdown anchor-right"> <ul> <?php - if(($open=$client->getNumOpenTickets())) + if(($open=$user->getNumOpenTickets())) echo sprintf('<li><a href="tickets.php?a=search&status=open&ownerId=%s"><i class="icon-folder-open-alt"></i> %d Open Tickets</a></li>', - $ticket->getOwnerId(), $open); - if(($closed=$client->getNumClosedTickets())) + $user->getId(), $open); + if(($closed=$user->getNumClosedTickets())) echo sprintf('<li><a href="tickets.php?a=search&status=closed&ownerId=%d"><i class="icon-folder-close-alt"></i> %d Closed Tickets</a></li>', - $ticket->getOwnerId(), $closed); + $user->getId(), $closed); ?> <li><a href="tickets.php?a=search&ownerId=<?php echo $ticket->getOwnerId(); ?>"><i class="icon-double-angle-right"></i> All Tickets</a></li> </u> diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php index bbd837c9ce8b94f5cd292854f13e85950a47be18..530d6b2d93ec7262370d81fed89c0a7bab14bdfa 100644 --- a/include/staff/tickets.inc.php +++ b/include/staff/tickets.inc.php @@ -115,7 +115,7 @@ if($search): $qstr.='&query='.urlencode($searchTerm); $queryterm=db_real_escape($searchTerm,false); //escape the term ONLY...no quotes. if (is_numeric($searchTerm)) { - $qwhere.=" AND ticket.ticketID LIKE '$queryterm%'"; + $qwhere.=" AND ticket.`number` LIKE '$queryterm%'"; } elseif (strpos($searchTerm,'@') && Validator::is_email($searchTerm)) { //pulling all tricks! # XXX: What about searching for email addresses in the body of @@ -143,7 +143,7 @@ if ($_REQUEST['advsid'] && isset($_SESSION['adv_'.$_REQUEST['advsid']])) { db_input($_SESSION['adv_'.$_REQUEST['advsid']])).')'; } -$sortOptions=array('date'=>'effective_date','ID'=>'ticketID', +$sortOptions=array('date'=>'effective_date','ID'=>'`number`', 'pri'=>'priority_id','name'=>'user.name','subj'=>'subject', 'status'=>'ticket.status','assignee'=>'assigned','staff'=>'staff', 'dept'=>'dept_name'); @@ -195,7 +195,7 @@ $$x=' class="'.strtolower($order).'" '; if($_GET['limit']) $qstr.='&limit='.urlencode($_GET['limit']); -$qselect ='SELECT ticket.ticket_id,lock_id,ticketID,ticket.dept_id,ticket.staff_id,ticket.team_id ' +$qselect ='SELECT ticket.ticket_id,lock_id,`number`,ticket.dept_id,ticket.staff_id,ticket.team_id ' .' ,user.name' .' ,email.address as email, dept_name ' .' ,ticket.status,ticket.source,isoverdue,isanswered,ticket.created '; @@ -386,7 +386,7 @@ if ($results) { }else{ $lc=Format::truncate($row['dept_name'],40); } - $tid=$row['ticketID']; + $tid=$row['number']; $subject = Format::htmlchars(Format::truncate($row['subject'],40)); $threadcount=$row['thread_count']; if(!strcasecmp($row['status'],'open') && !$row['isanswered'] && !$row['lock_id']) { diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 4fecf9e0832d67cadca7a05979a2e1f7fe464853..ac8e6b7747418718552fa3ecf14189a652bf9901 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -ed60ba203a473f4f32ac49eb45db16c7 +934954de8914d9bd2bb8343e805340ae diff --git a/include/upgrader/streams/core/ed60ba20-934954de.patch.sql b/include/upgrader/streams/core/ed60ba20-934954de.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..56c0b164ebc842d86be4a97e761764553e69f4f3 --- /dev/null +++ b/include/upgrader/streams/core/ed60ba20-934954de.patch.sql @@ -0,0 +1,50 @@ +/** + * @version v1.8.1 + * @signature 934954de8914d9bd2bb8343e805340ae + * @title Various schema improvements and bug fixes + * + */ + +-- [#317](https://github.com/osTicket/osTicket-1.8/issues/317) +ALTER TABLE `%TABLE_PREFIX%faq` + CHANGE `created` `created` datetime NOT NULL, + CHANGE `updated` `updated` datetime NOT NULL; + +-- [#328](https://github.com/osTicket/osTicket-1.8/issues/328) +UPDATE `%TABLE_PREFIX%filter_rule` + SET `how` = 'equal' WHERE `how` IS NULL; + +-- [#331](https://github.com/osTicket/osTicket-1.8/issues/331) +ALTER TABLE `%TABLE_PREFIX%ticket_email_info` + CHANGE `message_id` `thread_id` int(11) unsigned NOT NULL, + ADD PRIMARY KEY (`thread_id`), + DROP INDEX `message_id`, + ADD INDEX `email_mid` (`email_mid`); + +-- [#386](https://github.com/osTicket/osTicket-1.8/issues/386) +UPDATE `%TABLE_PREFIX%email_template` + SET `body` = REPLACE(`body`, '%{recipient}', '%{recipient.name}'); + +-- Change EndUser link to be recipient specific +UPDATE `%TABLE_PREFIX%email_template` + SET `body` = REPLACE(`body`, '%{ticket.client_link}', '%{recipient.ticket_link}'); + +-- Add inline flag and drop ref_type +ALTER TABLE `%TABLE_PREFIX%ticket_attachment` + ADD `inline` tinyint(1) NOT NULL default 0 AFTER `ref_id`, + DROP `ref_type`; + +ALTER TABLE `%TABLE_PREFIX%ticket_thread` + ADD `user_id` int(11) unsigned not null default 0 AFTER `staff_id`; + +ALTER TABLE `%TABLE_PREFIX%ticket` + ADD `email_id` int(11) unsigned not null default 0 AFTER `team_id`, + CHANGE `ticketID` `number` varchar(20); + +ALTER TABLE `%TABLE_PREFIX%ticket_collaborator` + ADD`created` datetime NOT NULL AFTER `role`; + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `value` = '934954de8914d9bd2bb8343e805340ae' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/login.php b/login.php index 789938980ff451cdcc72920d2b9f88080dcc7a47..435eebd93a03e7f4f7d1b75509e3c1685b627a02 100644 --- a/login.php +++ b/login.php @@ -21,11 +21,11 @@ define('OSTCLIENTINC',TRUE); //make includes happy require_once(INCLUDE_DIR.'class.client.php'); require_once(INCLUDE_DIR.'class.ticket.php'); -if($_POST) { - - if(($user=Client::login(trim($_POST['lticket']), trim($_POST['lemail']), null, $errors))) { +if ($_POST) { + if (($user = UserAuthenticationBackend::process($_POST['lemail'], + $_POST['lticket'], $errors))) { //XXX: Ticket owner is assumed. - @header('Location: tickets.php?id='.$user->getTicketID()); + @header('Location: tickets.php?id='.$user->getTicketId()); require_once('tickets.php'); //Just in case of 'header already sent' error. exit; } elseif(!$errors['err']) { diff --git a/logout.php b/logout.php index 6c6482d9c8c6fe4e452c6a0494abdcba8ae01a93..4b9ea91b133fae4adf96823b8037428d1ed5acd0 100644 --- a/logout.php +++ b/logout.php @@ -16,12 +16,10 @@ require('client.inc.php'); //Check token: Make sure the user actually clicked on the link to logout. -if(!$_GET['auth'] || !$ost->validateLinkToken($_GET['auth'])) +if(!$thisclient || !$_GET['auth'] || !$ost->validateLinkToken($_GET['auth'])) @header('Location: index.php'); -$_SESSION['_client']=array(); -session_unset(); -session_destroy(); +$thisclient->logOut(); header('Location: index.php'); require('index.php'); ?> diff --git a/open.php b/open.php index 706c9ccd4a31e6febee613c9416acfc19988ef36..4cb7684a8167b7cd5a3c07f67d876b83c313cb4d 100644 --- a/open.php +++ b/open.php @@ -20,9 +20,8 @@ $errors=array(); if($_POST): $vars = $_POST; $vars['deptId']=$vars['emailId']=0; //Just Making sure we don't accept crap...only topicId is expected. - if($thisclient) { - $vars['name']=$thisclient->getName(); - $vars['email']=$thisclient->getEmail(); + if ($thisclient) { + $vars['uid']=$thisclient->getId(); } elseif($cfg->isCaptchaEnabled()) { if(!$_POST['captcha']) $errors['captcha']='Enter text shown on the image'; @@ -54,11 +53,9 @@ if($_POST): } //Logged in...simply view the newly created ticket. if($thisclient && $thisclient->isValid()) { - if(!$cfg->showRelatedTickets()) - $_SESSION['_client']['key']= $ticket->getExtId(); //Resetting login Key to the current ticket! session_write_close(); session_regenerate_id(); - @header('Location: tickets.php?id='.$ticket->getExtId()); + @header('Location: tickets.php?id='.$ticket->getId()); } }else{ $errors['err']=$errors['err']?$errors['err']:'Unable to create a ticket. Please correct errors below and try again!'; diff --git a/scp/login.php b/scp/login.php index 17424b6aeda068aa29b8bd233416cc3c604ad868..840716c65deda81e7b87026450ea3bfd6fd51d93 100644 --- a/scp/login.php +++ b/scp/login.php @@ -26,7 +26,7 @@ $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest if($_POST) { // Lookup support backends for this staff $username = trim($_POST['userid']); - if ($user = AuthenticationBackend::process($username, + if ($user = StaffAuthenticationBackend::process($username, $_POST['passwd'], $errors)) { @header("Location: $dest"); require_once('index.php'); //Just incase header is messed up. @@ -35,11 +35,10 @@ if($_POST) { $msg = $errors['err']?$errors['err']:'Invalid login'; } - // Consider single sign-on authentication backends -if (!$thisstaff || !($thisstaff->getId() || $thisstaff->isValid())) { - if (($user = AuthenticationBackend::singleSignOn($errors)) - && ($user instanceof Staff)) +else if (!$thisstaff || !($thisstaff->getId() || $thisstaff->isValid())) { + if (($user = StaffAuthenticationBackend::processSignOn($errors)) + && ($user instanceof StaffSession)) @header("Location: $dest"); } diff --git a/scp/logout.php b/scp/logout.php index 7076dcec4c0984192acab06de20c5a16cba91416..4cf519d35bc3a66597853f3a96f661190a245b33 100644 --- a/scp/logout.php +++ b/scp/logout.php @@ -15,16 +15,15 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ require('staff.inc.php'); + //Check token: Make sure the user actually clicked on the link to logout. if(!$_GET['auth'] || !$ost->validateLinkToken($_GET['auth'])) @header('Location: index.php'); -$ost->logDebug('Staff logout', - sprintf("%s logged out [%s]", - $thisstaff->getUserName(), $_SERVER['REMOTE_ADDR'])); //Debug. -$_SESSION['_staff']=array(); -session_unset(); -session_destroy(); +$thisstaff->logOut(); +//Clear upgrader session on logout. +$_SESSION['ost_upgrader'] = null; + @header('Location: login.php'); require('login.php'); ?> diff --git a/scp/pwreset.php b/scp/pwreset.php index 5b7a20fa86ab35ce1c9e701107a8a9e8be93dee0..6d749e2d5fe8c3b882bf2e0ffb1f012a046d3636 100644 --- a/scp/pwreset.php +++ b/scp/pwreset.php @@ -47,26 +47,15 @@ if($_POST) { case 'newpasswd': // TODO: Compare passwords $tpl = 'pwreset.login.php'; - $_config = new Config('pwreset'); - if (($staff = new StaffSession($_POST['userid'])) && - !$staff->getId()) - $msg = 'Invalid user-id given'; - elseif (!($id = $_config->get($_POST['token'])) - || $id != $staff->getId()) - $msg = 'Invalid reset token'; - elseif (!($ts = $_config->lastModified($_POST['token'])) - && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts)))) - $msg = 'Invalid reset token'; - elseif (!$staff->forcePasswdRest()) - $msg = 'Unable to reset password'; - else { + $errors = array(); + if ($staff = StaffAuthenticationBackend::processSignOn($errors)) { $info = array('page' => 'index.php'); - Signal::send('auth.pwreset.login', $staff, $info); - Staff::_do_login($staff, $_POST['userid']); - $_SESSION['_staff']['reset-token'] = $_POST['token']; header('Location: '.$info['page']); exit(); } + elseif (isset($errors['msg'])) { + $msg = $errors['msg']; + } break; } } diff --git a/scp/staff.inc.php b/scp/staff.inc.php index 73fe46d3804b449966f0dc58cbda68308a8145fc..e900ab0f89e7a9c0f2f51eab3efda01299746dd0 100644 --- a/scp/staff.inc.php +++ b/scp/staff.inc.php @@ -57,14 +57,13 @@ if(!function_exists('staffLoginPage')) { //Ajax interface can pre-declare the fu } } -$thisstaff = new StaffSession($_SESSION['_staff']['userID']); //Set staff object. +$thisstaff = StaffAuthenticationBackend::getUser(); //1) is the user Logged in for real && is staff. -if(!$thisstaff->getId() || !$thisstaff->isValid()){ +if (!$thisstaff || !$thisstaff->getId() || !$thisstaff->isValid()) { if (isset($_SESSION['_staff']['auth']['msg'])) { $msg = $_SESSION['_staff']['auth']['msg']; unset($_SESSION['_staff']['auth']['msg']); - } - elseif (isset($_SESSION['_staff']['userID']) && !$thisstaff->isValid()) + } elseif ($thisstaff && !$thisstaff->isValid()) $msg = 'Session timed out due to inactivity'; else $msg = 'Authentication Required'; diff --git a/scp/tickets.php b/scp/tickets.php index f28ac8e3e31a1677cceede9c867e49e204473b83..dd7dd0cd924aac00bda229407ee13318f120de33 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -222,7 +222,7 @@ if($_POST && !$errors): } elseif($ticket->isClosed()) { $errors['err'] = 'Ticket is already closed!'; } elseif($ticket->close()) { - $msg='Ticket #'.$ticket->getExtId().' status set to CLOSED'; + $msg='Ticket #'.$ticket->getNumber().' status set to CLOSED'; //Log internal note if($_POST['ticket_status_notes']) $note = $_POST['ticket_status_notes']; diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 060c3114885d0923651aa8569165d4cabe3790d3..1dbdd3b7794c5ebd17a4ae6d8494d2642fcbcde3 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -33,8 +33,8 @@ CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq` ( `answer` text NOT NULL, `keywords` tinytext, `notes` text, - `created` date NOT NULL, - `updated` date NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, PRIMARY KEY (`faq_id`), UNIQUE KEY `question` (`question`), KEY `category_id` (`category_id`), @@ -506,7 +506,7 @@ CREATE TABLE `%TABLE_PREFIX%team_member` ( DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket`; CREATE TABLE `%TABLE_PREFIX%ticket` ( `ticket_id` int(11) unsigned NOT NULL auto_increment, - `ticketID` int(11) unsigned NOT NULL default '0', + `number` varchar(20), `user_id` int(11) unsigned NOT NULL default '0', `user_email_id` int(11) unsigned NOT NULL default '0', `dept_id` int(10) unsigned NOT NULL default '0', @@ -514,6 +514,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket` ( `topic_id` int(10) unsigned NOT NULL default '0', `staff_id` int(10) unsigned NOT NULL default '0', `team_id` int(10) unsigned NOT NULL default '0', + `email_id` int(11) unsigned NOT NULL default '0', `ip_address` varchar(64) NOT NULL default '', `status` enum('open','closed') NOT NULL default 'open', `source` enum('Web','Email','Phone','API','Other') NOT NULL default 'Other', @@ -544,11 +545,10 @@ CREATE TABLE `%TABLE_PREFIX%ticket_attachment` ( `ticket_id` int(11) unsigned NOT NULL default '0', `file_id` int(10) unsigned NOT NULL default '0', `ref_id` int(11) unsigned NOT NULL default '0', - `ref_type` enum('M','R','N') NOT NULL default 'M', + `inline` tinyint(1) NOT NULL default '0', `created` datetime NOT NULL, PRIMARY KEY (`attach_id`), KEY `ticket_id` (`ticket_id`), - KEY `ref_type` (`ref_type`), KEY `ref_id` (`ref_id`), KEY `file_id` (`file_id`) ) DEFAULT CHARSET=utf8; @@ -567,10 +567,11 @@ CREATE TABLE `%TABLE_PREFIX%ticket_lock` ( DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_email_info`; CREATE TABLE `%TABLE_PREFIX%ticket_email_info` ( - `message_id` int(11) unsigned NOT NULL, + `thread_id` int(11) unsigned NOT NULL, `email_mid` varchar(255) NOT NULL, `headers` text, - KEY `message_id` (`email_mid`) + PRIMARY KEY (`thread_id`), + KEY `email_mid` (`email_mid`) ) DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_event`; @@ -608,6 +609,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket_thread` ( `pid` int(11) unsigned NOT NULL default '0', `ticket_id` int(11) unsigned NOT NULL default '0', `staff_id` int(11) unsigned NOT NULL default '0', + `user_id` int(11) unsigned not null default 0, `thread_type` enum('M','R','N') NOT NULL, `poster` varchar(128) NOT NULL default '', `source` varchar(32) NOT NULL default '', @@ -629,6 +631,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` ( `user_id` int(11) unsigned NOT NULL DEFAULT '0', -- M => (message) clients, N => (note) 3rd-Party, R => (reply) external authority `role` char(1) NOT NULL DEFAULT 'M', + `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `collab` (`ticket_id`,`user_id`) diff --git a/tickets.php b/tickets.php index b47ed0f1528335e9ee5eb871fcd8ca3c34d7b975..7c49a74d34611ff7f7f4b6fd0aeda80c31fcce0f 100644 --- a/tickets.php +++ b/tickets.php @@ -20,10 +20,10 @@ require_once(INCLUDE_DIR.'class.ticket.php'); require_once(INCLUDE_DIR.'class.json.php'); $ticket=null; if($_REQUEST['id']) { - if(!($ticket=Ticket::lookupByExtId($_REQUEST['id']))) { + if (!($ticket = Ticket::lookup($_REQUEST['id']))) { $errors['err']='Unknown or invalid ticket ID.'; - }elseif(!$ticket->checkClientAccess($thisclient)) { - $errors['err']='Unknown or invalid ticket ID.'; //Using generic message on purpose! + } elseif(!$ticket->checkUserAccess($thisclient)) { + $errors['err']='Unknown or invalid ticket.'; //Using generic message on purpose! $ticket=null; } } @@ -33,7 +33,7 @@ if($_POST && is_object($ticket) && $ticket->getId()): $errors=array(); switch(strtolower($_POST['a'])){ case 'reply': - if(!$ticket->checkClientAccess($thisclient)) //double check perm again! + if(!$ticket->checkUserAccess($thisclient)) //double check perm again! $errors['err']='Access Denied. Possibly invalid ticket ID'; if(!$_POST['message']) @@ -41,7 +41,10 @@ if($_POST && is_object($ticket) && $ticket->getId()): if(!$errors) { //Everything checked out...do the magic. - $vars = array('message'=>$_POST['message']); + $vars = array( + 'userId' => $thisclient->getId(), + 'poster' => (string) $thisclient->getName(), + 'message' => $_POST['message']); if($cfg->allowOnlineAttachments() && $_FILES['attachments']) $vars['files'] = AttachmentFile::format($_FILES['attachments'], true); if (isset($_POST['draft_id'])) @@ -51,7 +54,7 @@ if($_POST && is_object($ticket) && $ticket->getId()): $msg='Message Posted Successfully'; // Cleanup drafts for the ticket. If not closed, only clean // for this staff. Else clean all drafts for the ticket. - Draft::deleteForNamespace('ticket.client.' . $ticket->getExtId()); + Draft::deleteForNamespace('ticket.client.' . $ticket->getId()); } else { $errors['err']='Unable to post the message. Try again'; } @@ -66,7 +69,7 @@ if($_POST && is_object($ticket) && $ticket->getId()): $ticket->reload(); endif; $nav->setActiveNav('tickets'); -if($ticket && $ticket->checkClientAccess($thisclient)) { +if($ticket && $ticket->checkUserAccess($thisclient)) { $inc='view.inc.php'; } elseif($cfg->showRelatedTickets() && $thisclient->getNumTickets()) { $inc='tickets.inc.php'; diff --git a/view.php b/view.php index 10e5374fe71b8dcf3551c0ee922ac5b4800e961a..2398fff5628b9e0f380d38d53304352e650ecf66 100644 --- a/view.php +++ b/view.php @@ -3,7 +3,6 @@ view.php Ticket View. - TODO: Support different views based on auth_token - e.g for BCC'ed users vs. Ticket owner. Peter Rotich <peter@osticket.com> Copyright (c) 2006-2010 osTicket @@ -17,20 +16,16 @@ **********************************************************************/ require_once('client.inc.php'); -//If the user is NOT logged in - try auto-login (if params exists). -if(!$thisclient || !$thisclient->isValid()) { - // * On login Client::login will redirect the user to tickets.php view. - // * See TODO above for planned multi-view. - $user = null; - if($_GET['t'] && $_GET['e'] && $_GET['a']) - $user = Client::login($_GET['t'], $_GET['e'], $_GET['a'], $errors); - //XXX: For now we're assuming the user is the ticket owner - // (multi-view based on auth token will come later). - if($user && $user->getTicketID()==trim($_GET['t'])) - @header('Location: tickets.php?id='.$user->getTicketID()); +//If the user is NOT logged in - try auto-login (if params exists). +if (!$thisclient || !$thisclient->isValid()) { + // Try autologin the user + // Authenticated user can be of type ticket owner or collaborator + $errors = array(); + $user = UserAuthenticationBackend::processSignOn($errors); + if ($user && $user->getTicketId()) + @header('Location: tickets.php?id='.$user->getTicketId()); } - //Simply redirecting to tickets.php until multiview is implemented. require('tickets.php'); ?>