diff --git a/account.php b/account.php new file mode 100644 index 0000000000000000000000000000000000000000..66de95ecae8e048b0291b248b7c109fdf38daa9b --- /dev/null +++ b/account.php @@ -0,0 +1,117 @@ +<?php +/********************************************************************* + profile.php + + Manage client profile. This will allow a logged-in user to manage + his/her own public (non-internal) information + + Peter Rotich <peter@osticket.com> + Jared Hancock <jared@osticket.com> + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: + $Id: $ +**********************************************************************/ +require 'client.inc.php'; + +$inc = 'register.inc.php'; + +$errors = array(); + +if (!$cfg || !$cfg->isClientRegistrationEnabled()) { + Http::redirect('index.php'); +} + +elseif ($thisclient) { + // Guest registering for an account + if ($thisclient->isGuest()) { + foreach ($thisclient->getForms() as $f) + if ($f->get('type') == 'U') + $user_form = $f; + $user_form->getField('email')->configure('disabled', true); + } + // Existing client (with an account) updating profile + else { + $user = User::lookup($thisclient->getId()); + $content = Page::lookup(Page::getIdByType('registration-thanks')); + $inc = isset($_GET['confirmed']) + ? 'register.confirmed.inc.php' : 'profile.inc.php'; + } +} + +if ($user && $_POST) { + if ($acct = $thisclient->getAccount()) { + $acct->update($_POST, $errors); + } + if (!$errors && $user->updateInfo($_POST, $errors)) + Http::redirect('tickets.php'); +} + +elseif ($_POST) { + $user_form = UserForm::getUserForm()->getForm($_POST); + $user_form->getField('email')->configure('disabled', true); + if ($thisclient) + $user_form->getField('email')->value = $thisclient->getEmail(); + + if (!$user_form->isValid(function($f) { return !$f->get('internal'); })) + $errors['err'] = 'Incomplete client information'; + elseif (!$_POST['backend'] && !$_POST['passwd1']) + $errors['passwd1'] = 'New password required'; + elseif (!$_POST['backend'] && $_POST['passwd2'] != $_POST['passwd1']) + $errors['passwd1'] = 'Passwords do not match'; + + // XXX: The email will always be in use already if a guest is logged in + // and is registering for an account. Instead, + elseif (($addr = $user_form->getField('email')->getClean()) + && ClientAccount::lookupByUsername($addr)) { + $user_form->getField('email')->addError( + 'Email already registered. Would you like to <a href="login.php?e=' + .urlencode($addr).'" style="color:inherit"><strong>sign in</strong></a>?'); + $errors['err'] = 'Unable to register account. See messages below'; + } + // Users created from ClientCreateRequest + elseif (isset($_POST['backend']) && !($user = User::fromVars($user_form->getClean()))) + $errors['err'] = 'Unable to create local account. See messages below'; + // New users and users registering from a ticket access link + elseif (!$user && !($user = $thisclient ?: User::fromForm($user_form))) + $errors['err'] = 'Unable to register account. See messages below'; + else { + if (!($acct = ClientAccount::createForUser($user))) + $errors['err'] = 'Internal error. Unable to create new account'; + elseif (!$acct->update($_POST, $errors)) + $errors['err'] = 'Errors configuring your profile. See messages below'; + } + + if (!$errors) { + switch ($_POST['do']) { + case 'create': + $content = Page::lookup(Page::getIdByType('registration-confirm')); + $inc = 'register.confirm.inc.php'; + $acct->sendResetEmail('registration-client'); + break; + case 'import': + foreach (UserAuthenticationBackend::allRegistered() as $bk) { + if ($bk::$id == $_POST['backend']) { + $cl = new ClientSession(new EndUser($user)); + $acct->confirm(); + if ($user = $bk->login($cl, $bk)) + Http::redirect('tickets.php'); + break; + } + } + break; + } + } + + if ($errors && $user && $user != $thisclient) + $user->delete(); +} + +include(CLIENTINC_DIR.'header.inc.php'); +include(CLIENTINC_DIR.$inc); +include(CLIENTINC_DIR.'footer.inc.php'); + diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css index c4bd447e8ce924d267453b60b4b0faa764ce5a5f..3c558720ba239b9e2ef28648102d5f5aaa6737b1 100644 --- a/assets/default/css/theme.css +++ b/assets/default/css/theme.css @@ -219,6 +219,7 @@ h2 { border: 1px solid #a00; background: url('../images/icons/error.png') 10px 50% no-repeat #fff0f0; } +#msg_info { margin: 0; padding: 5px; margin-bottom: 10px; color: #3a87ad; border: 1px solid #bce8f1; background-color: #d9edf7; } .warning { background: #ffc; font-style: italic; @@ -626,21 +627,21 @@ label.required { text-align: left; } #clientLogin { - width: 400px; + display: block; margin-top: 20px; - padding: 10px 100px 10px 10px; + padding: 20px; border: 1px solid #ccc; - background: url('../images/lock.png?1319655200') 440px 50% no-repeat #f6f6f6; + border-radius: 5px; + box-shadow: inset 0 1px 2px rgba(0,0,0,0.3); + background: url('../images/lock.png?1319655200') 95% 50% no-repeat #f6f6f6; } #clientLogin p { clear: both; - text-align: center; } #clientLogin strong { font-size: 11px; color: #d00; display: block; - padding-left: 140px; } #clientLogin #email { width: 250px; @@ -870,3 +871,8 @@ a.refresh { color: #777; text-decoration: none; } +table.padded tr > td, +table.padded tr > th { + height: 20px; + padding-bottom: 5px; +} diff --git a/bootstrap.php b/bootstrap.php index 0a50c4a2635b693e575d87a3a13dda9f66e3a997..832fe96e50a6b89f9c7d952c5fa355295fcd8ca5 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -63,13 +63,14 @@ class Bootstrap { define('CONFIG_TABLE',$prefix.'config'); define('CANNED_TABLE',$prefix.'canned_response'); - define('PAGE_TABLE', $prefix.'page'); + define('PAGE_TABLE', $prefix.'content'); define('FILE_TABLE',$prefix.'file'); define('FILE_CHUNK_TABLE',$prefix.'file_chunk'); define('ATTACHMENT_TABLE',$prefix.'attachment'); define('USER_TABLE',$prefix.'user'); define('USER_EMAIL_TABLE',$prefix.'user_email'); + define('USER_ACCOUNT_TABLE',$prefix.'user_account'); define('STAFF_TABLE',$prefix.'staff'); define('TEAM_TABLE',$prefix.'team'); diff --git a/client.inc.php b/client.inc.php index ee9a9fcae443df2d4c0ade4636270d102b9955e8..ad4c7ee4e18c12841a46ac239fd0a7b90bd3e597 100644 --- a/client.inc.php +++ b/client.inc.php @@ -56,7 +56,7 @@ if($thisclient && $thisclient->getId() && $thisclient->isValid()){ /******* CSRF Protectin *************/ // Enforce CSRF protection for POSTS if ($_POST && !$ost->checkCSRFToken()) { - @header('Location: index.php'); + Http::redirect('index.php'); //just incase redirect fails die('Action denied (400)!'); } @@ -68,4 +68,13 @@ $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'" define('PAGE_LIMIT', DEFAULT_PAGE_LIMIT); $nav = new UserNav($thisclient, 'home'); + +$exempt = in_array(basename($_SERVER['SCRIPT_NAME']), array('logout.php', 'ajax.php', 'logs.php', 'upgrade.php')); + +if (!$exempt && $thisclient && ($acct = $thisclient->getAccount()) + && $acct->isPasswdResetForced()) { + $warn = 'Password change required to continue'; + require('profile.php'); //profile.php must request this file as require_once to avoid problems. + exit; +} ?> diff --git a/include/ajax.content.php b/include/ajax.content.php index acc238c34e828c414d3be133c836bb82d3f311b7..463248382493cbc58fd1bc302c03ac54c2d4efab 100644 --- a/include/ajax.content.php +++ b/include/ajax.content.php @@ -125,5 +125,44 @@ class ContentAjaxAPI extends AjaxController { break; } } + + function manageContent($id, $lang=false) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Login Required'); + + $content = Page::lookup($id, $lang); + include STAFFINC_DIR . 'templates/content-manage.tmpl.php'; + } + + function manageNamedContent($type, $lang=false) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Login Required'); + + $content = Page::lookup(Page::getIdByType($type, $lang)); + include STAFFINC_DIR . 'templates/content-manage.tmpl.php'; + } + + function updateContent($id) { + global $thisstaff; + + if (!$thisstaff) + Http::response(403, 'Login Required'); + elseif (!$_POST['name'] || !$_POST['body']) + Http::response(422, 'Please submit name and body'); + elseif (!($content = Page::lookup($id))) + Http::response(404, 'No such content'); + + $vars = array_merge($content->getHashtable(), $_POST); + if (!$content->save($id, $vars, $errors)) { + if ($errors['err']) + Http::response(422, $errors['err']); + else + Http::response(500, 'Unable to update content: '.print_r($errors, true)); + } + } } ?> diff --git a/include/class.auth.php b/include/class.auth.php index 660acf9ba2aba5946acd40fd2ec07bedea154237..dcde257c8ed86d3a83983fbfb4e7ff5f8bc71164 100644 --- a/include/class.auth.php +++ b/include/class.auth.php @@ -50,6 +50,44 @@ interface AuthDirectorySearch { function search($query); } +/** + * Class: ClientCreateRequest + * + * Simple container to represent a remote authentication success for a + * client which should be imported into the local database. The class will + * provide access to the backend that authenticated the user, the username + * that the user entered when logging in, and any other information about + * the user that the backend was able to lookup. Generally, this extra + * information would be the same information retrieved from calling the + * AuthDirectorySearch::lookup() method. + */ +class ClientCreateRequest { + + var $backend; + var $username; + var $info; + + function __construct($backend, $username, $info=array()) { + $this->backend = $backend; + $this->username = $username; + $this->info = $info; + } + + function getBackend() { + return $this->backend; + } + function setBackend($what) { + $this->backend = $what; + } + + function getUsername() { + return $this->username; + } + function getInfo() { + return $this->info; + } +} + /** * Authentication backend * @@ -128,17 +166,26 @@ abstract class AuthenticationBackend { // 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 - && ($bk->login($result, $bk))) - return $result; - elseif ($result instanceof AccessDenied) { + try { + $result = $bk->authenticate($username, $password); + if ($result instanceof AuthenticatedUser + && ($bk->login($result, $bk))) + return $result; + elseif ($result instanceof ClientCreateRequest + && $bk instanceof UserAuthenticationBackend) + return $result; + elseif ($result instanceof AccessDenied) { + break; + } + } + catch (AccessDenied $e) { + $result = $e; break; } } if (!$result) - $result = new AccessDenied('Access denied'); + $result = new AccessDenied('Access denied'); if ($result && $result instanceof AccessDenied) $errors['err'] = $result->reason; @@ -322,6 +369,9 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend { Signal::send('auth.login.succeeded', $staff); + if ($bk->supportsAuthentication()) + $staff->cancelResetTokens(); + return true; } @@ -398,8 +448,20 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend { } function getAllowedBackends($userid) { - // White listing backends for specific user not supported. - return array(); + $backends = array('authtoken'); + $sql = 'SELECT A1.backend FROM '.USER_ACCOUNT_TABLE + .' A1 INNER JOIN '.USER_EMAIL_TABLE.' A2 ON (A2.user_id = A1.user_id)' + .' WHERE backend IS NOT NULL ' + .' AND (A1.username='.db_input($userid) + .' OR A2.`address`='.db_input($userid).')'; + + if (!($res=db_query($sql, false))) + return $backends; + + while (list($bk) = db_fetch_row($res)) + $backends[] = $bk; + + return array_filter($backends); } function login($user, $bk) { @@ -410,6 +472,15 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend { || !($authkey = $bk->getAuthKey($user))) return false; + $acct = $user->getAccount(); + + if ($acct) { + if (!$acct->isConfirmed()) + throw new AccessDenied('Account confirmation required'); + elseif ($acct->isLocked()) + throw new AccessDenied('Account is administratively locked'); + } + //Tag the authkey. $authkey = $bk::$id.':'.$authkey; @@ -419,8 +490,6 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend { $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?? @@ -433,6 +502,9 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend { $user->getUserName(), $user->getId(), $_SERVER['REMOTE_ADDR']); $ost->logDebug('User login', $msg); + if ($bk->supportsAuthentication() && ($acct=$user->getAccount())) + $acct->cancelResetTokens(); + return true; } @@ -451,7 +523,7 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend { } protected function getAuthKey($user) { - return $user->getUsername(); + return $user->getId(); } static function getUser() { @@ -463,7 +535,6 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend { 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 @@ -474,14 +545,24 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend { return $user; } + + protected function validate($userid) { + if (!($user = User::lookup($userid))) + return false; + elseif (!$user->getAccount()) + return false; + + return new ClientSession(new EndUser($user)); + } } /** * This will be an exception in later versions of PHP */ -class AccessDenied { +class AccessDenied extends Exception { function __construct($reason) { $this->reason = $reason; + parent::__construct($reason); } } @@ -745,7 +826,6 @@ class AuthTokenAuthentication extends UserAuthenticationBackend { return $user; } - protected function getAuthKey($user) { if (!$this->supportsAuthentication() || !$user) @@ -790,6 +870,7 @@ class AuthTokenAuthentication extends UserAuthenticationBackend { if (!$user || strcmp($this->getAuthKey($user), $authkey)) return null; + $user->flagGuest(); return $user; } @@ -807,20 +888,18 @@ class AccessLinkAuthentication extends UserAuthenticationBackend { function authenticate($email, $number) { if (!($ticket = Ticket::lookupByNumber($number)) - || !($user=User::lookup(array('emails__address' => - $email)))) + || !($user=User::lookup(array('emails__address' => $email)))) return false; - //Ticket owner? + // Ticket owner? if ($ticket->getUserId() == $user->getId()) $user = $ticket->getOwner(); - //Collaborator? - elseif (!($user = Collaborator::lookup(array('userId' => - $user->getId(), 'ticketId' => - $ticket->getId())))) + // Collaborator? + elseif (!($user = Collaborator::lookup(array( + 'userId' => $user->getId(), + 'ticketId' => $ticket->getId())))) return false; //Bro, we don't know you! - return new ClientSession($user); } @@ -831,4 +910,88 @@ class AccessLinkAuthentication extends UserAuthenticationBackend { } UserAuthenticationBackend::register('AccessLinkAuthentication'); + +class osTicketClientAuthentication extends UserAuthenticationBackend { + static $name = "Local Client Authentication"; + static $id = "client"; + + function authenticate($username, $password) { + if (!($acct = ClientAccount::lookupByUsername($username))) + return; + + if (($client = new ClientSession(new EndUser($acct->getUser()))) + && !$client->getId()) + return false; + elseif (!$acct->checkPassword($password)) + return false; + else + return $client; + } +} +UserAuthenticationBackend::register('osTicketClientAuthentication'); + +class ClientPasswordResetTokenBackend extends UserAuthenticationBackend { + static $id = "pwreset.client"; + + function supportsAuthentication() { + return false; + } + + function signOn($errors=array()) { + global $ost; + + if (!isset($_POST['userid']) || !isset($_POST['token'])) + return false; + elseif (!($_config = new Config('pwreset'))) + return false; + elseif (!($acct = ClientAccount::lookupByUsername($_POST['userid'])) + || !$acct->getId() + || !($client = new ClientSession(new EndUser($acct->getUser())))) + $errors['msg'] = 'Invalid user-id given'; + elseif (!($id = $_config->get($_POST['token'])) + || $id != $client->getId()) + $errors['msg'] = 'Invalid reset token'; + elseif (!($ts = $_config->lastModified($_POST['token'])) + && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts)))) + $errors['msg'] = 'Invalid reset token'; + elseif (!$acct->forcePasswdReset()) + $errors['msg'] = 'Unable to reset password'; + else + return $client; + } + + function login($client, $bk) { + $_SESSION['_client']['reset-token'] = $_POST['token']; + Signal::send('auth.pwreset.login', $client); + return parent::login($client, $bk); + } +} +UserAuthenticationBackend::register('ClientPasswordResetTokenBackend'); + +class ClientAcctConfirmationTokenBackend extends UserAuthenticationBackend { + static $id = "confirm.client"; + + function supportsAuthentication() { + return false; + } + + function signOn($errors=array()) { + global $ost; + + if (!isset($_GET['token'])) + return false; + elseif (!($_config = new Config('pwreset'))) + return false; + elseif (!($id = $_config->get($_GET['token']))) + return false; + elseif (!($acct = ClientAccount::lookup(array('user_id'=>$id))) + || !$acct->getId() + || $id != $acct->getUserId() + || !($client = new ClientSession(new EndUser($acct->getUser())))) + return false; + else + return $client; + } +} +UserAuthenticationBackend::register('ClientAcctConfirmationTokenBackend'); ?> diff --git a/include/class.client.php b/include/class.client.php index c9f6fc394ec8fa53619d2d1b2867dfdccbb3db3d..7d1dfbcb7c93e6852070098c10774cee7f891dab 100644 --- a/include/class.client.php +++ b/include/class.client.php @@ -18,6 +18,7 @@ abstract class TicketUser { static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i'; protected $user; + protected $_guest = false; function __construct($user) { $this->user = $user; @@ -51,18 +52,23 @@ abstract class TicketUser { global $ost; if (!($ticket = $this->getTicket()) - || !($dept = $ticket->getDept()) - || !($email = $dept->getAutoRespEmail()) - || !($tpl = $dept->getTemplate()->getMsgTemplate('user.accesslink'))) + || !($email = $ost->getConfig()->getDefaultEmail()) + || !($content = Page::lookup(Page::getIdByType('access-link')))) return; $vars = array( 'url' => $ost->getConfig()->getBaseUrl(), 'ticket' => $this->getTicket(), + 'user' => $this, 'recipient' => $this); - $msg = $ost->replaceTemplateVariables($tpl->asArray(), $vars); - $email->send($this->getEmail(), $msg['subj'], $msg['body']); + $msg = $ost->replaceTemplateVariables(array( + 'subj' => $content->getName(), + 'body' => $content->getBody(), + ), $vars); + + $email->send($this->getEmail(), Format::striptags($msg['subj']), + $msg['body']); } protected function getAuthToken($algo=1) { @@ -133,6 +139,14 @@ abstract class TicketUser { && $this->user->getId() == $this->getTicket()->getOwnerId()); } + function flagGuest() { + $this->_guest = true; + } + + function isGuest() { + return $this->_guest; + } + abstract function getTicketId(); abstract function getTicket(); } @@ -163,6 +177,7 @@ class TicketOwner extends TicketUser { class EndUser extends AuthenticatedUser { protected $user; + protected $_account = false; function __construct($user) { $this->user = $user; @@ -187,7 +202,10 @@ class EndUser extends AuthenticatedUser { if ($this->user instanceof Collaborator) return $this->user->getUserId(); - return $this->user->getId(); + elseif ($this->user) + return $this->user->getId(); + + return false; } function getUserName() { @@ -225,6 +243,14 @@ class EndUser extends AuthenticatedUser { return ($stats=$this->getTicketStats())?$stats['closed']:0; } + function getAccount() { + if ($this->_account === false) + $this->_account = + ClientAccount::lookup(array('user_id'=>$this->getId())); + + return $this->_account; + } + private function getStats() { $sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed ' @@ -242,4 +268,242 @@ class EndUser extends AuthenticatedUser { return db_fetch_array(db_query($sql)); } } + +require_once INCLUDE_DIR.'class.orm.php'; +class ClientAccountModel extends VerySimpleModel { + static $meta = array( + 'table' => USER_ACCOUNT_TABLE, + 'pk' => array('id'), + 'joins' => array( + 'user' => array( + 'null' => false, + 'constraint' => array('user_id' => 'UserModel.id') + ), + ), + ); +} + +class ClientAccount extends ClientAccountModel { + var $_options = null; + var $timezone; + + const CONFIRMED = 0x0001; + const LOCKED = 0x0002; + const PASSWD_RESET_REQUIRED = 0x0004; + + function __onload() { + if ($this->get('timezone_id')) { + $this->timezone = Timezone::getOffsetById($this->ht['timezone_id']); + $_SESSION['TZ_OFFSET'] = $this->timezone; + $_SESSION['TZ_DST'] = $this->get('dst'); + } + } + + function getId() { + return $this->get('id'); + } + + function getUserId() { + return $this->get('user_id'); + } + + function checkPassword($password, $autoupdate=true) { + + /*bcrypt based password match*/ + if(Passwd::cmp($password, $this->get('passwd'))) + return true; + + //Fall back to MD5 + if(!$password || strcmp($this->get('passwd'), MD5($password))) + return false; + + //Password is a MD5 hash: rehash it (if enabled) otherwise force passwd change. + if ($autoupdate) + $this->set('passwd', Passwd::hash($password)); + + if (!$autoupdate || !$this->save()) + $this->forcePasswdReset(); + + return true; + } + + function hasCurrentPassword($password) { + return $this->checkPassword($password, false); + } + + function hasPassword() { + return (bool) $this->get('passwd'); + } + + function sendResetEmail($template='pwreset-client') { + global $ost, $cfg; + + $token = Misc::randCode(48); // 290-bits + + $email = $cfg->getDefaultEmail(); + $content = Page::lookup(Page::getIdByType($template)); + + if (!$email || !$content) + return new Error('Unable to retrieve password reset email template'); + + $vars = array( + 'url' => $ost->getConfig()->getBaseUrl(), + 'token' => $token, + 'user' => $this->getUser(), + 'recipient' => $this->getUser(), + 'link' => sprintf( + "%s/pwreset.php?token=%s", + $ost->getConfig()->getBaseUrl(), + $token), + ); + $vars['reset_link'] = &$vars['link']; + + $info = array('email' => $email, 'vars' => &$vars, 'log'=>true); + Signal::send('auth.pwreset.email', $this, $info); + + $msg = $ost->replaceTemplateVariables(array( + 'subj' => $content->getName(), + 'body' => $content->getBody(), + ), $vars); + + $_config = new Config('pwreset'); + $_config->set($vars['token'], $this->user->getId()); + + $email->send($this->user->default_email->get('address'), + Format::striptags($msg['subj']), $msg['body']); + } + + function confirm() { + $this->_setStatus(self::CONFIRMED); + return $this->save(); + } + + function isConfirmed() { + return $this->_getStatus(self::CONFIRMED); + } + + function lock() { + $this->_setStatus(self::LOCKED); + $this->save(); + } + + function isLocked() { + return $this->_getStatus(self::LOCKED); + } + + function forcePasswdReset() { + $this->_setStatus(self::PASSWD_RESET_REQUIRED); + return $this->save(); + } + + function isPasswdResetForced() { + return $this->_getStatus(self::PASSWD_RESET_REQUIRED); + } + + function _getStatus($flag) { + return 0 !== ($this->get('status') & $flag); + } + + function _clearStatus($flag) { + return $this->set('status', $this->get('status') & ~$flag); + } + + function _setStatus($flag) { + return $this->set('status', $this->get('status') | $flag); + } + + function cancelResetTokens() { + // TODO: Drop password-reset tokens from the config table for + // this user id + $sql = 'DELETE FROM '.CONFIG_TABLE.' WHERE `namespace`="pwreset" + AND `key`='.db_input($this->getUserId()); + if (!db_query($sql, false)) + return false; + + unset($_SESSION['_client']['reset-token']); + } + + function getInfo() { + $base = $this->ht; + $base['tz_offset'] = $this->timezone; + return $base; + } + + function getUser() { + $user = User::lookup($this->get('user_id')); + $user->set('account', $this); + return $user; + } + + function update($vars, &$errors) { + $rtoken = $_SESSION['_client']['reset-token']; + if ($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd'] || $rtoken) { + + if (!$vars['passwd1']) + $errors['passwd1']='New password required'; + elseif ($vars['passwd1'] && strlen($vars['passwd1'])<6) + $errors['passwd1']='Must be at least 6 characters'; + elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) + $errors['passwd2']='Password(s) do not match'; + + if ($rtoken) { + $_config = new Config('pwreset'); + if ($_config->get($rtoken) != $this->getUserId()) + $errors['err'] = + 'Invalid reset token. Logout and try again'; + elseif (!($ts = $_config->lastModified($rtoken)) + && ($cfg->getPwResetWindow() < (time() - strtotime($ts)))) + $errors['err'] = + 'Invalid reset token. Logout and try again'; + } + elseif ($this->get('passwd')) { + if (!$vars['cpasswd']) + $errors['cpasswd']='Current password required'; + elseif (!$this->hasCurrentPassword($vars['cpasswd'])) + $errors['cpasswd']='Invalid current password!'; + elseif (!strcasecmp($vars['passwd1'], $vars['cpasswd'])) + $errors['passwd1']='New password MUST be different from the current password!'; + } + } + + if (!$vars['timezone_id']) + $errors['timezone_id']='Time zone required'; + + if ($errors) return false; + + $this->set('timezone_id', $vars['timezone_id']); + $this->set('dst', isset($vars['dst']) ? 1 : 0); + + if ($vars['backend']) { + $this->set('backend', $vars['backend']); + if ($vars['username']) + $this->set('username', $vars['username']); + } + + if ($vars['passwd1']) { + $this->set('passwd', Passwd::hash($vars['passwd1'])); + $info = array('password' => $vars['passwd1']); + Signal::send('auth.pwchange', $this, $info); + $this->cancelResetTokens(); + $this->_clearStatus(self::PASSWD_RESET_REQUIRED); + } + + return $this->save(); + } + + static function createForUser($user) { + return static::create(array('user_id'=>$user->getId())); + } + + static function lookupByUsername($username) { + if (strpos($username, '@') !== false) + $user = self::lookup(array('user__emails__address'=>$username)); + else + $user = self::lookup(array('username'=>$username)); + + return $user; + } +} +ClientAccount::_inspect(); + ?> diff --git a/include/class.config.php b/include/class.config.php index 68704cd7b65523acb6f5610746b4e9741847c962..03fe8891c0acfbabe2ea41c750f926a606225277 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -153,6 +153,9 @@ class OsticketConfig extends Config { 'allow_client_updates' => false, 'message_autoresponder_collabs' => true, 'add_email_collabs' => true, + 'clients_only' => false, + 'client_registration' => 'closed', + 'accept_unregistered_email' => true, ); function OsticketConfig($section=null) { @@ -239,6 +242,10 @@ class OsticketConfig extends Config { return $this->get('db_tz_offset'); } + function getDefaultTimezoneId() { + return $this->get('default_timezone_id'); + } + /* Date & Time Formats */ function observeDaylightSaving() { return ($this->get('enable_daylight_saving')); @@ -290,10 +297,6 @@ class OsticketConfig extends Config { return $this->get('passwd_reset_period'); } - function showRelatedTickets() { - return $this->get('show_related_tickets'); - } - function isHtmlThreadEnabled() { return $this->get('enable_html_thread'); } @@ -517,6 +520,19 @@ class OsticketConfig extends Config { return $this->get('pw_reset_window') * 60; } + function isClientLoginRequired() { + return $this->get('clients_only'); + } + + function isClientRegistrationEnabled() { + return in_array($this->getClientRegistrationMode(), + array('public', 'auto')); + } + + function getClientRegistrationMode() { + return $this->get('client_registration'); + } + function isCaptchaEnabled() { return (extension_loaded('gd') && function_exists('gd_info') && $this->get('enable_captcha')); } @@ -533,6 +549,10 @@ class OsticketConfig extends Config { return ($this->get('use_email_priority')); } + function acceptUnregisteredEmail() { + return $this->get('accept_unregistered_email'); + } + function addCollabsViaEmail() { return ($this->get('add_email_collabs')); } @@ -767,7 +787,10 @@ class OsticketConfig extends Config { case 'pages': return $this->updatePagesSettings($vars, $errors); break; - case 'autoresp': + case 'access': + return $this->updateAccessSettings($vars, $errors); + break; + case 'autoresp': return $this->updateAutoresponderSettings($vars, $errors); break; case 'alerts': @@ -789,17 +812,12 @@ class OsticketConfig extends Config { $f['helpdesk_url']=array('type'=>'string', 'required'=>1, 'error'=>'Helpdesk URl required'); $f['helpdesk_title']=array('type'=>'string', 'required'=>1, 'error'=>'Helpdesk title required'); $f['default_dept_id']=array('type'=>'int', 'required'=>1, 'error'=>'Default Dept. required'); - $f['staff_session_timeout']=array('type'=>'int', 'required'=>1, 'error'=>'Enter idle time in minutes'); - $f['client_session_timeout']=array('type'=>'int', 'required'=>1, 'error'=>'Enter idle time in minutes'); //Date & Time Options $f['time_format']=array('type'=>'string', 'required'=>1, 'error'=>'Time format required'); $f['date_format']=array('type'=>'string', 'required'=>1, 'error'=>'Date format required'); $f['datetime_format']=array('type'=>'string', 'required'=>1, 'error'=>'Datetime format required'); $f['daydatetime_format']=array('type'=>'string', 'required'=>1, 'error'=>'Day, Datetime format required'); $f['default_timezone_id']=array('type'=>'int', 'required'=>1, 'error'=>'Default Timezone required'); - $f['pw_reset_window']=array('type'=>'int', 'required'=>1, 'min'=>1, - 'error'=>'Valid password reset window required'); - if(!Validator::process($f, $vars, $errors) || $errors) return false; @@ -813,6 +831,27 @@ class OsticketConfig extends Config { 'log_level'=>$vars['log_level'], 'log_graceperiod'=>$vars['log_graceperiod'], 'name_format'=>$vars['name_format'], + 'time_format'=>$vars['time_format'], + 'date_format'=>$vars['date_format'], + 'datetime_format'=>$vars['datetime_format'], + 'daydatetime_format'=>$vars['daydatetime_format'], + 'default_timezone_id'=>$vars['default_timezone_id'], + 'enable_daylight_saving'=>isset($vars['enable_daylight_saving'])?1:0, + )); + } + + function updateAccessSettings($vars, &$errors) { + $f=array(); + $f['staff_session_timeout']=array('type'=>'int', 'required'=>1, 'error'=>'Enter idle time in minutes'); + $f['client_session_timeout']=array('type'=>'int', 'required'=>1, 'error'=>'Enter idle time in minutes'); + $f['pw_reset_window']=array('type'=>'int', 'required'=>1, 'min'=>1, + 'error'=>'Valid password reset window required'); + + + if(!Validator::process($f, $vars, $errors) || $errors) + return false; + + return $this->updateAll(array( 'passwd_reset_period'=>$vars['passwd_reset_period'], 'staff_max_logins'=>$vars['staff_max_logins'], 'staff_login_timeout'=>$vars['staff_login_timeout'], @@ -823,18 +862,12 @@ class OsticketConfig extends Config { 'client_session_timeout'=>$vars['client_session_timeout'], 'allow_pw_reset'=>isset($vars['allow_pw_reset'])?1:0, 'pw_reset_window'=>$vars['pw_reset_window'], - 'time_format'=>$vars['time_format'], - 'date_format'=>$vars['date_format'], - 'datetime_format'=>$vars['datetime_format'], - 'daydatetime_format'=>$vars['daydatetime_format'], - 'default_timezone_id'=>$vars['default_timezone_id'], - 'enable_daylight_saving'=>isset($vars['enable_daylight_saving'])?1:0, + 'clients_only'=>isset($vars['clients_only'])?1:0, + 'client_registration'=>$vars['client_registration'], )); } function updateTicketsSettings($vars, &$errors) { - - $f=array(); $f['default_sla_id']=array('type'=>'int', 'required'=>1, 'error'=>'Selection required'); $f['default_priority_id']=array('type'=>'int', 'required'=>1, 'error'=>'Selection required'); @@ -932,6 +965,7 @@ class OsticketConfig extends Config { 'enable_mail_polling'=>isset($vars['enable_mail_polling'])?1:0, 'strip_quoted_reply'=>isset($vars['strip_quoted_reply'])?1:0, 'use_email_priority'=>isset($vars['use_email_priority'])?1:0, + 'accept_unregistered_email'=>isset($vars['accept_unregistered_email'])?1:0, 'add_email_collabs'=>isset($vars['add_email_collabs'])?1:0, 'reply_separator'=>$vars['reply_separator'], )); diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 0045b6a430e3d864d4b8792df143a187214451fe..01682d00cf20c9f71976f7821a73076a98881f60 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -73,9 +73,9 @@ class DynamicForm extends VerySimpleModel { } function getField($name) { - foreach ($this->getDynamicFields() as $f) + foreach ($this->getFields() as $f) if (!strcasecmp($f->get('name'), $name)) - return $f->getImpl(); + return $f; } function hasField($name) { @@ -494,7 +494,7 @@ class DynamicFormEntry extends VerySimpleModel { function getForm() { if (!isset($this->_form)) { $this->_form = DynamicForm::lookup($this->get('form_id')); - if ($this->id) + if (isset($this->id)) $this->_form->data($this); } return $this->_form; diff --git a/include/class.forms.php b/include/class.forms.php index 2bbb04a66559d7264816968e2ce023b9b271e083..42f4651a1debd6fd0234244ef6f9f1f6aec10ea7 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -478,6 +478,11 @@ class FormField { return $this->_cform; } + function configure($prop, $value) { + $this->getConfiguration(); + $this->_config[$prop] = $value; + } + function getWidget() { if (!static::$widget) throw new Exception('Widget not defined for this field'); @@ -932,7 +937,7 @@ class Widget { $this->value = $this->getValue(); if (!isset($this->value) && is_object($this->field->getAnswer())) $this->value = $this->field->getAnswer()->getValue(); - if (!isset($this->value) && $this->field->value) + if (!isset($this->value) && isset($this->field->value)) $this->value = $this->field->value; } @@ -960,12 +965,14 @@ class TextboxWidget extends Widget { $classes = 'class="'.$config['classes'].'"'; if (isset($config['autocomplete'])) $autocomplete = 'autocomplete="'.($config['autocomplete']?'on':'off').'"'; + if (isset($config['disabled'])) + $disabled = 'disabled="disabled"'; ?> <span style="display:inline-block"> <input type="<?php echo static::$input_type; ?>" id="<?php echo $this->name; ?>" - <?php echo $size . " " . $maxlength; ?> - <?php echo $classes.' '.$autocomplete + <?php echo implode(' ', array_filter(array( + $size, $maxlength, $classes, $autocomplete, $disabled))) .' placeholder="'.$config['placeholder'].'"'; ?> name="<?php echo $this->name; ?>" value="<?php echo Format::htmlchars($this->value); ?>"/> @@ -1090,6 +1097,8 @@ class CheckboxWidget extends Widget { function render() { $config = $this->field->getConfiguration(); + if (!isset($this->value)) + $this->value = $this->field->get('default'); ?> <input type="checkbox" name="<?php echo $this->name; ?>[]" <?php if ($this->value) echo 'checked="checked"'; ?> value="<?php diff --git a/include/class.i18n.php b/include/class.i18n.php index 7335495d5bd75ecdc9547a27d5af35f8f5de5076..b7e7289430bf993618579a5b2f5726d7c682e100 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -24,6 +24,11 @@ class Internationalization { var $langs = array('en_US'); function Internationalization($language=false) { + global $cfg; + + if ($cfg && ($lang = $cfg->getSystemLanguage())) + array_unshift($this->langs, $language); + if ($language) array_unshift($this->langs, $language); } @@ -85,9 +90,13 @@ class Internationalization { } } - // Pages + // Pages and content $_config = new OsticketConfig(); - foreach (array('landing','thank-you','offline') as $type) { + foreach (array('landing','thank-you','offline', + 'registration-staff', 'pwreset-staff', 'banner-staff', + 'registration-client', 'pwreset-client', 'banner-client', + 'registration-confirm', 'registration-thanks', + 'access-link') as $type) { $tpl = $this->getTemplate("templates/page/{$type}.yaml"); if (!($page = $tpl->getData())) continue; @@ -97,12 +106,16 @@ class Internationalization { .', lang='.db_input($tpl->getLang()) .', notes='.db_input($page['notes']) .', created=NOW(), updated=NOW(), isactive=1'; - if (db_query($sql) && ($id = db_insert_id())) + if (db_query($sql) && ($id = db_insert_id()) + && in_array($type, array('landing', 'thank-you', 'offline'))) $_config->set("{$type}_page_id", $id); } // Default Language $_config->set('system_language', $this->langs[0]); + // content_id defaults to the `id` field value + db_query('UPDATE '.PAGE_TABLE.' SET content_id=id'); + // Canned response examples if (($tpl = $this->getTemplate('templates/premade.yaml')) && ($canned = $tpl->getData())) { diff --git a/include/class.nav.php b/include/class.nav.php index 1648332948ec1a32e5ac9a12aba75827d3087a15..6e0179890938e6f7a933f7b089b2368dd8e0afa2 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -200,6 +200,7 @@ class AdminNav extends StaffNav{ $subnav[]=array('desc'=>'System','href'=>'settings.php?t=system','iconclass'=>'preferences'); $subnav[]=array('desc'=>'Tickets','href'=>'settings.php?t=tickets','iconclass'=>'ticket-settings'); $subnav[]=array('desc'=>'Emails','href'=>'settings.php?t=emails','iconclass'=>'email-settings'); + $subnav[]=array('desc'=>'Access','href'=>'settings.php?t=access','iconclass'=>'users'); $subnav[]=array('desc'=>'Knowledgebase','href'=>'settings.php?t=kb','iconclass'=>'kb-settings'); $subnav[]=array('desc'=>'Autoresponder','href'=>'settings.php?t=autoresp','iconclass'=>'email-autoresponders'); $subnav[]=array('desc'=>'Alerts & Notices','href'=>'settings.php?t=alerts','iconclass'=>'alert-settings'); @@ -279,9 +280,15 @@ class UserNav { if($cfg && $cfg->isKnowledgebaseEnabled()) $navs['kb']=array('desc'=>'Knowledgebase','href'=>'kb/index.php','title'=>''); - $navs['new']=array('desc'=>'Open New Ticket','href'=>'open.php','title'=>''); + // Show the "Open New Ticket" link unless BOTH client + // registration is disabled and client login is required for new + // tickets. In such a case, creating a ticket would not be + // possible for web clients. + if ($cfg->getClientRegistrationMode() != 'disabled' + || !$cfg->isClientLoginRequired()) + $navs['new']=array('desc'=>'Open New Ticket','href'=>'open.php','title'=>''); if($user && $user->isValid()) { - if($cfg && $cfg->showRelatedTickets()) { + if(!$user->isGuest()) { $navs['tickets']=array('desc'=>sprintf('Tickets (%d)',$user->getNumTickets()), 'href'=>'tickets.php', 'title'=>'Show all tickets'); diff --git a/include/class.orm.php b/include/class.orm.php index b9bda6a1176ac4beea1aee9301360e50328a315b..cdf25599501b82e474893d2ccca18823804d38f1 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -16,6 +16,8 @@ vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ +class OrmException extends Exception {} + class VerySimpleModel { static $meta = array( 'table' => false, @@ -46,6 +48,13 @@ class VerySimpleModel { $v = $this->ht[$field] = $class::lookup($this->ht[$j['local']]); return $v; } + throw new OrmException(sprintf('%s: %s: Field not defined', + get_class($this), $field)); + } + + function __isset($field) { + return array_key_exists($field, $this->ht) + || isset(static::$meta['joins'][$field]); } function set($field, $value) { @@ -86,7 +95,8 @@ class VerySimpleModel { // Construct related lists if (isset(static::$meta['joins'])) { foreach (static::$meta['joins'] as $name => $j) { - if (isset($j['list']) && $j['list']) { + if (isset($this->{$j['local']}) + && isset($j['list']) && $j['list']) { $fkey = $j['fkey']; $this->{$name} = new InstrumentedList( // Send Model, Foriegn-Field, Local-Id @@ -97,6 +107,8 @@ class VerySimpleModel { } } + function __onload() {} + static function _inspect() { if (!static::$meta['table']) throw new OrmConfigurationError( @@ -113,7 +125,8 @@ class VerySimpleModel { $constraint[$field] = "$model.$foreign"; } $j['constraint'] = $constraint; - $j['list'] = true; + if (!isset($j['list'])) + $j['list'] = true; } // XXX: Make this better (ie. composite keys) $keys = array_keys($j['constraint']); @@ -365,7 +378,9 @@ class ModelInstanceIterator implements Iterator, ArrayAccess { function buildModel($row) { // TODO: Traverse to foreign keys - return new $this->model($row); # nolint + $model = new $this->model($row); # nolint + $model->__onload(); + return $model; } function fillTo($index) { diff --git a/include/class.page.php b/include/class.page.php index f0ada9a6c077dfa1099a55d64867a82cb294c501..553e342a7e2f657aa6d2df307479b65f7dffa764 100644 --- a/include/class.page.php +++ b/include/class.page.php @@ -19,13 +19,13 @@ class Page { var $ht; var $attachments; - function Page($id) { + function Page($id, $lang=false) { $this->id=0; $this->ht = array(); - $this->load($id); + $this->load($id, $lang); } - function load($id=0) { + function load($id=0, $lang=false) { if(!$id && !($id=$this->getId())) return false; @@ -33,7 +33,8 @@ class Page { $sql='SELECT page.*, count(topic.page_id) as topics ' .' FROM '.PAGE_TABLE.' page ' .' LEFT JOIN '.TOPIC_TABLE. ' topic ON(topic.page_id=page.id) ' - .' WHERE page.id='.db_input($id) + . ' WHERE page.content_id='.db_input($id) + . ($lang ? ' AND lang='.db_input($lang) : '') .' GROUP By page.id'; if (!($res=db_query($sql)) || !db_num_rows($res)) @@ -195,10 +196,25 @@ class Page { return self::getActivePages(array('type' => 'thank-you')); } - function getIdByName($name) { + function getIdByName($name, $lang=false) { $id = 0; $sql = ' SELECT id FROM '.PAGE_TABLE.' WHERE name='.db_input($name); + if ($lang) + $sql .= ' AND lang='.db_input($lang); + + if(($res=db_query($sql)) && db_num_rows($res)) + list($id) = db_fetch_row($res); + + return $id; + } + + function getIdByType($type, $lang=false) { + $id = 0; + $sql = ' SELECT id FROM '.PAGE_TABLE.' WHERE `type`='.db_input($type); + if ($lang) + $sql .= ' AND lang='.db_input($lang); + if(($res=db_query($sql)) && db_num_rows($res)) list($id) = db_fetch_row($res); @@ -224,8 +240,6 @@ class Page { if(!$vars['type']) $errors['type'] = 'Type required'; - elseif(!in_array($vars['type'], array('landing', 'offline', 'thank-you', 'other'))) - $errors['type'] = 'Invalid selection'; if(!$vars['name']) $errors['name'] = 'Name required'; @@ -254,10 +268,13 @@ class Page { } else { $sql='INSERT INTO '.PAGE_TABLE.' SET '.$sql.', created=NOW()'; - if(db_query($sql) && ($id=db_insert_id())) - return $id; + if (!db_query($sql) || !($id=db_insert_id())) { + $errors['err']='Unable to create page. Internal error'; + return false; + } - $errors['err']='Unable to create page. Internal error'; + // TODO: Update `content_id` + return $id; } return false; diff --git a/include/class.plugin.php b/include/class.plugin.php index 6a8ac830b8d372b3dd9974af60ca555635e77898..f571ad75d5c135c1dfcc4258ab93394ac3af8d72 100644 --- a/include/class.plugin.php +++ b/include/class.plugin.php @@ -9,6 +9,8 @@ class PluginConfig extends Config { // Use parent constructor to place configurable information into the // central config table in a namespace of "plugin.<id>" parent::Config("plugin.$name"); + foreach ($this->getOptions() as $name => $field) + $this->config[$name]['value'] = $field->to_php($this->get($name)); } /* abstract */ @@ -56,8 +58,14 @@ class PluginConfig extends Config { $commit = $this->pre_save($config, $errors); } $errors += $f->errors(); - if ($commit && count($errors) === 0) - return $this->updateAll($config); + if ($commit && count($errors) === 0) { + $dbready = array(); + foreach ($config as $name => $val) { + $field = $f->getField($name); + $dbready[$name] = $field->to_database($val); + } + return $this->updateAll($dbready); + } return false; } diff --git a/include/class.staff.php b/include/class.staff.php index 819c68d2814dc4c834255d891dd765d361ddbffd..9ab47f6b4e4bbdfb14f939535ebbb8cf7d852420 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -721,8 +721,11 @@ class Staff extends AuthenticatedUser { } function create($vars, &$errors) { - if(($id=self::save(0, $vars, $errors)) && $vars['teams'] && ($staff=Staff::lookup($id))) { - $staff->updateTeams($vars['teams']); + if(($id=self::save(0, $vars, $errors)) && ($staff=Staff::lookup($id))) { + if ($vars['teams']) + $staff->updateTeams($vars['teams']); + if ($vars['welcome_email']) + $staff->sendResetEmail('registration-staff'); Signal::send('model.created', $staff); } @@ -738,27 +741,28 @@ class Staff extends AuthenticatedUser { unset($_SESSION['_staff']['reset-token']); } - function sendResetEmail() { + function sendResetEmail($template='pwreset-staff') { global $ost, $cfg; - if(!($tpl = $this->getDept()->getTemplate())) - $tpl= $ost->getConfig()->getDefaultTemplate(); - + $content = Page::lookup(Page::getIdByType($template)); $token = Misc::randCode(48); // 290-bits - if (!($template = $tpl->getMsgTemplate('staff.pwreset'))) + + if (!$content) return new Error('Unable to retrieve password reset email template'); $vars = array( 'url' => $ost->getConfig()->getBaseUrl(), 'token' => $token, 'staff' => $this, + 'recipient' => $this, 'reset_link' => sprintf( "%s/scp/pwreset.php?token=%s", $ost->getConfig()->getBaseUrl(), $token), ); + $vars['link'] = &$vars['reset_link']; - if(!($email=$cfg->getAlertEmail())) + if (!($email = $cfg->getAlertEmail())) $email = $cfg->getDefaultEmail(); $info = array('email' => $email, 'vars' => &$vars, 'log'=>true); @@ -778,12 +782,16 @@ class Staff extends AuthenticatedUser { $email->getEmail() ), false); - $msg = $ost->replaceTemplateVariables($template->asArray(), $vars); + $msg = $ost->replaceTemplateVariables(array( + 'subj' => $content->getName(), + 'body' => $content->getBody(), + ), $vars); $_config = new Config('pwreset'); $_config->set($vars['token'], $this->getId()); - $email->send($this->getEmail(), $msg['subj'], $msg['body']); + $email->send($this->getEmail(), Format::stripTags($msg['subj']), + $msg['body']); } function save($id, $vars, &$errors) { @@ -823,7 +831,7 @@ class Staff extends AuthenticatedUser { if($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) { $errors['passwd2']='Password(s) do not match'; } - elseif ($vars['backend'] != 'local') { + elseif ($vars['backend'] != 'local' || $vars['welcome_email']) { // Password can be omitted } elseif(!$vars['passwd1'] && !$id) { diff --git a/include/class.template.php b/include/class.template.php index 88f3e5c5701383cca67b68f55764d0f30eb92262..4394c0fc1af09a7fbeeb2d3a06c7de1f550afea2 100644 --- a/include/class.template.php +++ b/include/class.template.php @@ -79,14 +79,6 @@ class EmailTemplateGroup { 'group'=>'ticket.staff', 'name'=>'Overdue Ticket Alert', 'desc'=>'Alert sent to staff on stale or overdue tickets.'), - 'staff.pwreset' => array( - 'group'=>'sys', - 'name' => 'Staff Password Reset', - 'desc' => 'Notice sent to staff with the password reset link.'), - 'user.accesslink' => array( - 'group'=>'sys', - 'name' => 'User Access Link Recovery', - 'desc' => 'Notice sent to user on request with ticket access link.'), ); function EmailTemplateGroup($id){ diff --git a/include/class.ticket.php b/include/class.ticket.php index bd05210c3611429f2170cf4636edabda418bd7c3..a69109204c85f16a88ab60fa01f2c0b87b1a2d8f 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -2157,6 +2157,14 @@ class Ticket { }; }; + $reject_ticket = function($message) use (&$errors) { + $errors = array( + 'errno' => 403, + 'err' => 'This help desk is for use by authorized users only'); + $ost->logWarning('Ticket Denied', $message); + return 0; + }; + // Create and verify the dynamic form entry for the new ticket $form = TicketForm::getNewInstance(); // If submitting via email, ensure we have a subject and such @@ -2193,12 +2201,7 @@ class Ticket { //Make sure the email address is not banned if (TicketFilter::isBanned($vars['email'])) { - $errors = array( - 'errno' => 403, - 'err' => 'This help desk is for use by authorized - users only'); - $ost->logWarning('Ticket denied', 'Banned email - '.$vars['email']); - return 0; + return $reject_ticket('Banned email - '.$vars['email']); } //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL) @@ -2220,17 +2223,11 @@ class Ticket { //Init ticket filters... $ticket_filter = new TicketFilter($origin, $vars); // Make sure email contents should not be rejected - if($ticket_filter + if ($ticket_filter && ($filter=$ticket_filter->shouldReject())) { - $errors = array( - 'errno' => 403, - 'err' => "This help desk is for use by authorized users - only"); - $ost->logWarning('Ticket denied', - sprintf('Ticket rejected ( %s) by filter "%s"', - $vars['email'], $filter->getName())); - - return 0; + return $reject_ticket( + sprintf('Ticket rejected ( %s) by filter "%s"', + $vars['email'], $filter->getName())); } $id=0; @@ -2281,10 +2278,18 @@ class Ticket { || !($user=User::fromVars($user_form->getClean()))) $errors['user'] = 'Incomplete client information'; } + + // Reject emails if not from registered clients (if configured) + if (!$cfg->acceptUnregisteredEmail() && !$user->getAccount()) { + return $reject_ticket( + sprintf('Ticket rejected (%s) (unregistered client)', + $vars['email'])); + } } - //Any error above is fatal. - if($errors) return 0; + // Any error above is fatal. + if ($errors) + return 0; # Some things will need to be unpacked back into the scope of this # function diff --git a/include/class.user.php b/include/class.user.php index 0a945f68b57c1b37ed93c59fa40fbd7b5ba45969..d23d1e9f1b857bd343f74c42bc7348580703df56 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -36,6 +36,10 @@ class UserModel extends VerySimpleModel { 'emails' => array( 'reverse' => 'UserEmailModel.user', ), + 'account' => array( + 'list' => false, + 'reverse' => 'ClientAccount.user', + ), 'default_email' => array( 'null' => true, 'constraint' => array('default_email_id' => 'UserEmailModel.id') @@ -143,6 +147,10 @@ class User extends UserModel { return $this->created; } + function getAccount() { + return $this->account; + } + function to_json() { $info = array( @@ -284,6 +292,10 @@ class User extends UserModel { $this->set('updated', new SqlFunction('NOW')); return parent::save($refetch); } + + function delete() { + return parent::delete() && $this->default_email->delete(); + } } User::_inspect(); diff --git a/include/class.usersession.php b/include/class.usersession.php index 31c4ce058d50939e10dabda80fe736ab3095c83f..9e7fd277baf58d59b39ade1255bd29a1308493a4 100644 --- a/include/class.usersession.php +++ b/include/class.usersession.php @@ -120,7 +120,7 @@ class ClientSession extends EndUser { parent::__construct($user); $this->token = &$_SESSION[':token']['client']; // XXX: Change the key to user-id - $this->session= new UserSession($user->getUserName()); + $this->session= new UserSession($user->getId()); } function isValid(){ diff --git a/include/client/accesslink.inc.php b/include/client/accesslink.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..0f1ef8c319afb1b388bc0710dd6e4fc30809ceae --- /dev/null +++ b/include/client/accesslink.inc.php @@ -0,0 +1,43 @@ +<?php +if(!defined('OSTCLIENTINC')) die('Access Denied'); + +$email=Format::input($_POST['lemail']?$_POST['lemail']:$_GET['e']); +$ticketid=Format::input($_POST['lticket']?$_POST['lticket']:$_GET['t']); +?> +<h1>Check Ticket Status</h1> +<p>Please provide us with your email address and a ticket number, and an access +link will be emailed to you.</p> +<form action="login.php" method="post" id="clientLogin"> + <?php csrf_token(); ?> +<div style="display:table-row"> + <div style="width:40%;display:table-cell;box-shadow: 12px 0 15px -15px rgba(0,0,0,0.4);padding-right: 2em;"> + <strong><?php echo Format::htmlchars($errors['login']); ?></strong> + <br> + <div> + <label for="email">E-Mail Address:</label><br/> + <input id="email" type="text" name="lemail" size="30" value="<?php echo $email; ?>"> + </div> + <div> + <label for="ticketno">Ticket Number:</label><br/> + <input id="ticketno" type="text" name="lticket" size="16" value="<?php echo $ticketid; ?>"></td> + </div> + <p> + <input class="btn" type="submit" value="Email Access Link"> + </p> + </div> + <div style="display:table-cell;padding-left: 2em;padding-right:90px;"> +<?php if ($cfg && $cfg->getClientRegistrationMode() !== 'disabled') { ?> + Have an account with us? + <a href="account.php?do=create">Sign In</a> <?php + if ($cfg->isClientRegistrationEnabled()) { ?> + or <a href="login.php?do=create">register for an account</a> <?php + } ?> to access all your tickets. +<?php +} ?> + </div> +</div> +</form> +<br> +<p> +If this is your first time contacting us or you've lost the ticket number, please <a href="open.php">open a new ticket</a>. +</p> diff --git a/include/client/header.inc.php b/include/client/header.inc.php index 107cf479c43884bf362b57f72378c40bc1a5be78..e6ea209f4048ee21b92244bfc0e504e82a89c8fa 100644 --- a/include/client/header.inc.php +++ b/include/client/header.inc.php @@ -43,21 +43,23 @@ header("Content-Type: text/html; charset=UTF-8\r\n"); style="height: 5em"></a> <p> <?php - if($thisclient && is_object($thisclient) && $thisclient->isValid()) { + if ($thisclient && is_object($thisclient) && $thisclient->isValid() + && !$thisclient->isGuest()) { echo Format::htmlchars($thisclient->getName()).' |'; ?> - <a href="<?php echo ROOT_PATH; ?>profile.php">Profile</a> | - <?php - if($cfg->showRelatedTickets()) {?> + <a href="<?php echo ROOT_PATH; ?>account.php">Profile</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> - <?php - }elseif($nav){ ?> - Guest User | <a href="<?php echo ROOT_PATH; ?>login.php">Log In</a> - <?php - } ?> + <?php + } elseif($nav) { + if ($cfg->getClientRegistrationMode() == 'public') { ?> + Guest User | <?php + } + if ($cfg->getClientRegistrationMode() != 'disabled') { ?> + <a href="<?php echo ROOT_PATH; ?>login.php">Sign In</a> +<?php + } + } ?> </p> </div> <?php diff --git a/include/client/login.inc.php b/include/client/login.inc.php index 586ef9808b75144e37dafc775debb9cc8f4ca1c4..fd1d7bc80abac91077654ae8a58d2cfc285b0444 100644 --- a/include/client/login.inc.php +++ b/include/client/login.inc.php @@ -1,29 +1,54 @@ <?php if(!defined('OSTCLIENTINC')) die('Access Denied'); -$email=Format::input($_POST['lemail']?$_POST['lemail']:$_GET['e']); -$ticketid=Format::input($_POST['lticket']?$_POST['lticket']:$_GET['t']); +$email=Format::input($_POST['luser']?:$_GET['e']); +$passwd=Format::input($_POST['lpasswd']?:$_GET['t']); + +$content = Page::lookup(Page::getIdByType('banner-client')); + +if ($content) { + list($title, $body) = $ost->replaceTemplateVariables( + array($content->getName(), $content->getBody())); +} else { + $title = 'Sign In'; + $body = 'To better serve you, we encourage our clients to register for + an account and verify the email address we have on record.'; +} + ?> -<h1>Check Ticket Status</h1> -<p>Please provide us with your email address and a ticket number, and an access -link will be emailed to you.</p> +<h1><?php echo Format::display($title); ?></h1> +<p><?php echo Format::display($body); ?></p> <form action="login.php" method="post" id="clientLogin"> <?php csrf_token(); ?> +<div style="display:table-row"> + <div style="width:40%;display:table-cell;box-shadow: 12px 0 15px -15px rgba(0,0,0,0.4);padding-left: 2em;"> <strong><?php echo Format::htmlchars($errors['login']); ?></strong> <br> <div> - <label for="email">E-Mail Address:</label> - <input id="email" type="text" name="lemail" size="30" value="<?php echo $email; ?>"> + <label for="username">Username:</label> + <input id="username" type="text" name="luser" size="30" value="<?php echo $email; ?>"> </div> <div> - <label for="ticketno">Ticket Number:</label> - <input id="ticketno" type="text" name="lticket" size="16" value="<?php echo $ticketid; ?>"></td> + <label for="passwd">Password:</label> + <input id="passwd" type="password" name="lpasswd" size="30" value="<?php echo $passwd; ?>"></td> </div> <p> - <input class="btn" type="submit" value="Email Access Link"> + <input class="btn" type="submit" value="Sign In"> +<?php if ($suggest_pwreset) { ?> + <a style="padding-top:4px;display:inline-block;" href="pwreset.php">Forgot My Password</a> +<?php } ?> </p> + </div> + <div style="display:table-cell;padding-left: 2em;"> +<?php if ($cfg && $cfg->isClientRegistrationEnabled()) { ?> + Not yet registered? <a href="account.php?do=create">Create an account</a> +<?php } ?> + </div> +</div> </form> <br> <p> +<?php if ($cfg && !$cfg->isClientLoginRequired()) { ?> If this is your first time contacting us or you've lost the ticket number, please <a href="open.php">open a new ticket</a>. +<?php } ?> </p> diff --git a/include/client/profile.inc.php b/include/client/profile.inc.php index a103ba1f7ec5f00ec1ec182b520ccfb2eaa8077b..d5651031cdaac3a0a5975edd862384a18d69bfe6 100644 --- a/include/client/profile.inc.php +++ b/include/client/profile.inc.php @@ -8,12 +8,83 @@ account </p> <form action="profile.php" method="post"> <?php csrf_token(); ?> -<table width="800"> +<table width="800" class="padded"> <?php foreach ($user->getForms() as $f) { $f->render(false); } +if ($acct = $thisclient->getAccount()) { + $info=$acct->getInfo(); + $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); ?> +<tr> + <td colspan="2"> + <div><hr><h3>Preferences</h3> + </div> + </td> +</tr> + <td>Time Zone:</td> + <td> + <select name="timezone_id" id="timezone_id"> + <option value="0">— Select Time Zone —</option> + <?php + $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$offset, $tz)=db_fetch_row($res)){ + $sel=($info['timezone_id']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz); + } + } + ?> + </select> + <span class="error"><?php echo $errors['timezone_id']; ?></span> + </td> +</tr> +<tr> + <td width="180"> + Daylight Saving: + </td> + <td> + <input type="checkbox" name="dst" value="1" <?php echo $info['dst']?'checked="checked"':''; ?>> + Observe daylight saving + <em>(Current Time: <strong><?php echo Format::date($cfg->getDateTimeFormat(),Misc::gmtime(),$info['tz_offset'],$info['dst']); ?></strong>)</em> + </td> +</tr> +<tr> + <td colspan=2"> + <div><hr><h3>Access Credentials</h3></div> + </td> +</tr> +<?php if (!isset($_SESSION['_client']['reset-token'])) { ?> +<tr> + <td width="180"> + Current Password: + </td> + <td> + <input type="password" size="18" name="cpasswd" value="<?php echo $info['cpasswd']; ?>"> + <span class="error"> <?php echo $errors['cpasswd']; ?></span> + </td> +</tr> +<?php } ?> +<tr> + <td width="180"> + New Password: + </td> + <td> + <input type="password" size="18" name="passwd1" value="<?php echo $info['passwd1']; ?>"> + <span class="error"> <?php echo $errors['passwd1']; ?></span> + </td> +</tr> +<tr> + <td width="180"> + Confirm New Password: + </td> + <td> + <input type="password" size="18" name="passwd2" value="<?php echo $info['passwd2']; ?>"> + <span class="error"> <?php echo $errors['passwd2']; ?></span> + </td> +</tr> +<?php } ?> </table> <hr> <p style="text-align: center;"> diff --git a/include/client/pwreset.login.php b/include/client/pwreset.login.php new file mode 100644 index 0000000000000000000000000000000000000000..a1c1ed4e75f656b8372d95052aa1201ba2e28545 --- /dev/null +++ b/include/client/pwreset.login.php @@ -0,0 +1,26 @@ +<?php +if(!defined('OSTCLIENTINC')) die('Access Denied'); + +$userid=Format::input($_POST['userid']); +?> +<h1>Forgot My Password</h1> +<p> +Enter your username or email address again in the form below and press the +<strong>Login</strong> to access your account and reset your password. + +<form action="pwreset.php" method="post" id="clientLogin"> + <div style="width:50%;display:inline-block"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="reset"/> + <input type="hidden" name="token" value="<?php echo $_REQUEST['token']; ?>"/> + <strong><?php echo Format::htmlchars($banner); ?></strong> + <br> + <div> + <label for="username">Username:</label> + <input id="username" type="text" name="userid" size="30" value="<?php echo $userid; ?>"> + </div> + <p> + <input class="btn" type="submit" value="Login"> + </p> + </div> +</form> diff --git a/include/client/pwreset.request.php b/include/client/pwreset.request.php new file mode 100644 index 0000000000000000000000000000000000000000..28922c8d2a9d6028ab18ec533ea7be9873c847ec --- /dev/null +++ b/include/client/pwreset.request.php @@ -0,0 +1,26 @@ +<?php +if(!defined('OSTCLIENTINC')) die('Access Denied'); + +$userid=Format::input($_POST['userid']); +?> +<h1>Forgot My Password</h1> +<p> +Enter your username or email address in the form below and press the +<strong>Send Email</strong> button to have a password reset link sent to +your email account on file. + +<form action="pwreset.php" method="post" id="clientLogin"> + <div style="width:50%;display:inline-block"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="sendmail"/> + <strong><?php echo Format::htmlchars($banner); ?></strong> + <br> + <div> + <label for="username">Username:</label> + <input id="username" type="text" name="userid" size="30" value="<?php echo $userid; ?>"> + </div> + <p> + <input class="btn" type="submit" value="Send Email"> + </p> + </div> +</form> diff --git a/include/client/pwreset.sent.php b/include/client/pwreset.sent.php new file mode 100644 index 0000000000000000000000000000000000000000..c76f50957ecc56ec8a2946a26d729e364d332968 --- /dev/null +++ b/include/client/pwreset.sent.php @@ -0,0 +1,13 @@ +<h1>Forgot My Password</h1> +<p> +Enter your username or email address in the form below and press the +<strong>Send Email</strong> button to have a password reset link sent to +your email account on file. + +<form action="pwreset.php" method="post" id="clientLogin"> + <div style="width:50%;display:inline-block"> + We have sent you a reset email to the email address you have on file for + your account. If you do not receive the email or cannot reset your + password, please ... + </div> +</form> diff --git a/include/client/register.confirm.inc.php b/include/client/register.confirm.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..a0628c0bcd287ed760cc95d48b44f8b1dae380bd --- /dev/null +++ b/include/client/register.confirm.inc.php @@ -0,0 +1,17 @@ +<?php if ($content) { + list($title, $body) = $ost->replaceTemplateVariables( + array($content->getName(), $content->getBody())); ?> +<h1><?php echo Format::display($title); ?></h1> +<p><?php +echo Format::display($body); ?> +</p> +<?php } else { ?> +<h1>Account Registration</h1> +<p> +<strong>Thanks for registering for an account.</strong> +</p> +<p> +We've just sent you an email to the address you entered. Please follow the +link in the email to confirm your account and gain access to your tickets. +</p> +<?php } ?> diff --git a/include/client/register.confirmed.inc.php b/include/client/register.confirmed.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..5bba31e8c539699fa2834be37c03a86a3cca30e6 --- /dev/null +++ b/include/client/register.confirmed.inc.php @@ -0,0 +1,18 @@ +<?php if ($content) { + list($title, $body) = $ost->replaceTemplateVariables( + array($content->getName(), $content->getBody())); ?> +<h1><?php echo Format::display($title); ?></h1> +<p><?php +echo Format::display($body); ?> +</p> +<?php } else { ?> +<h1>Account Registration</h1> +<p> +<strong>Thanks for registering for an account.</strong> +</p> +<p> +You've confirmed your email address and successfully activated your account. +You may proceed to check on previously opened tickets or open a new ticket. +</p> +<p><em>Your friendly support center</em></p> +<?php } ?> diff --git a/include/client/register.inc.php b/include/client/register.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..0a5676781c0de4625ac2e033279ed3c67ec7bbf5 --- /dev/null +++ b/include/client/register.inc.php @@ -0,0 +1,115 @@ +<?php +$info = $_POST; +if (!isset($info['timezone_id'])) + $info += array( + 'timezone_id' => $cfg->getDefaultTimezoneId(), + 'dst' => $cfg->observeDaylightSaving(), + 'backend' => null, + ); +if (isset($user) && $user instanceof ClientCreateRequest) { + $bk = $user->getBackend(); + $info = array_merge($info, array( + 'backend' => $bk::$id, + 'username' => $user->getUsername(), + )); +} + +?> +<h1>Account Registration</h1> +<p> +Use the forms below to create or update the information we have on file for +your account +</p> +<form action="account.php" method="post"> + <?php csrf_token(); ?> + <input type="hidden" name="do" value="<?php echo $_REQUEST['do'] + ?: ($info['backend'] ? 'import' :'create'); ?>" /> +<table width="800" class="padded"> +<tbody> +<?php + $cf = $user_form ?: UserForm::getInstance(); + $cf->render(false); +?> +<tr> + <td colspan="2"> + <div><hr><h3>Preferences</h3> + </div> + </td> +</tr> + <td>Time Zone:</td> + <td> + <select name="timezone_id" id="timezone_id"> + <?php + $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id'; + if(($res=db_query($sql)) && db_num_rows($res)){ + while(list($id,$offset, $tz)=db_fetch_row($res)){ + $sel=($info['timezone_id']==$id)?'selected="selected"':''; + echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz); + } + } + ?> + </select> + <span class="error"><?php echo $errors['timezone_id']; ?></span> + </td> +</tr> +<tr> + <td width="180"> + Daylight Saving: + </td> + <td> + <input type="checkbox" name="dst" value="1" <?php echo $info['dst']?'checked="checked"':''; ?>> + Observe daylight saving + <em>(Current Time: <strong><?php echo Format::date($cfg->getDateTimeFormat(),Misc::gmtime(),$info['tz_offset'],$info['dst']); ?></strong>)</em> + </td> +</tr> +<tr> + <td colspan=2"> + <div><hr><h3>Access Credentials</h3></div> + </td> +</tr> +<?php if ($info['backend']) { ?> +<tr> + <td width="180"> + Login With: + </td> + <td> + <input type="hidden" name="backend" value="<?php echo $info['backend']; ?>"/> + <input type="hidden" name="username" value="<?php echo $info['username']; ?>"/> +<?php foreach (UserAuthenticationBackend::allRegistered() as $bk) { + if ($bk::$id == $info['backend']) { + echo $bk::$name; + break; + } +} ?> + </td> +</tr> +<?php } else { ?> +<tr> + <td width="180"> + Create a Password: + </td> + <td> + <input type="password" size="18" name="passwd1" value="<?php echo $info['passwd1']; ?>"> + <span class="error"> <?php echo $errors['passwd1']; ?></span> + </td> +</tr> +<tr> + <td width="180"> + Confirm New Password: + </td> + <td> + <input type="password" size="18" name="passwd2" value="<?php echo $info['passwd2']; ?>"> + <span class="error"> <?php echo $errors['passwd2']; ?></span> + </td> +</tr> +<?php } ?> +</tbody> +</table> +<hr> +<p style="text-align: center;"> + <input type="submit" value="Register"/> + <input type="button" value="Cancel" onclick="javascript: + window.location.href='index.php';"/> +</p> +</form> + diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php index 1c227ba4cdec255b7e75607abfeab143e549d305..b355e2265d69e7c6121474edc41f414c4c3cee37 100644 --- a/include/client/tickets.inc.php +++ b/include/client/tickets.inc.php @@ -1,5 +1,5 @@ <?php -if(!defined('OSTCLIENTINC') || !is_object($thisclient) || !$thisclient->isValid() || !$cfg->showRelatedTickets()) die('Access Denied'); +if(!defined('OSTCLIENTINC') || !is_object($thisclient) || !$thisclient->isValid()) die('Access Denied'); $qstr='&'; //Query string collector $status=null; diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 469740f6c7e60d1c434d8b1d9709c4c27fcfe89e..27f2bd0845f90a09050801e8bb6763a1ef0840e8 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -8,7 +8,18 @@ $dept = $ticket->getDept(); if(!$dept || !$dept->isPublic()) $dept = $cfg->getDefaultDept(); -?> +if ($thisclient && $thisclient->isGuest() + && $cfg->isClientRegistrationEnabled()) { ?> + +<div id="msg_info"> + <i class="icon-compass icon-2x pull-left"></i> + <strong>Looking for your other tickets?</strong></br> + <a href="login.php" style="text-decoration:underline">Sign in</a> or + <a href="account.php?do=create" style="text-decoration:underline">register for an account</a> + for the best experience on our help desk.</div> + +<?php } ?> + <table width="800" cellpadding="1" cellspacing="0" border="0" id="ticketInfo"> <tr> <td colspan="2" width="100%"> diff --git a/include/i18n/en_US/config.yaml b/include/i18n/en_US/config.yaml index 17c7083c31afd188c833e97ce2727fd01c641b54..9ec6537a20eaf27afa0a7568a7c7048a1237ccd0 100644 --- a/include/i18n/en_US/config.yaml +++ b/include/i18n/en_US/config.yaml @@ -81,3 +81,4 @@ core: random_ticket_ids: 1 log_level: 2 log_graceperiod: 12 + client_registration: 'public' diff --git a/include/i18n/en_US/help/tips/settings.access.yaml b/include/i18n/en_US/help/tips/settings.access.yaml new file mode 100644 index 0000000000000000000000000000000000000000..911a4b34c3b665cdf1be6d3930344fc28ac5c520 --- /dev/null +++ b/include/i18n/en_US/help/tips/settings.access.yaml @@ -0,0 +1,22 @@ +# +# This is popup help messages for the Admin Panel -> Settings -> System page +# +# Fields: +# title - Shown in bold at the top of the popover window +# content - The body of the help popover +# links - List of links shows below the content +# title - Link title +# href - href of link (links starting with / are translated to the +# helpdesk installation path) +# +# The key names such as 'helpdesk_name' should not be translated as they +# must match the HTML #ids put into the page template. +# +--- +# Authentication settings +password_reset: + title: Password Expiration Policy + content: > + Sets how often (in months) staff members will be required to change + their password. If disabled (set to "No expiration"), passwords will + not expire. diff --git a/include/i18n/en_US/help/tips/settings.system.yaml b/include/i18n/en_US/help/tips/settings.system.yaml index 1d08d55b0d9f77bd2ee8122a971e5fcd3e7ca1b8..60e336b73470394cc7f7821317dd883e80d2006d 100644 --- a/include/i18n/en_US/help/tips/settings.system.yaml +++ b/include/i18n/en_US/help/tips/settings.system.yaml @@ -68,14 +68,6 @@ name_format: Email templates will use the format for names if no other format is specified in the place holder. -# Authentication settings -password_reset: - title: Password Expiration Policy - content: > - Sets how often (in months) staff members will be required to change - their password. If disabled (set to "No expiration"), passwords will - not expire. - # Date and time options date_and_time: title: Localized Date Formats diff --git a/include/i18n/en_US/templates/email/staff.pwreset.yaml b/include/i18n/en_US/templates/email/staff.pwreset.yaml deleted file mode 100644 index 12f6fd0f3d1268fc29a30281d6019b6c1ab0b3bf..0000000000000000000000000000000000000000 --- a/include/i18n/en_US/templates/email/staff.pwreset.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# -# Email template: staff.pwreset -# -# Sent when a staff member requests a password reset via the <a>Forgot my -# password</a> link in the staff control login screen -# ---- -notes: | - Sent when a staff member requests a password reset via the <a>Forgot my - password</a> link in the staff control login screen - -subject: | - osTicket Staff Password Reset -body: | - <h3><strong>Hi %{staff.name.first},</strong></h3> - A password reset request has been submitted on your behalf for the - helpdesk at %{url}. - <br> - <br> - If you feel that this has been done in error, delete and disregard this - email. Your account is still secure and no one has been given access to - it. It is not locked and your password has not been reset. Someone could - have mistakenly entered your email address. - <br> - <br> - Follow the link below to login to the help desk and change your - password. - <br> - <br> - <a href="%{reset_link}">%{reset_link}</a> - <br> - <br> - <em style="font-size: small">Your friendly Customer Support System</em> - <br> - <img src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc" alt="Powered by osTicket" - width="126" height="19" style="width: 126px;"> diff --git a/include/i18n/en_US/templates/email/user.accesslink.yaml b/include/i18n/en_US/templates/email/user.accesslink.yaml deleted file mode 100644 index acd50fe0fd5b9c89b9efffffa72e271b85491f0e..0000000000000000000000000000000000000000 --- a/include/i18n/en_US/templates/email/user.accesslink.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# -# Email template: user.accesslink -# -# Sent when a user requests an access link to check the status of a ticket -# -# -# ---- -notes: | - Sent when a user requests an access link to check the status of a ticket - -subject: | - Ticket [#%{ticket.number}] Access Link -body: | - <h3><strong>Hi %{recipient.name.first},</strong></h3> - An access link request for ticket #%{ticket.number} has been submitted on your behalf for the - helpdesk at %{url}. - <br> - <br> - Follow the link below to check the status of the ticket #%{ticket.number}. - <br> - <br> - <a href="%{recipient.ticket_link}">%{recipient.ticket_link}</a> - <br> - <br>If you <strong>did not</strong> make the request, please delete - and disregard this email. Your account is still secure and no one has - been given access to the ticket. Someone could have mistakenly entered - your email address. - <br> - <br> - --<br> - %{company.name} diff --git a/include/i18n/en_US/templates/page/access-link.yaml b/include/i18n/en_US/templates/page/access-link.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cd445d171e1e9b243ffc5aa8a969e4245f18eda3 --- /dev/null +++ b/include/i18n/en_US/templates/page/access-link.yaml @@ -0,0 +1,28 @@ +# +# access-link.yaml +# +# Ticket access link sent to clients for guest-only systems where the ticket +# number and email address will trigger an access link sent via email +# +--- +notes: > + Ticket access link sent to clients for guest-only systems where the ticket + number and email address will trigger an access link sent via email +name: "Ticket [#%{ticket.number}] Access Link" +body: > + <h3><strong>Hi %{recipient.name.first},</strong></h3> + An access link request for ticket #%{ticket.number} has been submitted + on your behalf for the helpdesk at %{url}.<br /> + <br /> + Follow the link below to check the status of the ticket + #%{ticket.number}.<br /> + <br /> + <a href="%{recipient.ticket_link}">%{recipient.ticket_link}</a><br /> + <br /> + If you <strong>did not</strong> make the request, please delete and + disregard this email. Your account is still secure and no one has been + given access to the ticket. Someone could have mistakenly entered your + email address.<br /> + <br /> + --<br /> + %{company.name} diff --git a/include/i18n/en_US/templates/page/banner-client.yaml b/include/i18n/en_US/templates/page/banner-client.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5bab4cd11c11027a3ca93740d481cb6f040a2a35 --- /dev/null +++ b/include/i18n/en_US/templates/page/banner-client.yaml @@ -0,0 +1,13 @@ +# +# banner-client.yaml +# +# This forms the header on the staff login page. It can be useful to inform +# your clients about your login and registration policies. +--- +notes: > + This forms the header on the staff login page. It can be useful to + inform your clients about your login and registration policies. +name: "Sign in to %{company.name}" +body: > + To better serve you, we encourage our clients to register for an account + and verify the email address we have on record. diff --git a/include/i18n/en_US/templates/page/banner-staff.yaml b/include/i18n/en_US/templates/page/banner-staff.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cde341856b768d108f378bf7f2d5086e0fd7714e --- /dev/null +++ b/include/i18n/en_US/templates/page/banner-staff.yaml @@ -0,0 +1,9 @@ +# +# banner-staff.yaml +# +# This is the initial message and banner shown on the staff login page +--- +notes: > + This is the initial message and banner shown on the staff login page +name: "Authentication Required" +body: "" diff --git a/include/i18n/en_US/templates/page/pwreset-client.yaml b/include/i18n/en_US/templates/page/pwreset-client.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d3ea3a3917f4754e3c1e0eab805e8a60bde72bbb --- /dev/null +++ b/include/i18n/en_US/templates/page/pwreset-client.yaml @@ -0,0 +1,28 @@ +# +# pwreset-client.yaml +# +# Template of the email sent to clients when using the Forgot My Password +# link on the login page +--- +notes: > + Template of the email sent to clients when using the Forgot My Password + link on the login page +name: "%{company.name} Help Desk Access" +body: > + <h3><strong>Hi %{user.name.first},</strong></h3> + A password reset request has been submitted on your behalf for the + helpdesk at %{url}.<br /> + <br /> + If you feel that this has been done in error, delete and disregard this + email. Your account is still secure and no one has been given access to + it. It is not locked and your password has not been reset. Someone could + have mistakenly entered your email address.<br /> + <br /> + Follow the link below to login to the help desk and change your + password.<br /> + <br /> + <a href="%{link}">%{link}</a><br /> + <br /> + <em style="font-size: small">Your friendly Customer Support System + <br /> + %{company.name}</em> diff --git a/include/i18n/en_US/templates/page/pwreset-staff.yaml b/include/i18n/en_US/templates/page/pwreset-staff.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ff8c8c157fc1143848281f13c2fa4e0630ad7eba --- /dev/null +++ b/include/i18n/en_US/templates/page/pwreset-staff.yaml @@ -0,0 +1,29 @@ +# +# pwreset-staff.yaml +# +# Template of the email sent to staff members when using the Forgot My +# Password link +--- +notes: > + Template of the email sent to staff members when using the Forgot My + Password link +name: "osTicket Staff Password Reset" +body: > + <h3><strong>Hi %{staff.name.first},</strong></h3> + A password reset request has been submitted on your behalf for the + helpdesk at %{url}.<br /> + <br /> + If you feel that this has been done in error, delete and disregard this + email. Your account is still secure and no one has been given access to + it. It is not locked and your password has not been reset. Someone could + have mistakenly entered your email address.<br /> + <br /> + Follow the link below to login to the help desk and change your + password.<br /> + <br /> + <a href="%{link}">%{link}</a><br /> + <br /> + <em style="font-size: small">Your friendly Customer Support System</em> + <br /> + <img src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc" alt="Powered by + osTicket" width="126" height="19" style="width: 126px" /> diff --git a/include/i18n/en_US/templates/page/registration-client.yaml b/include/i18n/en_US/templates/page/registration-client.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ea820a31fb9a6a945a2156a570fb369258225699 --- /dev/null +++ b/include/i18n/en_US/templates/page/registration-client.yaml @@ -0,0 +1,24 @@ +# +# registration-staff.yaml +# +# Confirmation email sent to clients when accounts are created for them by +# staff or via the client portal. This email serves as an email address +# verification. +# +--- +notes: > + Confirmation email sent to clients when accounts are created for them by + staff or via the client portal. This email serves as an email address + verification. Please use %{link} somewhere in the body. +name: "Welcome to %{company.name}" +body: > + <h3><strong>Hi %{recipient.name.first},</strong></h3> We've created an + account for you at our help desk at %{url}.<br /> + <br /> + Please follow the link below to confirm your account and gain access to + your tickets.<br /> + <br /> + <a href="%{link}">%{link}</a><br /> + <br /> + <em style="font-size: small">Your friendly Customer Support System<br + />%{company.name}</em> diff --git a/include/i18n/en_US/templates/page/registration-confirm.yaml b/include/i18n/en_US/templates/page/registration-confirm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..00aa6880940fd5a32fda2362fee360bde992e126 --- /dev/null +++ b/include/i18n/en_US/templates/page/registration-confirm.yaml @@ -0,0 +1,22 @@ +# +# registration-confirm.yaml +# +# Template of the page shown to the user after registering for an account. +# The system will send the user an email with a link they should follow to +# confirm the account. This page should inform them of the next step in +# the process. +# +--- +notes: > + Template of the page shown to the user after registering for an account. + The system will send the user an email with a link they should follow to + confirm the account. This page should inform them of the next step in + the process. +name: "Account registration" +body: > + <strong>Thanks for registering for an account.</strong> + <p> + We've just sent you an email to the address you entered. Please follow + the link in the email to confirm your account and gain access to your + tickets. + </p> diff --git a/include/i18n/en_US/templates/page/registration-staff.yaml b/include/i18n/en_US/templates/page/registration-staff.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c544a06f0e537ddcabffba8fafc4907b07820784 --- /dev/null +++ b/include/i18n/en_US/templates/page/registration-staff.yaml @@ -0,0 +1,22 @@ +# +# registration-staff.yaml +# +# Initial (optional) email sent to staff members when accounts are created +# for them in the staff control panel +# +--- +notes: > + Initial (optional) email sent to staff members when accounts are created + for them in the staff control panel +name: "Welcome to osTicket" +body: > + <h3><strong>Hi %{user.name.first},</strong></h3> + We've created an account for you at our help desk at %{url}.<br /> + <br /> + Please follow the link below to confirm your account and gain access to + your tickets.<br /> + <br /> + <a href="%{link}">%{link}</a><br /> + <br /> + <em style="font-size: small">Your friendly Customer Support System + <br />%{company.name}</em> diff --git a/include/i18n/en_US/templates/page/registration-thanks.yaml b/include/i18n/en_US/templates/page/registration-thanks.yaml new file mode 100644 index 0000000000000000000000000000000000000000..adfb922a76d2d0eb5d90121e38103e4b3961a381 --- /dev/null +++ b/include/i18n/en_US/templates/page/registration-thanks.yaml @@ -0,0 +1,22 @@ +# +# registration-thanks.yaml +# +# Page shown to the user after successfully registring and confirming their +# account. This page should inform the user that the process is complete and +# that the user can now submit a ticket or access existing tickets +# +--- +notes: > + Page shown to the user after successfully registring and confirming their + account. This page should inform the user that the process is complete and + that the user can now submit a ticket or access existing tickets +name: "Account Confirmed!" +body: > + <strong>Thanks for registering for an account.</strong><br /> + <br /> + You've confirmed your email address and successfully activated your + account. You may proceed to check on previously opened tickets or open a + new ticket.<br /> + <br /> + <em>Your friendly support center</em><br /> + %{company.name} diff --git a/include/staff/login.tpl.php b/include/staff/login.tpl.php index 35fffcc98ebb9374656b9f4fee90945e15c89d3b..d65aeea471a03ae27b7748e8fa82468ce5a73e35 100644 --- a/include/staff/login.tpl.php +++ b/include/staff/login.tpl.php @@ -5,6 +5,7 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array(); <div id="loginBox"> <h1 id="logo"><a href="index.php">osTicket Staff Control Panel</a></h1> <h3><?php echo Format::htmlchars($msg); ?></h3> + <div><small><?php echo ($content) ? Format::display($content->getBody()) : ''; ?></small></div> <form action="login.php" method="post"> <?php csrf_token(); ?> <input type="hidden" name="do" value="scplogin"> diff --git a/include/staff/pages.inc.php b/include/staff/pages.inc.php index 34a3fa2f9d2ca062b285400e398a2ffc756598a4..5dff4eced87e1d141ff292cff029cd46b6a94119 100644 --- a/include/staff/pages.inc.php +++ b/include/staff/pages.inc.php @@ -6,7 +6,7 @@ $sql='SELECT page.id, page.isactive, page.name, page.created, page.updated, ' .'page.type, count(topic.topic_id) as topics ' .' FROM '.PAGE_TABLE.' page ' .' LEFT JOIN '.TOPIC_TABLE.' topic ON(topic.page_id=page.id) ' - .' WHERE 1 '; + .' WHERE type in ("other","landing","thank-you","offline") '; $sortOptions=array( 'name'=>'page.name', 'status'=>'page.isactive', 'created'=>'page.created', 'updated'=>'page.updated', diff --git a/include/staff/settings-access.inc.php b/include/staff/settings-access.inc.php new file mode 100644 index 0000000000000000000000000000000000000000..35c8ef8a53915b2220e4d9a14c79544673b8009b --- /dev/null +++ b/include/staff/settings-access.inc.php @@ -0,0 +1,201 @@ +<?php +if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied'); + +?> +<h2>Access Control Settings</h2> +<form action="settings.php?t=access" method="post" id="save"> +<?php csrf_token(); ?> +<input type="hidden" name="t" value="access" > +<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="2"> + <h4>Configure Access to this Help Desk</h4> + </th> + </tr> + </thead> + <tbody> + <tr> + <th colspan="2"> + <em><b>Staff Authentication Settings</b></em> + </th> + </tr> + <tr><td>Password Expiration Policy:</th> + <td> + <select name="passwd_reset_period"> + <option value="0"> — No expiration —</option> + <?php + for ($i = 1; $i <= 12; $i++) { + echo sprintf('<option value="%d" %s>%s%s</option>', + $i,(($config['passwd_reset_period']==$i)?'selected="selected"':''), $i>1?"Every $i ":'', $i>1?' Months':'Monthly'); + } + ?> + </select> + <font class="error"><?php echo $errors['passwd_reset_period']; ?></font> + <i class="help-tip icon-question-sign" href="#password_reset"></i> + </td> + </tr> + <tr><td>Allow Password Resets:</th> + <td> + <input type="checkbox" name="allow_pw_reset" <?php echo $config['allow_pw_reset']?'checked="checked"':''; ?>> + <em>Enables the <u>Forgot my password</u> link on the staff + control panel</em> + </td> + </tr> + <tr><td>Password Reset Window:</th> + <td> + <input type="text" name="pw_reset_window" size="6" value="<?php + echo $config['pw_reset_window']; ?>"> + Maximum time <em>in minutes</em> a password reset token can + be valid. + <font class="error"> <?php echo $errors['pw_reset_window']; ?></font> + </td> + </tr> + <tr><td>Staff Excessive Logins:</td> + <td> + <select name="staff_max_logins"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_max_logins']==$i)?'selected="selected"':''), $i); + } + ?> + </select> failed login attempt(s) allowed before a + <select name="staff_login_timeout"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_login_timeout']==$i)?'selected="selected"':''), $i); + } + ?> + </select> minute lock-out is enforced. + </td> + </tr> + <tr><td>Staff Session Timeout:</td> + <td> + <input type="text" name="staff_session_timeout" size=6 value="<?php echo $config['staff_session_timeout']; ?>"> + Maximum idle time in minutes before a staff member must log in again (enter 0 to disable). + </td> + </tr> + <tr><td>Bind Staff Session to IP:</td> + <td> + <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>> + <em>(binds staff session to originating IP address upon login)</em> + </td> + </tr> + <tr> + <th colspan="2"> + <em><b>Client Authentication Settings</b></em> + </th> + </tr> + <tr><td>Registration Required:</td> + <td><input type="checkbox" name="clients_only" <?php + if ($config['clients_only']) + echo 'checked="checked"'; ?>/> + Require registration and login to create tickets + </td> + <tr><td>Registration Method:</td> + <td><select name="client_registration"> +<?php foreach (array( + 'disabled' => 'Disabled — All users are guests', + 'public' => 'Public — Anyone can register', + 'closed' => 'Private — Only staff can register clients',) + as $key=>$val) { ?> + <option value="<?php echo $key; ?>" <?php + if ($config['client_registration'] == $key) + echo 'selected="selected"'; ?>><?php echo $val; + ?></option><?php + } ?> + </select></td> + </tr> + <tr><td>Client Excessive Logins:</td> + <td> + <select name="client_max_logins"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_max_logins']==$i)?'selected="selected"':''), $i); + } + + ?> + </select> failed login attempt(s) allowed before a + <select name="client_login_timeout"> + <?php + for ($i = 1; $i <= 10; $i++) { + echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_login_timeout']==$i)?'selected="selected"':''), $i); + } + ?> + </select> minute lock-out is enforced. + </td> + </tr> + <tr><td>Client Session Timeout:</td> + <td> + <input type="text" name="client_session_timeout" size=6 value="<?php echo $config['client_session_timeout']; ?>"> + Maximum idle time in minutes before a client must log in again (enter 0 to disable). + </td> + </tr> + </tbody> + <thead> + <tr> + <th colspan="2"> + <h4>Authentication and Registration Templates</h4> + </th> + </tr> + </thead> + <tbody> +<?php +$res = db_query('select distinct(`type`), content_id, notes, name, updated from ' + .PAGE_TABLE + .' where isactive=1 group by `type`'); +$contents = array(); +while (list($type, $id, $notes, $name, $u) = db_fetch_row($res)) + $contents[$type] = array($id, $name, $notes, $u); + +$manage_content = function($title, $content) use ($contents) { + list($id, $name, $notes, $upd) = $contents[$content]; + $notes = explode('. ', $notes); + $notes = $notes[0]; + ?><tr><td colspan="2"> + <a href="#ajax.php/content/<?php echo $id; ?>/manage" + onclick="javascript: + $.dialog($(this).attr('href').substr(1), 200); + return false;"><i class="icon-file-text pull-left icon-2x" + style="color:#bbb;"></i> <?php + echo Format::htmlchars($title); ?></a><br/> + <span class="faded" style="display:inline-block;width:90%"><?php + echo Format::display($notes); ?> + <em>(Last Updated <?php echo Format::db_datetime($upd); ?>)</em></span></td></tr><?php +}; ?> + <tr> + <th colspan="2"> + <em><b>Authentication and Registration Templates</b></em> + </th> + </tr> + <?php $manage_content('Staff Members', 'pwreset-staff'); ?> + <?php $manage_content('Clients', 'pwreset-client'); ?> + <?php $manage_content('Guess Ticket Access', 'access-link'); ?> + <tr> + <th colspan="2"> + <em><b>Sign-In Pages</b></em> + </th> + </tr> + <?php $manage_content('Staff Login Banner', 'banner-staff'); ?> + <?php $manage_content('Client Sign-In Page', 'banner-client'); ?> + <tr> + <th colspan="2"> + <em><b>Client Account Registration</b></em> + </th> + </tr> + <?php $manage_content('Please Confirm Email Address Page', 'registration-confirm'); ?> + <?php $manage_content('Confirmation Email', 'registration-client'); ?> + <?php $manage_content('Account Confirmed Page', 'registration-thanks'); ?> + <tr> + <th colspan="2"> + <em><b>Staff Account Registration</b></em> + </th> + </tr> + <?php $manage_content('Staff Welcome Email', 'registration-staff'); ?> +</tbody> +</table> +<p style="text-align:center"> + <input class="button" type="submit" name="submit" value="Save Changes"> + <input class="button" type="reset" name="reset" value="Reset Changes"> +</p> +</form> diff --git a/include/staff/settings-emails.inc.php b/include/staff/settings-emails.inc.php index b4c95d0247d77b0199e60ca796d21aa433a26835..1d12a8e142e2bb94ae98cb0c02e24c613592299c 100644 --- a/include/staff/settings-emails.inc.php +++ b/include/staff/settings-emails.inc.php @@ -117,6 +117,12 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) <i class="help-tip icon-question-sign" href="#use_email_priority"></i> </td> </tr> + <tr> + <td width="180">Accept Unregistered Email:</td> + <td><input type="checkbox" name="accept_unregistered_email" <?php + echo $config['accept_unregistered_email'] ? 'checked="checked"' : ''; ?>/> + Allow emailed tickets from clients without an account + </tr> <tr> <td width="180">Accept Email Collaborators:</td> <td><input type="checkbox" name="add_email_collabs" <?php diff --git a/include/staff/settings-system.inc.php b/include/staff/settings-system.inc.php index 3ffd9b5af9cf7474c398b5b6bb6a22631afcdfcd..3156492fb8fe6de114d4e0290a83a7736026e8a1 100644 --- a/include/staff/settings-system.inc.php +++ b/include/staff/settings-system.inc.php @@ -118,98 +118,6 @@ $gmtime = Misc::gmtime(); <i class="help-tip icon-question-sign" href="#name_format"></i> </td> </tr> - <tr> - <th colspan="2"> - <em><b>Authentication Settings</b></em> - </th> - </tr> - <tr><td>Password Expiration Policy:</th> - <td> - <select name="passwd_reset_period"> - <option value="0"> — No expiration —</option> - <?php - for ($i = 1; $i <= 12; $i++) { - echo sprintf('<option value="%d" %s>%s%s</option>', - $i,(($config['passwd_reset_period']==$i)?'selected="selected"':''), $i>1?"Every $i ":'', $i>1?' Months':'Monthly'); - } - ?> - </select> - <font class="error"><?php echo $errors['passwd_reset_period']; ?></font> - <i class="help-tip icon-question-sign" href="#password_reset"></i> - </td> - </tr> - <tr><td>Allow Password Resets:</th> - <td> - <input type="checkbox" name="allow_pw_reset" <?php echo $config['allow_pw_reset']?'checked="checked"':''; ?>> - <em>Enables the <u>Forgot my password</u> link on the staff - control panel</em> - </td> - </tr> - <tr><td>Password Reset Window:</th> - <td> - <input type="text" name="pw_reset_window" size="6" value="<?php - echo $config['pw_reset_window']; ?>"> - Maximum time <em>in minutes</em> a password reset token can - be valid. - <font class="error"> <?php echo $errors['pw_reset_window']; ?></font> - </td> - </tr> - <tr><td>Staff Excessive Logins:</td> - <td> - <select name="staff_max_logins"> - <?php - for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_max_logins']==$i)?'selected="selected"':''), $i); - } - ?> - </select> failed login attempt(s) allowed before a - <select name="staff_login_timeout"> - <?php - for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_login_timeout']==$i)?'selected="selected"':''), $i); - } - ?> - </select> minute lock-out is enforced. - </td> - </tr> - <tr><td>Staff Session Timeout:</td> - <td> - <input type="text" name="staff_session_timeout" size=6 value="<?php echo $config['staff_session_timeout']; ?>"> - Maximum idle time in minutes before a staff member must log in again (enter 0 to disable). - </td> - </tr> - <tr><td>Client Excessive Logins:</td> - <td> - <select name="client_max_logins"> - <?php - for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_max_logins']==$i)?'selected="selected"':''), $i); - } - - ?> - </select> failed login attempt(s) allowed before a - <select name="client_login_timeout"> - <?php - for ($i = 1; $i <= 10; $i++) { - echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_login_timeout']==$i)?'selected="selected"':''), $i); - } - ?> - </select> minute lock-out is enforced. - </td> - </tr> - - <tr><td>Client Session Timeout:</td> - <td> - <input type="text" name="client_session_timeout" size=6 value="<?php echo $config['client_session_timeout']; ?>"> - Maximum idle time in minutes before a client must log in again (enter 0 to disable). - </td> - </tr> - <tr><td>Bind Staff Session to IP:</td> - <td> - <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>> - <em>(binds staff session to originating IP address upon login)</em> - </td> - </tr> <tr> <th colspan="2"> <em><b>Date and Time Options</b> diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php index c3d204328c300e9f8d3235883cf52b1227a14499..796ffcdd1bca64af46cac9282d991aa2066dbbd5 100644 --- a/include/staff/settings-tickets.inc.php +++ b/include/staff/settings-tickets.inc.php @@ -76,13 +76,6 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) <em>(Minutes to lock a ticket on activity - enter 0 to disable locking)</em> </td> </tr> - <tr> - <td width="180">Show Related Tickets:</td> - <td> - <input type="checkbox" name="show_related_tickets" value="1" <?php echo $config['show_related_tickets'] ?'checked="checked"':''; ?> > - <em>(Show all related tickets on user login - otherwise access is restricted to one ticket view per login)</em> - </td> - </tr> <tr> <td>Human Verification:</td> <td> diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php index 55351fbee7864581e1294a72e56d758d2d068c48..954aaa8f106eda30b116f8c8ef84d3af9c0793c4 100644 --- a/include/staff/staff.inc.php +++ b/include/staff/staff.inc.php @@ -12,6 +12,7 @@ if($staff && $_REQUEST['a']!='add'){ $info=$staff->getInfo(); $info['id']=$staff->getId(); $info['teams'] = $staff->getTeams(); + $info['signature'] = Format::viewableImages($info['signature']); $qstr.='&id='.$staff->getId(); }else { $title='Add New Staff'; @@ -20,9 +21,12 @@ if($staff && $_REQUEST['a']!='add'){ $passwd_text='Temporary password required only for "Local" authenication'; //Some defaults for new staff. $info['change_passwd']=1; + $info['welcome_email']=1; $info['isactive']=1; $info['isvisible']=1; $info['isadmin']=0; + $info['timezone_id'] = $cfg->getDefaultTimezoneId(); + $info['daylight_saving'] = $cfg->observeDaylightSaving(); $qstr.='&a=add'; } $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); @@ -106,6 +110,22 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <span class="error"> <?php echo $errors['mobile']; ?></span> </td> </tr> +<?php if (!$staff) { ?> + <tr> + <td width="180">Welcome Email</td> + <td><input type="checkbox" name="welcome_email" id="welcome-email" <?php + if ($info['welcome_email']) echo 'checked="checked"'; + ?> onchange="javascript: + var sbk = $('#backend-selection'); + if ($(this).is(':checked')) + $('#password-fields').hide(); + else if (sbk.val() == '' || sbk.val() == 'local') + $('#password-fields').show(); + " /> + Send staff welcome email with account access link + </td> + </tr> +<?php } ?> <tr> <th colspan="2"> <em><strong>Authentication</strong>: <?php echo $passwd_text; ?> <span class="error"> <?php echo $errors['temppasswd']; ?></span></em> @@ -114,10 +134,10 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <tr> <td>Authentication Backend</td> <td> - <select name="backend" onchange="javascript: + <select name="backend" id="backend-selection" onchange="javascript: if (this.value != '' && this.value != 'local') $('#password-fields').hide(); - else + else if (!$('#welcome-email').is(':checked')) $('#password-fields').show(); "> <option value="">— Use any available backend —</option> @@ -131,8 +151,9 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </select> </td> </tr> - </tbody> - <tbody id="password-fields" style="<?php if ($info['backend'] && $info['backend'] != 'local') + </tbody> + <tbody id="password-fields" style="<?php + if ($info['welcome_email'] || ($info['backend'] && $info['backend'] != 'local')) echo 'display:none;'; ?>"> <tr> <td width="180"> diff --git a/include/staff/templates/content-manage.tmpl.php b/include/staff/templates/content-manage.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..d2a546cd0367b02b9c4defb3f63c8531200972c8 --- /dev/null +++ b/include/staff/templates/content-manage.tmpl.php @@ -0,0 +1,26 @@ +<h3>Manage Content — <?php echo Format::htmlchars($content->getName()); ?></h3> +<a class="close" href=""><i class="icon-remove-circle"></i></a> +<hr/> +<form method="post" action="#content/<?php echo $content->getId(); ?>"> + <input type="text" style="width: 100%; font-size: 14pt" name="name" value="<?php + echo Format::htmlchars($content->getName()); ?>" /> + <div style="margin-top: 5px"> + <textarea class="richtext no-bar" name="body"><?php + echo Format::viewableImages($content->getBody()); +?></textarea> + </div> + <div id="msg_info" style="margin-top:7px"><?php +echo $content->getNotes(); ?></div> + <hr/> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="reset" value="Reset"> + <input type="button" name="cancel" class="<?php echo $user ? 'cancel' : 'close' ?>" value="Cancel"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="Save Changes"> + </span> + </p> +</form> +</div> +<div class="clear"></div> diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig index 3ac67bf82846c663c880c6fc441e5e108e310989..19fc2a074f6659a91cc9a83bc9ce83334660c472 100644 --- a/include/upgrader/streams/core.sig +++ b/include/upgrader/streams/core.sig @@ -1 +1 @@ -f5692e24c7afba7ab6168dde0b3bb3c8 +4323a6a81c35efbf7722b7fc4e475440 diff --git a/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql b/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..c3bc8ccc3a34347bd98745a7cd3eab026d9996a0 --- /dev/null +++ b/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql @@ -0,0 +1,122 @@ +/** + * @version v1.8.2 + * @signature 4323a6a81c35efbf7722b7fc4e475440 + * @title Add client login feature + * + */ + +ALTER TABLE `%TABLE_PREFIX%session` + CHANGE `user_id` `user_id` varchar(16) NOT NULL default '0' COMMENT 'osTicket staff/client ID'; + +ALTER TABLE `%TABLE_PREFIX%staff` + CHANGE `signature` `signature` text NOT NULL; + +ALTER TABLE `%TABLE_PREFIX%department` + CHANGE `dept_signature` `dept_signature` text NOT NULL; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_account`; +CREATE TABLE `%TABLE_PREFIX%email_account` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL, + `active` tinyint(1) NOT NULL DEFAULT '1', + `protocol` varchar(64) NOT NULL DEFAULT '', + `host` varchar(128) NOT NULL DEFAULT '', + `port` int(11) NOT NULL, + `username` varchar(128) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `options` varchar(512) DEFAULT NULL, + `errors` int(11) unsigned DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP, + `lastconnect` timestamp NULL DEFAULT NULL, + `lasterror` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + +ALTER TABLE `%TABLE_PREFIX%ticket_thread` + ADD `format` varchar(16) NOT NULL default 'html' AFTER `body`; + +ALTER TABLE `%TABLE_PREFIX%faq_category` + CHANGE `created` `created` datetime NOT NULL, + CHANGE `updated` `updated` datetime NOT NULL; + +ALTER TABLE `%TABLE_PREFIX%filter` + ADD `topic_id` int(11) unsigned NOT NULL default '0' AFTER `form_id`; + +ALTER TABLE `%TABLE_PREFIX%email` + ADD `topic_id` int(11) unsigned NOT NULL default '0' AFTER `dept_id`; + +ALTER TABLE `%TABLE_PREFIX%help_topic` + ADD `sort` int(10) unsigned NOT NULL default '0' AFTER `form_id`; + +-- Add `content_id` to the content table to allow for translations +RENAME TABLE `%TABLE_PREFIX%page` TO `%TABLE_PREFIX%content`; +ALTER TABLE `%TABLE_PREFIX%content` + CHANGE `type` `type` varchar(32) NOT NULL default 'other', + ADD `content_id` int(10) unsigned NOT NULL default 0 AFTER `id`; + +UPDATE `%TABLE_PREFIX%content` + SET `content_id` = `id`; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%user_account`; +CREATE TABLE `%TABLE_PREFIX%user_account` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `org_id` int(11) unsigned NOT NULL, + `status` int(11) unsigned NOT NULL DEFAULT '0', + `timezone_id` int(11) NOT NULL DEFAULT '0', + `dst` tinyint(1) NOT NULL DEFAULT '1', + `lang` varchar(16) DEFAULT NULL, + `username` varchar(64) DEFAULT NULL, + `passwd` varchar(128) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL, + `backend` varchar(32) DEFAULT NULL, + `registered` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%organization`; +CREATE TABLE `%TABLE_PREFIX%organization` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL DEFAULT '', + `staff_id` int(10) unsigned NOT NULL DEFAULT '0', + `created` timestamp NULL DEFAULT NULL, + `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + +DELETE FROM `%TABLE_PREFIX%config` where `namespace`='core' + AND `key` = 'show_related_tickets'; + +-- Transfer access link template +INSERT INTO `%TABLE_PREFIX%content` + (`name`, `body`, `type`, `isactive`, `created`, `updated`) + SELECT A1.`subject`, A1.`body`, 'access-link', 1, A1.`created`, A1.`updated` + FROM `%TABLE_PREFIX%email_template` A1 + WHERE A1.`tpl_id` = (SELECT `value` FROM `%TABLE_PREFIX%config` A3 + WHERE A3.`key` = 'default_template_id' and `namespace` = 'core') + AND A1.`code_name` = 'user.accesslink'; + +UPDATE `%TABLE_PREFIX%content` SET `content_id` = LAST_INSERT_ID() + WHERE `id` = LAST_INSERT_ID(); + +-- Transfer staff password reset link +INSERT INTO `%TABLE_PREFIX%content` + (`name`, `body`, `type`, `isactive`, `created`, `updated`) + SELECT A1.`subject`, A1.`body`, 'pwreset-staff', 1, A1.`created`, A1.`updated` + FROM `%TABLE_PREFIX%email_template` A1 + WHERE A1.`tpl_id` = (SELECT `value` FROM `%TABLE_PREFIX%config` A3 + WHERE A3.`key` = 'default_template_id' and `namespace` = 'core') + AND A1.`code_name` = 'staff.pwreset'; + +UPDATE `%TABLE_PREFIX%content` SET `content_id` = LAST_INSERT_ID() + WHERE `id` = LAST_INSERT_ID(); + +-- No longer saved in the email_template table +DELETE FROM `%TABLE_PREFIX%email_template` + WHERE `code_name` IN ('staff.pwreset', 'user.accesslink'); + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `value` = '4323a6a81c35efbf7722b7fc4e475440' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/include/upgrader/streams/core/f5692e24-4323a6a8.task.php b/include/upgrader/streams/core/f5692e24-4323a6a8.task.php new file mode 100644 index 0000000000000000000000000000000000000000..566fab7a20ad09887f6e59cd30ff73c630a09f17 --- /dev/null +++ b/include/upgrader/streams/core/f5692e24-4323a6a8.task.php @@ -0,0 +1,37 @@ +<?php + + +class TemplateContentLoader extends MigrationTask { + var $description = "Loading initial system templates"; + + function run($max_time) { + foreach (array( + 'registration-staff', 'pwreset-staff', 'banner-staff', + 'registration-client', 'pwreset-client', 'banner-client', + 'registration-confirm', 'registration-thanks', + 'access-link') as $type) { + $i18n = new Internationalization(); + $tpl = $i18n->getTemplate("templates/page/{$type}.yaml"); + if (!($page = $tpl->getData())) + // No such template on disk + continue; + + if ($id = db_result(db_query('select id from '.PAGE_TABLE + .' where `type`='.db_input($type)))) + // Already have a template for the content type + continue; + + $sql = 'INSERT INTO '.PAGE_TABLE.' SET type='.db_input($type) + .', name='.db_input($page['name']) + .', body='.db_input($page['body']) + .', lang='.db_input($tpl->getLang()) + .', notes='.db_input($page['notes']) + .', created=NOW(), updated=NOW(), isactive=1'; + db_query($sql); + } + // Set the content_id for all the new items + db_query('UPDATE '.PAGE_TABLE + .' SET `content_id` = `id` WHERE `content_id` = 0'); + } +} +return 'TemplateContentLoader'; diff --git a/login.php b/login.php index 9e6d5f36f18d64384f96818ae82498a774553344..5d1faa1ecd24944e76a4b453731ffb164a425301 100644 --- a/login.php +++ b/login.php @@ -24,13 +24,44 @@ define('OSTCLIENTINC',TRUE); //make includes happy require_once(INCLUDE_DIR.'class.client.php'); require_once(INCLUDE_DIR.'class.ticket.php'); -$inc = 'login.inc.php'; -if ($_POST) { - if (!$_POST['lticket'] || !Validator::is_email($_POST['lemail'])) +if ($cfg->getClientRegistrationMode() == 'disabled' + || isset($_POST['lticket'])) + $inc = 'accesslink.inc.php'; +else + $inc = 'login.inc.php'; + +$suggest_pwreset = false; +if ($_POST && isset($_POST['luser'])) { + if (!$_POST['luser']) + $errors['err'] = 'Valid username or email address is required'; + elseif (($user = UserAuthenticationBackend::process($_POST['luser'], + $_POST['lpasswd'], $errors))) { + if ($user instanceof ClientCreateRequest) { + if ($cfg && $cfg->isClientRegistrationEnabled()) { + $inc = 'register.inc.php'; + $user_form = UserForm::getUserForm()->getForm($user->getInfo()); + } + else { + $errors['err'] = 'Access Denied. Contact your help desk + administrator to have an account registered for you'; + // fall through to show login page again + } + } + else { + Http::redirect($_SESSION['_client']['auth']['dest'] + ?: 'tickets.php'); + } + } elseif(!$errors['err']) { + $errors['err'] = 'Invalid username or password - try again!'; + } + $suggest_pwreset = true; +} +elseif ($_POST && isset($_POST['lticket'])) { + if (!Validator::is_email($_POST['lemail'])) $errors['err'] = 'Valid email address and ticket number required'; elseif (($user = UserAuthenticationBackend::process($_POST['lemail'], - $_POST['lticket'], $errors))) { - //We're using authentication backend so we can guard aganist brute + $_POST['lticket'], $errors))) { + // We're using authentication backend so we can guard aganist brute // force attempts (which doesn't buy much since the link is emailed) $user->sendAccessLink(); $msg = sprintf("%s - access link sent to your email!", @@ -41,8 +72,10 @@ if ($_POST) { } } -$nav = new UserNav(); -$nav->setActiveNav('status'); +if (!$nav) { + $nav = new UserNav(); + $nav->setActiveNav('status'); +} require CLIENTINC_DIR.'header.inc.php'; require CLIENTINC_DIR.$inc; require CLIENTINC_DIR.'footer.inc.php'; diff --git a/open.php b/open.php index 4cb7684a8167b7cd5a3c07f67d876b83c313cb4d..acb25fd70a9b94261f80ab1908eff404359d8f45 100644 --- a/open.php +++ b/open.php @@ -17,7 +17,7 @@ require('client.inc.php'); define('SOURCE','Web'); //Ticket source. $ticket = null; $errors=array(); -if($_POST): +if ($_POST) { $vars = $_POST; $vars['deptId']=$vars['emailId']=0; //Just Making sure we don't accept crap...only topicId is expected. if ($thisclient) { @@ -60,23 +60,30 @@ if($_POST): }else{ $errors['err']=$errors['err']?$errors['err']:'Unable to create a ticket. Please correct errors below and try again!'; } -endif; +} //page $nav->setActiveNav('new'); +if ($cfg->isClientLoginRequired()) { + if (!$thisclient) { + require_once 'secure.inc.php'; + } + elseif ($thisclient->isGuest()) { + require_once 'login.php'; + exit(); + } +} + require(CLIENTINC_DIR.'header.inc.php'); if($ticket && ( (($topic = $ticket->getTopic()) && ($page = $topic->getPage())) || ($page = $cfg->getThankYouPage()) - )) { //Thank the user and promise speedy resolution! - //Hide ticket number - it should only be delivered via email for security reasons. - echo Format::safe_html($ticket->replaceVars(str_replace( - array('%{ticket.number}', '%{ticket.extId}', '%{ticket}'), //ticket number vars. - array_fill(0, 3, 'XXXXXX'), - $page->getBody() - ))); -} else { + )) { + //Thank the user and promise speedy resolution! + echo Format::display($ticket->replaceVars($page->getBody())); +} +else { require(CLIENTINC_DIR.'open.inc.php'); } require(CLIENTINC_DIR.'footer.inc.php'); diff --git a/profile.php b/profile.php index e17bf690eb9241ff2b7bbad74efe8684bed447d2..47c100aef5fe46f44c1072e0248bc47058f9f047 100644 --- a/profile.php +++ b/profile.php @@ -23,7 +23,10 @@ $user = User::lookup($thisclient->getId()); if ($user && $_POST) { $errors = array(); - if ($user->updateInfo($_POST, $errors)) + if ($acct = $thisclient->getAccount()) { + $acct->update($_POST, $errors); + } + if (!$errors && $user->updateInfo($_POST, $errors)) Http::redirect('tickets.php'); } diff --git a/pwreset.php b/pwreset.php new file mode 100644 index 0000000000000000000000000000000000000000..6fb51b48728df6292ca5daddc24586434bca39c6 --- /dev/null +++ b/pwreset.php @@ -0,0 +1,85 @@ +<?php + +require_once('client.inc.php'); +if(!defined('INCLUDE_DIR')) die('Fatal Error'); +define('CLIENTINC_DIR',INCLUDE_DIR.'client/'); +define('OSTCLIENTINC',TRUE); //make includes happy + +require_once(INCLUDE_DIR.'class.client.php'); + +$inc = 'pwreset.request.php'; +if($_POST) { + if (!$ost->checkCSRFToken()) { + Http::response(400, 'Valid CSRF Token Required'); + exit; + } + switch ($_POST['do']) { + case 'sendmail': + if (($acct=ClientAccount::lookupByUsername($_POST['userid']))) { + if (!$acct->hasPassword()) { + $banner = 'Unable to reset password. Contact your administrator'; + } + elseif (!$acct->sendResetEmail()) { + $inc = 'pwreset.sent.php'; + } + } + else + $banner = 'Unable to verify username ' + .Format::htmlchars($_POST['userid']); + break; + case 'reset': + $inc = 'pwreset.login.php'; + $errors = array(); + if ($client = UserAuthenticationBackend::processSignOn($errors)) { + Http::redirect('index.php'); + } + elseif (isset($errors['msg'])) { + $banner = $errors['msg']; + } + break; + } +} +elseif ($_GET['token']) { + $banner = 'Re-enter your username or email'; + $_config = new Config('pwreset'); + if (($id = $_config->get($_GET['token'])) + && ($acct = ClientAccount::lookup(array('user_id'=>$id)))) { + if (!$acct->isConfirmed()) { + $inc = 'register.confirmed.inc.php'; + $acct->confirm(); + // TODO: Log the user in + if ($client = UserAuthenticationBackend::processSignOn($errors)) { + if ($acct->hasPassword() && !$acct->get('backend')) { + $acct->cancelResetTokens(); + } + // No password setup yet -- force one to be created + else { + $_SESSION['_client']['reset-token'] = $_GET['token']; + $acct->forcePasswdReset(); + } + Http::redirect('account.php?confirmed'); + } + } + else { + $inc = 'pwreset.login.php'; + } + } + elseif ($id && ($user = User::lookup($id))) + $inc = 'pwreset.create.php'; + else + Http::redirect('index.php'); +} +elseif ($cfg->allowPasswordReset()) { + $banner = 'Enter your username or email address below'; +} +else { + $_SESSION['_staff']['auth']['msg']='Password resets are disabled'; + return header('Location: index.php'); +} + +$nav = new UserNav(); +$nav->setActiveNav('status'); +require CLIENTINC_DIR.'header.inc.php'; +require CLIENTINC_DIR.$inc; +require CLIENTINC_DIR.'footer.inc.php'; +?> diff --git a/scp/ajax.php b/scp/ajax.php index 5bc266064fda0a468c766d093371efffa411d3c5..9ef736ef7999896da99e8c889e97dde6a7b682d2 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -42,7 +42,10 @@ $dispatcher = patterns('', url('^/content/', patterns('ajax.content.php:ContentAjaxAPI', url_get('^log/(?P<id>\d+)', 'log'), url_get('^ticket_variables', 'ticket_variables'), - url_get('^signature/(?P<type>\w+)(?:/(?P<id>\d+))?$', 'getSignature') + url_get('^signature/(?P<type>\w+)(?:/(?P<id>\d+))?$', 'getSignature'), + url_get('^(?P<id>\d+)/(?:(?P<lang>\w+)/)?manage$', 'manageContent'), + url_get('^(?P<id>[\w-]+)/(?:(?P<lang>\w+)/)?manage$', 'manageNamedContent'), + url_post('^(?P<id>\d+)(?:/(?P<lang>\w+))?$', 'updateContent') )), url('^/config/', patterns('ajax.config.php:ConfigAjaxAPI', url_get('^scp', 'scp') diff --git a/scp/css/scp.css b/scp/css/scp.css index ea1a1dddeec181a5c63271437c39f00925068394..4500c2dfbeb0a04c7820a08775f148bcef77c5be 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -12,6 +12,10 @@ a { text-decoration:none; } +.form_table a:hover { + text-decoration: underline; +} + .centered { text-align:center; } @@ -544,6 +548,7 @@ a.print { .form_table td { border-bottom:1px solid #ddd; + height: 20px; } table.fixed { diff --git a/scp/login.php b/scp/login.php index 20f53938e163334af2db7e8f83d2d0e450ac35fa..0505a9ce8d9d1a3dd099e06daa7600c41c7aa260 100644 --- a/scp/login.php +++ b/scp/login.php @@ -19,9 +19,11 @@ if(!defined('INCLUDE_DIR')) die('Fatal Error. Kwaheri!'); require_once(INCLUDE_DIR.'class.staff.php'); require_once(INCLUDE_DIR.'class.csrf.php'); +$content = Page::lookup(Page::getIdByType('banner-staff')); + $dest = $_SESSION['_staff']['auth']['dest']; $msg = $_SESSION['_staff']['auth']['msg']; -$msg = $msg?$msg:'Authentication Required'; +$msg = $msg ?: $content->getName(); $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest:'index.php'; $show_reset = false; if($_POST) { diff --git a/scp/pwreset.php b/scp/pwreset.php index b2826014c9b46b27572d3de6f3c6801c89183a6d..f5eed25fc0ed98e37cfa4d18dfc098c858bffe77 100644 --- a/scp/pwreset.php +++ b/scp/pwreset.php @@ -62,10 +62,11 @@ if($_POST) { } } elseif ($_GET['token']) { - $msg = 'Re-enter your username or email'; + $msg = 'Please enter your username or email'; $_config = new Config('pwreset'); if (($id = $_config->get($_GET['token'])) && ($staff = Staff::lookup($id))) + // TODO: Detect staff confirmation (for welcome email) $tpl = 'pwreset.login.php'; else header('Location: index.php'); diff --git a/scp/settings.php b/scp/settings.php index b72cdf2c377a436f8d188e11c8ea38f41937d708..a4243fd957fb780eea3b151fd96c8b9770ef9ccb 100644 --- a/scp/settings.php +++ b/scp/settings.php @@ -24,6 +24,8 @@ $settingOptions=array( array('Email Settings', 'settings.email'), 'pages' => array('Site Pages', 'settings.pages'), + 'access' => + array('Access Control', 'settings.access'), 'kb' => array('Knowledgebase Settings', 'settings.kb'), 'autoresp' => diff --git a/scp/staff.php b/scp/staff.php index b2d78f3cfb866d7b59596fd9e5f304292883165f..9a134e7e6376f9b0f99873b961e7ed44befabc94 100644 --- a/scp/staff.php +++ b/scp/staff.php @@ -31,7 +31,7 @@ if($_POST){ break; case 'create': if(($id=Staff::create($_POST,$errors))){ - $msg=Format::htmlchars($_POST['name']).' added successfully'; + $msg=Format::htmlchars($_POST['firstname']).' added successfully'; $_REQUEST['a']=null; }elseif(!$errors['err']){ $errors['err']='Unable to add staff. Correct any error(s) below and try again.'; diff --git a/secure.inc.php b/secure.inc.php index bf6a75b3e032590ca6ec89346e50c67393fd5514..8a0116306ae0fd6d5c50168c6a85feba6e80e110 100644 --- a/secure.inc.php +++ b/secure.inc.php @@ -20,7 +20,9 @@ require_once('client.inc.php'); //Client Login page: Ajax interface can pre-declare the function to trap logins. if(!function_exists('clientLoginPage')) { function clientLoginPage($msg ='') { - global $ost; + global $ost, $cfg, $nav; + $_SESSION['_client']['auth']['dest'] = + '/' . ltrim($_SERVER['REQUEST_URI'], '/'); require('./login.php'); exit; } diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 85f06999698f21093930575429e967ff7e28ab29..c15a946a6aa97c5e965a1ebebe6667f36b46b681 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -48,8 +48,8 @@ CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%faq_category` ( `name` varchar(125) default NULL, `description` TEXT NOT NULL, `notes` tinytext NOT NULL, - `created` date NOT NULL, - `updated` date NOT NULL, + `created` datetime NOT NULL, + `updated` datetime NOT NULL, PRIMARY KEY (`category_id`), KEY (`ispublic`) ) DEFAULT CHARSET=utf8; @@ -181,7 +181,7 @@ CREATE TABLE `%TABLE_PREFIX%department` ( `autoresp_email_id` int(10) unsigned NOT NULL default '0', `manager_id` int(10) unsigned NOT NULL default '0', `dept_name` varchar(128) NOT NULL default '', - `dept_signature` tinytext NOT NULL, + `dept_signature` text NOT NULL, `ispublic` tinyint(1) unsigned NOT NULL default '1', `group_membership` tinyint(1) NOT NULL default '0', `ticket_auto_response` tinyint(1) NOT NULL default '1', @@ -212,6 +212,7 @@ CREATE TABLE `%TABLE_PREFIX%email` ( `noautoresp` tinyint(1) unsigned NOT NULL default '0', `priority_id` tinyint(3) unsigned NOT NULL default '2', `dept_id` tinyint(3) unsigned NOT NULL default '0', + `topic_id` int(11) unsigned NOT NULL default '0', `email` varchar(255) NOT NULL default '', `name` varchar(255) NOT NULL default '', `userid` varchar(255) NOT NULL, @@ -243,6 +244,25 @@ CREATE TABLE `%TABLE_PREFIX%email` ( KEY `dept_id` (`dept_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%email_account`; +CREATE TABLE `%TABLE_PREFIX%email_account` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL, + `active` tinyint(1) NOT NULL DEFAULT '1', + `protocol` varchar(64) NOT NULL DEFAULT '', + `host` varchar(128) NOT NULL DEFAULT '', + `port` int(11) NOT NULL, + `username` varchar(128) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `options` varchar(512) DEFAULT NULL, + `errors` int(11) unsigned DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `updated` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP, + `lastconnect` timestamp NULL DEFAULT NULL, + `lasterror` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%filter`; CREATE TABLE `%TABLE_PREFIX%filter` ( `id` int(11) unsigned NOT NULL auto_increment, @@ -261,6 +281,7 @@ CREATE TABLE `%TABLE_PREFIX%filter` ( `team_id` int(10) unsigned NOT NULL default '0', `sla_id` int(10) unsigned NOT NULL default '0', `form_id` int(11) unsigned NOT NULL default '0', + `topic_id` int(11) unsigned NOT NULL default '0', `target` ENUM( 'Any', 'Web', 'Email', 'API' ) NOT NULL DEFAULT 'Any', `name` varchar(32) NOT NULL default '', `notes` text, @@ -385,6 +406,7 @@ CREATE TABLE `%TABLE_PREFIX%help_topic` ( `sla_id` int(10) unsigned NOT NULL default '0', `page_id` int(10) unsigned NOT NULL default '0', `form_id` int(10) unsigned NOT NULL default '0', + `sort` int(10) unsigned NOT NULL default '0', `topic` varchar(32) NOT NULL default '', `notes` text, `created` datetime NOT NULL, @@ -399,6 +421,16 @@ CREATE TABLE `%TABLE_PREFIX%help_topic` ( KEY `page_id` (`page_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%organization`; +CREATE TABLE `%TABLE_PREFIX%organization` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(128) NOT NULL DEFAULT '', + `staff_id` int(10) unsigned NOT NULL DEFAULT '0', + `created` timestamp NULL DEFAULT NULL, + `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%canned_response`; CREATE TABLE `%TABLE_PREFIX%canned_response` ( `canned_id` int(10) unsigned NOT NULL auto_increment, @@ -422,7 +454,7 @@ CREATE TABLE `%TABLE_PREFIX%session` ( `session_data` blob, `session_expire` datetime default NULL, `session_updated` datetime default NULL, - `user_id` int(10) unsigned NOT NULL default '0' COMMENT 'osTicket staff ID', + `user_id` varchar(16) NOT NULL default '0' COMMENT 'osTicket staff/client ID', `user_ip` varchar(64) NOT NULL, `user_agent` varchar(255) collate utf8_unicode_ci NOT NULL, PRIMARY KEY (`session_id`), @@ -445,7 +477,7 @@ CREATE TABLE `%TABLE_PREFIX%staff` ( `phone` varchar(24) NOT NULL default '', `phone_ext` varchar(6) default NULL, `mobile` varchar(24) NOT NULL default '', - `signature` tinytext NOT NULL, + `signature` text NOT NULL, `notes` text, `isactive` tinyint(1) NOT NULL default '1', `isadmin` tinyint(1) NOT NULL default '0', @@ -621,6 +653,7 @@ CREATE TABLE `%TABLE_PREFIX%ticket_thread` ( `source` varchar(32) NOT NULL default '', `title` varchar(255), `body` text NOT NULL, + `format` varchar(16) NOT NULL default 'html', `ip_address` varchar(64) NOT NULL default '', `created` datetime NOT NULL, `updated` datetime NOT NULL, @@ -686,10 +719,11 @@ INSERT INTO `%TABLE_PREFIX%timezone` (`id`, `offset`, `timezone`) VALUES (30, 12.0, 'Auckland, Wellington, Fiji, Kamchatka'); -- pages -CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%page` ( +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%content` ( `id` int(10) unsigned NOT NULL auto_increment, + `content_id` int(10) unsigned NOT NULL default '0', `isactive` tinyint(1) unsigned NOT NULL default '0', - `type` enum('landing','offline','thank-you','other') NOT NULL default 'other', + `type` varchar(32) NOT NULL default 'other', `name` varchar(255) NOT NULL, `body` text NOT NULL, `lang` varchar(16) NOT NULL default 'en_US', @@ -708,6 +742,7 @@ CREATE TABLE `%TABLE_PREFIX%plugin` ( `install_path` varchar(60) not null, `isphar` tinyint(1) not null default 0, `isactive` tinyint(1) not null default 0, + `version` varchar(64), `installed` datetime not null, primary key (`id`) ) DEFAULT CHARSET=utf8; @@ -731,3 +766,20 @@ CREATE TABLE `%TABLE_PREFIX%user_email` ( UNIQUE KEY `address` (`address`), KEY `user_email_lookup` (`user_id`) ) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%user_account`; +CREATE TABLE `%TABLE_PREFIX%user_account` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `org_id` int(11) unsigned NOT NULL, + `status` int(11) unsigned NOT NULL DEFAULT '0', + `timezone_id` int(11) NOT NULL DEFAULT '0', + `dst` tinyint(1) NOT NULL DEFAULT '1', + `lang` varchar(16) DEFAULT NULL, + `username` varchar(64) DEFAULT NULL, + `passwd` varchar(128) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL, + `backend` varchar(32) DEFAULT NULL, + `registered` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`) +) DEFAULT CHARSET=utf8; diff --git a/tickets.php b/tickets.php index 01d8b799c4220aa9ffc66ad7ce3e238ef2984925..9561da31dd62bd7ecbccb4f4b83326845b279e3f 100644 --- a/tickets.php +++ b/tickets.php @@ -16,6 +16,10 @@ **********************************************************************/ require('secure.inc.php'); if(!is_object($thisclient) || !$thisclient->isValid()) die('Access denied'); //Double check again. + +if ($thisclient->isGuest()) + $_REQUEST['id'] = $thisclient->getTicketId(); + require_once(INCLUDE_DIR.'class.ticket.php'); require_once(INCLUDE_DIR.'class.json.php'); $ticket=null; @@ -28,6 +32,9 @@ if($_REQUEST['id']) { } } +if (!$ticket && $thisclient->isGuest()) + Http::redirect('view.php'); + //Process post...depends on $ticket object above. if($_POST && is_object($ticket) && $ticket->getId()): $errors=array(); @@ -98,7 +105,7 @@ if($ticket && $ticket->checkUserAccess($thisclient)) { } else $inc='view.inc.php'; -} elseif($cfg->showRelatedTickets() && $thisclient->getNumTickets()) { +} elseif($thisclient->getNumTickets()) { $inc='tickets.inc.php'; } else { $nav->setActiveNav('new'); diff --git a/view.php b/view.php index b8590aab4f5ddaa25f004123dbfcbae950965a2a..abf7d805ac5acf5f82353ab66201ac787c9357bf 100644 --- a/view.php +++ b/view.php @@ -19,10 +19,15 @@ require_once('client.inc.php'); // Try autologin the user // Authenticated user can be of type ticket owner or collaborator $errors = array(); -$user = UserAuthenticationBackend::processSignOn($errors); +$user = UserAuthenticationBackend::processSignOn($errors, false); if ($user && $user->getTicketId()) Http::redirect('tickets.php?id='.$user->getTicketId()); -//Simply redirecting to tickets.php until multiview is implemented. -require('tickets.php'); +$nav = new UserNav(); +$nav->setActiveNav('status'); + +$inc = 'accesslink.inc.php'; +require CLIENTINC_DIR.'header.inc.php'; +require CLIENTINC_DIR.$inc; +require CLIENTINC_DIR.'footer.inc.php'; ?>