in ajax.php create an api for login in and return the right parameters... Make it simple and please don't care about security.
account.php:
<?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 <<PRESIDIO_ANONYMIZED_EMAIL_ADDRESS>>
    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('object_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::lookupByType('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);
    if ($thisclient) {
        $user_form->getField('email')->configure('disabled', true);
        $user_form->getField('email')->value = $thisclient->getEmail();
        $_POST['email'] = $thisclient->getEmail();
    }

    if (!$user_form->isValid(function($f) { return $f->isVisibleToUsers(); }))
        $errors['err'] = __('Incomplete client information');
    elseif (!$_POST['backend'] && !$_POST['passwd1'])
        $errors['passwd1'] = __('New password is required');
    elseif (!$_POST['backend'] && $_POST['passwd2'] != $_POST['passwd1'])
        $errors['passwd1'] = __('Passwords do not match');
    else {
        try {
            UserAccount::checkPassword($_POST['passwd1']);
        } catch (BadPassword $ex) {
             $errors['passwd1'] = $ex->getMessage();
        }
    }

    if ($errors)
        $errors['err'] = $errors['err'] ?: __('Unable to register account. See messages below');
    // 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(
            sprintf(__('Email already registered. Would you like to %1$s sign in %2$s?'),
            '<a href="login.php?e='.urlencode($addr).'" style="color:inherit"><strong>',
            '</strong></a>'));
        $errors['err'] = __('Unable to register account. See messages below');
    }
    elseif (!$addr)
        $errors['email'] = sprintf(__('%s is a required field'), $user_form->getField('email')->getLocal('label'));
    elseif (!$user_form->getField('name')->getClean())
        $errors['name'] = sprintf(__('%s is a required field'), $user_form->getField('name')->getLocal('label'));
    // Registration for existing users
    elseif ($addr && ($user = User::lookupByEmail($addr)) && !$user->updateInfo($_POST, $errors))
      $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'] = __('Unable to create new account.')
                .' '.__('Internal error occurred');
        elseif (!$acct->update($_POST, $errors))
            $errors['err'] = __('Errors configuring your profile. See messages below');
    }

    if (!$errors) {
        switch ($_POST['do']) {
        case 'create':
            $content = Page::lookupByType('registration-confirm');
            $inc = 'register.confirm.inc.php';
            $acct->sendConfirmEmail();
            break;
        case 'import':
            if ($bk = UserAuthenticationBackend::getBackend($_POST['backend'])) {
                $cl = new ClientSession(new EndUser($user));
                if (!$bk->supportsInteractiveAuthentication())
                    $acct->set('backend', null);
                $acct->confirm();
                if ($user = $bk->login($cl, $bk))
                    Http::redirect('tickets.php');
            }
            break;
        }
        // Rotate CSRF on successful POST
        $ost->getCSRF()->rotate();
    }

    if ($errors && $user && $user != $thisclient)
        $user->delete();
}

include(CLIENTINC_DIR.'header.inc.php');
include(CLIENTINC_DIR.$inc);
include(CLIENTINC_DIR.'footer.inc.php');

ajax.php:
<?php
/*********************************************************************
    ajax.php

    Ajax utils for client interface.

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/

function clientLoginPage($msg='Unauthorized') {
    Http::response(403,'Must login: '.Format::htmlchars($msg));
    exit;
}

require('client.inc.php');

if(!defined('INCLUDE_DIR'))	Http::response(500, 'Server configuration error');
require_once INCLUDE_DIR.'/class.dispatcher.php';
require_once INCLUDE_DIR.'/class.ajax.php';

$dispatcher = patterns('',
    url('^/config/', patterns('ajax.config.php:ConfigAjaxAPI',
        url_get('^client$', 'client')
    )),
    url('^/draft/', patterns('ajax.draft.php:DraftAjaxAPI',
        url_post('^(?P<id>\d+)$', 'updateDraftClient'),
        url_delete('^(?P<id>\d+)$', 'deleteDraftClient'),
        url_post('^(?P<id>\d+)/attach$', 'uploadInlineImageClient'),
        url_post('^(?P<namespace>[\w.]+)/attach$', 'uploadInlineImageEarlyClient'),
        url_get('^(?P<namespace>[\w.]+)$', 'getDraftClient'),
        url_post('^(?P<namespace>[\w.]+)$', 'createDraftClient')
    )),
    url('^/form/', patterns('ajax.forms.php:DynamicFormsAjaxAPI',
        url_get('^help-topic/(?P<id>\d+)$', 'getClientFormsForHelpTopic'),
        url_post('^upload/(\d+)?$', 'upload'),
        url_post('^upload/(\w+)?$', 'attach'),
        url_post('^upload/(?P<object>ticket|task)/(\w+)$', 'attach')
    )),
    url('^/i18n/(?P<lang>[\w_]+)/', patterns('ajax.i18n.php:i18nAjaxAPI',
        url_get('(?P<tag>\w+)$', 'getLanguageFile')
    ))
);
Signal::send('ajax.client', $dispatcher);
print $dispatcher->resolve(Osticket::get_path_info());
?>

login.php:
<?php
/*********************************************************************
    login.php

    User access link recovery

    TODO: This is a temp. fix to allow for collaboration in lieu of real
    username and password coming in 1.8.2

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
require_once('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');
require_once(INCLUDE_DIR.'class.ticket.php');

if ($cfg->getClientRegistrationMode() == 'disabled'
        || isset($_POST['lticket']))
    $inc = 'accesslink.inc.php';
else
    $inc = 'login.inc.php';

$suggest_pwreset = false;

// Check the CSRF token, and ensure that future requests will have to use a
// different CSRF token. This will help ward off both parallel and serial
// brute force attacks, because new tokens will have to be requested for
// each attempt.
if ($_POST) {
    // Check CSRF token
    if (!$ost->checkCSRFToken())
        Http::response(400, __('Valid CSRF Token Required'));

    // Rotate the CSRF token (original cannot be reused)
    $ost->getCSRF()->rotate();
}

if ($_POST && isset($_POST['luser'])) {
    if (!$_POST['luser'])
        $errors['err'] = __('Valid username or email address is required');
    elseif (($user = UserAuthenticationBackend::process(trim($_POST['luser']),
            substr($_POST['lpasswd'], 0, 128), $errors))) {
        if ($user instanceof ClientCreateRequest) {
            if ($cfg && $cfg->isClientRegistrationEnabled()) {
                // Attempt to automatically register
                if ($user->attemptAutoRegister())
                    Http::redirect('tickets.php');

                // Auto-registration failed. Show the user the info we have
                $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'] = sprintf('%s - %s', __('Invalid username or password'), __('Please 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))) {

        // If email address verification is not required, then provide
        // immediate access to the ticket!
        if (!$cfg->isClientEmailVerificationRequired())
            Http::redirect('tickets.php');

        // This will succeed as it is checked in the authentication backend
        $ticket = Ticket::lookupByNumber($_POST['lticket'], $_POST['lemail']);

        // We're using authentication backend so we can guard aganist brute
        // force attempts (which doesn't buy much since the link is emailed)
        if ($ticket) {
            $ticket->sendAccessLink($user);
            $msg = sprintf(__("%s - access link sent to your email!"),
                Format::htmlchars($user->getName()->getFirst()));
            $_POST = null;
        } else {
            $errors['err'] = sprintf('%s - %s',
                __('Invalid email or ticket number'),
                __('Please try again!'));
        }
    } elseif(!$errors['err']) {
        $errors['err'] = sprintf('%s - %s', __('Invalid email or ticket number'), __('Please try again!'));
    }
}
elseif (isset($_GET['do'])) {
    switch($_GET['do']) {
    case 'ext':
        // Lookup external backend
        if ($bk = UserAuthenticationBackend::getBackend($_GET['bk'])) {
            $result = $bk->triggerAuth();
            if ($result instanceof AccessDenied) {
                $errors['err'] = $result->getMessage();
            }
        }
    }
}
elseif ($user = UserAuthenticationBackend::processSignOn($errors, false)) {
    // Users from the ticket access link
    if ($user && $user instanceof TicketUser && $user->getTicketId())
        Http::redirect('tickets.php?id='.$user->getTicketId());
    // Users imported from an external auth backend
    elseif ($user instanceof ClientCreateRequest) {
        if ($cfg && $cfg->isClientRegistrationEnabled()) {
            // Attempt to automatically register
            if ($user->attemptAutoRegister())
                Http::redirect('tickets.php');

            // Unable to auto-register. Fill in what we have and let the
            // user complete the info
            $inc = 'register.inc.php';
        }
        else {
            $errors['err'] = __('Access Denied. Contact your help desk administrator to have an account registered for you');
            // fall through to show login page again
        }
    }
    elseif ($user instanceof AuthenticatedUser) {
        Http::redirect($_SESSION['_client']['auth']['dest']
                ?: 'tickets.php');
    }
}

if (!$nav) {
    $nav = new UserNav();
    $nav->setActiveNav('status');
}

require CLIENTINC_DIR.'header.inc.php';
require CLIENTINC_DIR.$inc;
require CLIENTINC_DIR.'footer.inc.php';
?>


client.inc.php:
<?php
/*********************************************************************
    client.inc.php

    File included on every client page

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__))) die('kwaheri rafiki!');

$thisdir=str_replace('\\', '/', dirname(__FILE__)).'/';
if(!file_exists($thisdir.'main.inc.php')) die('Fatal Error.');

require_once($thisdir.'main.inc.php');

if(!defined('INCLUDE_DIR')) die('Fatal error');

// Enforce ACL (if applicable)
if (!Validator::check_acl('client'))
    die(__('Access Denied'));

/*Some more include defines specific to client only */
define('CLIENTINC_DIR',INCLUDE_DIR.'client/');
define('OSTCLIENTINC',TRUE);

define('ASSETS_PATH',ROOT_PATH.'assets/default/');

//Check the status of the HelpDesk.
if (!in_array(strtolower(basename($_SERVER['SCRIPT_NAME'])), array('logo.php','file.php'))
        && !(is_object($ost) && $ost->isSystemOnline())) {
    include(ROOT_DIR.'offline.php');
    exit;
}

/* include what is needed on client stuff */
require_once(INCLUDE_DIR.'class.client.php');
require_once(INCLUDE_DIR.'class.ticket.php');
require_once(INCLUDE_DIR.'class.dept.php');

//clear some vars
$errors=array();
$msg='';
$nav=null;
//Make sure the user is valid..before doing anything else.
$thisclient = UserAuthenticationBackend::getUser();

if (isset($_GET['lang']) && $_GET['lang']) {
    Internationalization::setCurrentLanguage($_GET['lang']);
}

// Bootstrap gettext translations as early as possible, but after attempting
// to sign on the agent
TextDomain::configureForUser($thisclient);

//is the user logged in?
if($thisclient && $thisclient->getId() && $thisclient->isValid()){
     $thisclient->refreshSession();
} else {
    $thisclient = null;
}

/******* CSRF Protectin *************/
// Enforce CSRF protection for POSTS
if ($_POST  && !$ost->checkCSRFToken()) {
    Http::redirect('index.php');
    //just incase redirect fails
    die('Action denied (400)!');
}

//Add token to the header - used on ajax calls [DO NOT CHANGE THE NAME]
$ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'" />');

/* Client specific defaults */
define('PAGE_LIMIT', DEFAULT_PAGE_LIMIT);
define('SESSION_MAXLIFE', $thisclient ? $thisclient->getMaxIdleTime() :
        SESSION_TTL);

require(INCLUDE_DIR.'class.nav.php');
$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;
}
?>

class.user.php:
<?php
/*********************************************************************
    class.user.php

    External end-user identification for osTicket

    Peter Rotich <peter@osticket.com>
    Jared Hancock <<PRESIDIO_ANONYMIZED_EMAIL_ADDRESS>>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
require_once INCLUDE_DIR . 'class.orm.php';
require_once INCLUDE_DIR . 'class.util.php';
require_once INCLUDE_DIR . 'class.variable.php';
require_once INCLUDE_DIR . 'class.search.php';
require_once INCLUDE_DIR . 'class.organization.php';

class UserEmailModel extends VerySimpleModel {
    static $meta = array(
        'table' => USER_EMAIL_TABLE,
        'pk' => array('id'),
        'joins' => array(
            'user' => array(
                'constraint' => array('user_id' => 'UserModel.id')
            )
        )
    );

    function __toString() {
        return (string) $this->address;
    }

    static function getIdByEmail($email) {
        $row = UserEmailModel::objects()
            ->filter(array('address'=>$email))
            ->values_flat('user_id')
            ->first();

        return $row ? $row[0] : 0;
    }
}

class UserModel extends VerySimpleModel {
    static $meta = array(
        'table' => USER_TABLE,
        'pk' => array('id'),
        'select_related' => array('default_email', 'org', 'account'),
        'joins' => array(
            'emails' => array(
                'reverse' => 'UserEmailModel.user',
            ),
            'tickets' => array(
                'null' => true,
                'reverse' => 'Ticket.user',
            ),
            'account' => array(
                'list' => false,
                'null' => true,
                'reverse' => 'ClientAccount.user',
            ),
            'org' => array(
                'null' => true,
                'constraint' => array('org_id' => 'Organization.id')
            ),
            'default_email' => array(
                'null' => true,
                'constraint' => array('default_email_id' => 'UserEmailModel.id')
            ),
            'cdata' => array(
                'constraint' => array('id' => 'UserCdata.user_id'),
                'null' => true,
            ),
            'entries' => array(
                'constraint' => array(
                    'id' => 'DynamicFormEntry.object_id',
                    "'U'" => 'DynamicFormEntry.object_type',
                ),
                'list' => true,
            ),
        )
    );

    const PRIMARY_ORG_CONTACT   = 0x0001;

    const PERM_CREATE =     'user.create';
    const PERM_EDIT =       'user.edit';
    const PERM_DELETE =     'user.delete';
    const PERM_MANAGE =     'user.manage';
    const PERM_DIRECTORY =  'user.dir';

    static protected $perms = array(
        self::PERM_CREATE => array(
            'title' => /* @trans */ 'Create',
            'desc' => /* @trans */ 'Ability to add new users',
            'primary' => true,
        ),
        self::PERM_EDIT => array(
            'title' => /* @trans */ 'Edit',
            'desc' => /* @trans */ 'Ability to manage user information',
            'primary' => true,
        ),
        self::PERM_DELETE => array(
            'title' => /* @trans */ 'Delete',
            'desc' => /* @trans */ 'Ability to delete users',
            'primary' => true,
        ),
        self::PERM_MANAGE => array(
            'title' => /* @trans */ 'Manage Account',
            'desc' => /* @trans */ 'Ability to manage active user accounts',
            'primary' => true,
        ),
        self::PERM_DIRECTORY => array(
            'title' => /* @trans */ 'User Directory',
            'desc' => /* @trans */ 'Ability to access the user directory',
            'primary' => true,
        ),
    );

    function getId() {
        return $this->id;
    }

    function getDefaultEmailAddress() {
        return $this->getDefaultEmail()->address;
    }

    function getDefaultEmail() {
        return $this->default_email;
    }

    function hasAccount() {
        return !is_null($this->account);
    }
    function getAccount() {
        return $this->account;
    }

    function getOrgId() {
         return $this->get('org_id');
    }

    function getOrganization() {
        return $this->org;
    }

    function setOrganization($org, $save=true) {

        $this->set('org', $org);

        if ($save)
            $this->save();

        return true;
    }

    public function setFlag($flag, $val) {
        if ($val)
            $this->status |= $flag;
        else
            $this->status &= ~$flag;
    }

    protected function hasStatus($flag) {
        return $this->get('status') & $flag !== 0;
    }

    protected function clearStatus($flag) {
        return $this->set('status', $this->get('status') & ~$flag);
    }

    protected function setStatus($flag) {
        return $this->set('status', $this->get('status') | $flag);
    }

    function isPrimaryContact() {
        return $this->hasStatus(User::PRIMARY_ORG_CONTACT);
    }

    function setPrimaryContact($flag) {
        if ($flag)
            $this->setStatus(User::PRIMARY_ORG_CONTACT);
        else
            $this->clearStatus(User::PRIMARY_ORG_CONTACT);
    }

    static function getPermissions() {
        return self::$perms;
    }
}
include_once INCLUDE_DIR.'class.role.php';
RolePermission::register(/* @trans */ 'Users', UserModel::getPermissions());

class UserCdata extends VerySimpleModel {
    static $meta = array(
        'table' => USER_CDATA_TABLE,
        'pk' => array('user_id'),
        'joins' => array(
            'user' => array(
                'constraint' => array('user_id' => 'UserModel.id'),
            ),
        ),
    );
}

class User extends UserModel
implements TemplateVariable, Searchable {

    var $_email;
    var $_entries;
    var $_forms;
    var $_queue;



    static function fromVars($vars, $create=true, $update=false) {
        // Try and lookup by email address
        $user = static::lookupByEmail($vars['email']);
        if (!$user
                // can create user?
                && $create
                // Make sure at least email is valid
                && Validator::is_email($vars['email'])) {
            $name = $vars['name'];
            if (is_array($name))
                $name = implode(', ', $name);
            elseif (!$name)
                list($name) = explode('@', $vars['email'], 2);

            $user = new User(array(
                'name' => Format::htmldecode(Format::sanitize($name, false)),
                'created' => new SqlFunction('NOW'),
                'updated' => new SqlFunction('NOW'),
                //XXX: Do plain create once the cause
                // of the detached emails is fixed.
                'default_email' => UserEmail::ensure($vars['email'])
            ));
            // Is there an organization registered for this domain
            list($mailbox, $domain) = explode('@', $vars['email'], 2);
            if (isset($vars['org_id']))
                $user->set('org_id', $vars['org_id']);
            elseif ($org = Organization::forDomain($domain))
                $user->setOrganization($org, false);

            try {
                $user->save(true);
                $user->emails->add($user->default_email);
                // Attach initial custom fields
                $user->addDynamicData($vars);
            }
            catch (OrmException $e) {
                return null;
            }
            $type = array('type' => 'created');
            Signal::send('object.created', $user, $type);
            Signal::send('user.created', $user);
        } elseif ($update && $user) {
            $errors = array();
            $user->updateInfo($vars, $errors, true);
        }

        return $user;
    }

    static function fromForm($form, $create=true) {
        global $thisstaff;

        if(!$form) return null;

        //Validate the form
        $valid = true;
        $filter = function($f) use ($thisstaff) {
            return !isset($thisstaff) || $f->isRequiredForStaff() || $f->isVisibleToStaff();
        };
        if (!$form->isValid($filter))
            $valid  = false;

        //Make sure the email is not in-use
        if (($field=$form->getField('email'))
                && $field->getClean()
                && User::lookup(array('emails__address'=>$field->getClean()))) {
            $field->addError(__('Email is assigned to another user'));
            $valid = false;
        }

        return $valid ? self::fromVars($form->getClean(), $create) : null;
    }

    function getEmail() {

        if (!isset($this->_email))
            $this->_email = new EmailAddress(sprintf('"%s" <%s>',
                    addcslashes($this->getName(), '"'),
                    $this->default_email->address));

        return $this->_email;
    }

    function getAvatar($size=null) {
        global $cfg;
        $source = $cfg->getClientAvatarSource();
        $avatar = $source->getAvatar($this);
        if (isset($size))
            $avatar->setSize($size);
        return $avatar;
    }

    function getFullName() {
        return $this->name;
    }

    function getPhoneNumber() {
        foreach ($this->getDynamicData() as $e)
            if ($a = $e->getAnswer('phone'))
                return $a;
    }

    function getName() {
        if (!$this->name)
            list($name) = explode('@', $this->getDefaultEmailAddress(), 2);
        else
            $name = $this->name;
        return new UsersName($name);
    }

    function getUpdateDate() {
        return $this->updated;
    }

    function getCreateDate() {
        return $this->created;
    }

    function getTimezone() {
        global $cfg;

        if (($acct = $this->getAccount()) && ($tz = $acct->getTimezone())) {
            return $tz;
        }
        return $cfg->getDefaultTimezone();
    }

    function addForm($form, $sort=1, $data=null) {
        $entry = $form->instanciate($sort, $data);
        $entry->set('object_type', 'U');
        $entry->set('object_id', $this->getId());
        $entry->save();
        return $entry;
    }

    function getLanguage($flags=false) {
        if ($acct = $this->getAccount())
            return $acct->getLanguage($flags);
    }

    function to_json() {

        $info = array(
                'id'  => $this->getId(),
                'name' => Format::htmlchars($this->getName()),
                'email' => (string) $this->getEmail(),
                'phone' => (string) $this->getPhoneNumber());

        return Format::json_encode($info);
    }

    function __toString() {
        return $this->asVar();
    }

    function asVar() {
        return (string) $this->getName();
    }

    function getVar($tag) {
        $tag = mb_strtolower($tag);
        foreach ($this->getDynamicData() as $e)
            if ($a = $e->getAnswer($tag))
                return $a;
    }

    static function getVarScope() {
        $base = array(
            'email' => array(
                'class' => 'EmailAddress', 'desc' => __('Default email address')
            ),
            'name' => array(
                'class' => 'PersonsName', 'desc' => 'User name, default format'
            ),
            'organization' => array('class' => 'Organization', 'desc' => __('Organization')),
        );
        $extra = VariableReplacer::compileFormScope(UserForm::getInstance());
        return $base + $extra;
    }

    static function getSearchableFields() {
        $base = array();
        $uform = UserForm::getUserForm();
        $base = array();
        foreach ($uform->getFields() as $F) {
            $fname = $F->get('name') ?: ('field_'.$F->get('id'));
            # XXX: email in the model corresponds to `emails__address` ORM path
            if ($fname == 'email')
                $fname = 'emails__address';
            if (!$F->hasData() || $F->isPresentationOnly())
                continue;
            if (!$F->isStorable())
                $base[$fname] = $F;
            else
                $base["cdata__{$fname}"] = $F;
        }
        return $base;
    }

    static function supportsCustomData() {
        return true;
    }

    function addDynamicData($data) {
        return $this->addForm(UserForm::objects()->one(), 1, $data);
    }

    function getDynamicData($create=true) {
        if (!isset($this->_entries)) {
            $this->_entries = DynamicFormEntry::forObject($this->id, 'U')->all();
            if (!$this->_entries && $create) {
                $g = UserForm::getNewInstance();
                $g->setClientId($this->id);
                $g->save();
                $this->_entries[] = $g;
            }
        }

        return $this->_entries ?: array();
    }

    function getFilterData() {
        $vars = array();
        foreach ($this->getDynamicData() as $entry) {
            $vars += $entry->getFilterData();

            // Add in special `name` and `email` fields
            if ($entry->getDynamicForm()->get('type') != 'U')
                continue;

            foreach (array('name', 'email') as $name) {
                if ($f = $entry->getField($name))
                    $vars['field.'.$f->get('id')] =
                        $name == 'name' ? $this->getName() : $this->getEmail();
            }
        }

        return $vars;
    }

    function getForms($data=null, $cb=null) {

        if (!isset($this->_forms)) {
            $this->_forms = array();
            $cb = $cb ?: function ($f) use($data) { return ($data); };
            foreach ($this->getDynamicData() as $entry) {
                $entry->addMissingFields();
                if(($form = $entry->getDynamicForm())
                        && $form->get('type') == 'U' ) {

                    foreach ($entry->getFields() as $f) {
                        if ($f->get('name') == 'name' && !$cb($f))
                            $f->value = $this->getFullName();
                        elseif ($f->get('name') == 'email' && !$cb($f))
                            $f->value = $this->getEmail();
                    }
                }

                $this->_forms[] = $entry;
            }
        }

        return $this->_forms;
    }

    function getAccountStatus() {

        if (!($account=$this->getAccount()))
            return __('Guest');

        return (string) $account->getStatus();
    }

    function canSeeOrgTickets() {
        return $this->org && (
                $this->org->shareWithEverybody()
            || ($this->isPrimaryContact() && $this->org->shareWithPrimaryContacts()));
    }

    function register($vars, &$errors) {

        // user already registered?
        if ($this->getAccount())
            return true;

        return UserAccount::register($this, $vars, $errors);
    }

    static function importCsv($stream, $defaults=array()) {
        require_once INCLUDE_DIR . 'class.import.php';

        $importer = new CsvImporter($stream);
        $imported = 0;
        try {
            db_autocommit(false);
            $records = $importer->importCsv(UserForm::getUserForm()->getFields(), $defaults);
            foreach ($records as $data) {
                if (!Validator::is_email($data['email']) || empty($data['name']))
                    throw new ImportError('Both `name` and `email` fields are required');
                if (!($user = static::fromVars($data, true, true)))
                    throw new ImportError(sprintf(__('Unable to import user: %s'),
                        print_r(Format::htmlchars($data), true)));
                $imported++;
            }
            db_autocommit(true);
        }
        catch (Exception $ex) {
            db_rollback();
            return $ex->getMessage();
        }
        return $imported;
    }

    static function importFromPost($stream, $extra=array()) {
        if (!is_array($stream))
            $stream = sprintf('name, email%s %s',PHP_EOL, $stream);

        return User::importCsv($stream, $extra);
    }

    function updateInfo($vars, &$errors, $staff=false) {
        $isEditable = function ($f) use($staff) {
            return ($staff ? $f->isEditableToStaff() :
                    $f->isEditableToUsers());
        };
        $valid = true;
        $forms = $this->getForms($vars, $isEditable);
        foreach ($forms as $entry) {
            $entry->setSource($vars);
            if ($staff && !$entry->isValidForStaff(true))
                $valid = false;
            elseif (!$staff && !$entry->isValidForClient(true))
                $valid = false;
            elseif ($entry->getDynamicForm()->get('type') == 'U'
                    && ($f=$entry->getField('email'))
                    && $isEditable($f)
                    && $f->getClean()
                    && ($u=User::lookup(array('emails__address'=>$f->getClean())))
                    && $u->id != $this->getId()) {
                $valid = false;
                $f->addError(__('Email is assigned to another user'));
            }

            if (!$valid)
                $errors = array_merge($errors, $entry->errors());
        }


        if (!$valid)
            return false;

        // Save the entries
        foreach ($forms as $entry) {
            $fields = $entry->getFields();
            foreach ($fields as $field) {
                $changes = $field->getChanges();
                if ((is_array($changes) && $changes[0]) || $changes && !is_array($changes)) {
                    $type = array('type' => 'edited', 'key' => $field->getLabel());
                    Signal::send('object.edited', $this, $type);
                }
            }

            if ($entry->getDynamicForm()->get('type') == 'U') {
                //  Name field
                if (($name = $entry->getField('name')) && $isEditable($name) ) {
                    $name = $name->getClean();
                    if (is_array($name))
                        $name = implode(', ', $name);
                    if ($this->name != $name) {
                        $type = array('type' => 'edited', 'key' => 'Name');
                        Signal::send('object.edited', $this, $type);
                    }
                    $this->name = $name;
                }

                // Email address field
                if (($email = $entry->getField('email'))
                        && $isEditable($email)) {
                    if ($this->default_email->address != $email->getClean()) {
                        $type = array('type' => 'edited', 'key' => 'Email');
                        Signal::send('object.edited', $this, $type);
                    }
                    $this->default_email->address = $email->getClean();
                    $this->default_email->save();
                }
            }

            // DynamicFormEntry::saveAnswers returns the number of answers updated
            if ($entry->saveAnswers($isEditable)) {
                $this->updated = SqlFunction::NOW();
            }
        }

        return $this->save();
    }


    function save($refetch=false) {
        // Drop commas and reorganize the name without them
        $parts = array_map('trim', explode(',', $this->name));
        switch (count($parts)) {
            case 2:
                // Assume last, first --or-- last suff., first
                $this->name = $parts[1].' '.$parts[0];
                // XXX: Consider last, first suff.
                break;
            case 3:
                // Assume last, first, suffix, write 'first last suffix'
                $this->name = $parts[1].' '.$parts[0].' '.$parts[2];
                break;
        }

        // Handle email addresses -- use the box name
        if (Validator::is_email($this->name)) {
            list($box, $domain) = explode('@', $this->name, 2);
            if (strpos($box, '.') !== false)
                $this->name = str_replace('.', ' ', $box);
            else
                $this->name = $box;
            $this->name = mb_convert_case($this->name, MB_CASE_TITLE);
        }

        if (count($this->dirty)) //XXX: doesn't work??
            $this->set('updated', new SqlFunction('NOW'));
        return parent::save($refetch);
    }

    function delete() {
        // Refuse to delete a user with tickets
        if ($this->tickets->count())
            return false;

        // Delete account record (if any)
        if ($this->getAccount())
            $this->getAccount()->delete();

        // Delete emails.
        $this->emails->expunge();

        // Drop dynamic data
        foreach ($this->getDynamicData() as $entry) {
            $entry->delete();
        }

        $type = array('type' => 'deleted');
        Signal::send('object.deleted', $this, $type);

        // Delete user
        return parent::delete();
    }

    function deleteAllTickets() {
        $status_id = TicketStatus::lookup(array('state' => 'deleted'));
        foreach($this->tickets as $ticket) {
            if (!$T = Ticket::lookup($ticket->getId()))
                continue;
            if (!$T->setStatus($status_id))
                return false;
        }
        $this->tickets->reset();
        return true;
    }

    static function lookupByEmail($email) {
        return static::lookup(array('emails__address'=>$email));
    }

    static function getNameById($id) {
        if ($user = static::lookup($id))
            return $user->getName();
    }

    static function getLink($id) {
        global $thisstaff;

        if (!$id || !$thisstaff)
            return false;

        return ROOT_PATH . sprintf('scp/users.php?id=%s', $id);
    }

    function getTicketsQueue($collabs=true) {
        global $thisstaff;

        if (!$this->_queue) {
            $email = $this->getDefaultEmailAddress();
            $filter = [
                ['user__id', 'equal', $this->getId()],
            ];
            if ($collabs)
                $filter = [
                    ['user__emails__address', 'equal', $email],
                    ['thread__collaborators__user__emails__address', 'equal',  $email],
                ];
            $this->_queue = new AdhocSearch(array(
                'id' => 'adhoc,uid'.$this->getId(),
                'root' => 'T',
                'staff_id' => $thisstaff->getId(),
                'title' => $this->getName()
            ));
            $this->_queue->config = $filter;
        }

        return $this->_queue;
    }
}

class EmailAddress
implements TemplateVariable {
    var $email;
    var $address;
    protected $_info;

    function __construct($address) {
        $this->_info = self::parse($address);
        $this->email = sprintf('%s@%s',
                $this->getMailbox(),
                $this->getDomain());

        if ($this->getName())
            $this->address = sprintf('"%s" <%s>',
                    $this->getName(),
                    $this->email);
        else
             $this->address =  $this->email;
    }

    function __toString() {
        return (string) $this->email;
    }

    function getVar($what) {

        if (!isset($this->_info))
            return '';

        switch ($what) {
        case 'host':
        case 'domain':
            return $this->_info->host;
        case 'personal':
            return trim($this->_info->personal, '"');
        case 'mailbox':
            return $this->_info->mailbox;
        }
    }

    function getAddress() {
        return $this->address ?: $this->getEmail();
    }

    function getEmail() {
        return $this->email;
    }

    function getHost() {
        return $this->getVar('host');
    }

    function getDomain() {
        return $this->getHost();
    }

    function getName() {
        return $this->getVar('personal');
    }

    function getMailbox() {
        return $this->getVar('mailbox');
    }

    // Parse and email adddress (RFC822) into it's parts.
    // @address - one address is expected
    static function parse($address) {
        require_once PEAR_DIR . 'PEAR.php';
        if (($parts = Mail_Parse::parseAddressList($address))
                && !PEAR::isError($parts))
            return current($parts);
    }

    static function getVarScope() {
        return array(
            'domain' => __('Domain'),
            'mailbox' => __('Mailbox'),
            'personal' => __('Personal name'),
        );
    }
}

class PersonsName
implements TemplateVariable {
    var $format;
    var $parts;
    var $name;

    static $formats = array(
        'first' => array(     /*@trans*/ "First", 'getFirst'),
        'last' => array(      /*@trans*/ "Last", 'getLast'),
        'full' => array(      /*@trans*/ "First Last", 'getFull'),
        'legal' => array(     /*@trans*/ "First M. Last", 'getLegal'),
        'lastfirst' => array( /*@trans*/ "Last, First", 'getLastFirst'),
        'formal' => array(    /*@trans*/ "Mr. Last", 'getFormal'),
        'short' => array(     /*@trans*/ "First L.", 'getShort'),
        'shortformal' => array(/*@trans*/ "F. Last", 'getShortFormal'),
        'complete' => array(  /*@trans*/ "Mr. First M. Last Sr.", 'getComplete'),
        'original' => array(  /*@trans*/ '-- As Entered --', 'getOriginal'),
    );

    function __construct($name, $format=null) {
        global $cfg;

        if ($format && isset(static::$formats[$format]))
            $this->format = $format;
        else
            $this->format = 'original';

        if (!is_array($name)) {
            $this->parts = static::splitName($name);
            $this->name = $name;
        }
        else {
            $this->parts = $name;
            $this->name = implode(' ', $name);
        }
    }

    function getFirst() {
        return $this->parts['first'];
    }

    function getLast() {
        return $this->parts['last'];
    }

    function getMiddle() {
        return $this->parts['middle'];
    }

    function getFirstInitial() {
        if ($this->parts['first'])
            return mb_substr($this->parts['first'],0,1).'.';
        return '';
    }

    function getMiddleInitial() {
        if ($this->parts['middle'])
            return mb_substr($this->parts['middle'],0,1).'.';
        return '';
    }

    function getLastInitial() {
        if ($this->parts['last'])
            return mb_substr($this->parts['last'],0,1).'.';
        return '';
    }

    function getFormal() {
        return trim($this->parts['salutation'].' '.$this->parts['last']);
    }

    function getFull() {
        return trim($this->parts['first'].' '.$this->parts['last']);
    }

    function getLegal() {
        $parts = array(
            $this->parts['first'],
            $this->getMiddleInitial(),
            $this->parts['last'],
        );
        return implode(' ', array_filter($parts));
    }

    function getComplete() {
        $parts = array(
            $this->parts['salutation'],
            $this->parts['first'],
            $this->getMiddleInitial(),
            $this->parts['last'],
            $this->parts['suffix']
        );
        return implode(' ', array_filter($parts));
    }

    function getLastFirst() {
        $name = $this->parts['last'].', '.$this->parts['first'];
        $name = trim($name, ', ');
        if ($this->parts['suffix'])
            $name .= ', '.$this->parts['suffix'];
        return $name;
    }

    function getShort() {
        return $this->parts['first'].' '.$this->getLastInitial();
    }

    function getShortFormal() {
        return $this->getFirstInitial().' '.$this->parts['last'];
    }

    function getOriginal() {
        return $this->name;
    }

    function getInitials() {
        $names = array($this->parts['first']);
        $names = array_merge($names, explode(' ', $this->parts['middle']));
        $names[] = $this->parts['last'];
        $initials = '';
        foreach (array_filter($names) as $n)
            $initials .= mb_substr($n,0,1);
        return mb_convert_case($initials, MB_CASE_UPPER);
    }

    function getName() {
        return $this;
    }

    function getNameFormats($user, $type) {
      $nameFormats = array();

      foreach (PersonsName::allFormats() as $format => $func) {
          $nameFormats[$type . '.name.' . $format] = $user->getName()->$func[1]();
      }

      return $nameFormats;
    }

    function asVar() {
        return $this->__toString();
    }

    static function getVarScope() {
        $formats = array();
        foreach (static::$formats as $name=>$info) {
            if (in_array($name, array('original', 'complete')))
                continue;
            $formats[$name] = $info[0];
        }
        return $formats;
    }

    function __toString() {

        @list(, $func) = static::$formats[$this->format];
        if (!$func) $func = 'getFull';

        return (string) call_user_func(array($this, $func));
    }

    static function allFormats() {
        return static::$formats;
    }

    /**
     * Thanks, http://stackoverflow.com/a/14420217
     */
    static function splitName($name) {
        $results = array();

        $r = explode(' ', $name);
        $size = count($r);

        //check if name is bad format (ex: J.Everybody), and fix them
        if($size==1 && mb_strpos($r[0], '.') !== false)
        {
            $r = explode('.', $name);
            $size = count($r);
        }

        //check first for period, assume salutation if so
        if (mb_strpos($r[0], '.') === false)
        {
            $results['salutation'] = '';
            $results['first'] = $r[0];
        }
        else
        {
            $results['salutation'] = $r[0];
            $results['first'] = $r[1];
        }

        //check last for period, assume suffix if so
        if (mb_strpos($r[$size - 1], '.') === false)
        {
            $results['suffix'] = '';
        }
        else
        {
            $results['suffix'] = $r[$size - 1];
        }

        //combine remains into last
        $start = ($results['salutation']) ? 2 : 1;
        $end = ($results['suffix']) ? $size - 2 : $size - 1;

        $middle = array();
        for ($i = $start; $i <= $end; $i++)
        {
            $middle[] = $r[$i];
        }
        if (count($middle) > 1) {
            $results['last'] = array_pop($middle);
            $results['middle'] = implode(' ', $middle);
        }
        else {
            $results['last'] = $middle[0];
            $results['middle'] = '';
        }

        return $results;
    }

}

class AgentsName extends PersonsName {
    function __construct($name, $format=null) {
        global $cfg;

        if (!$format && $cfg)
            $format = $cfg->getAgentNameFormat();

        parent::__construct($name, $format);
    }
}

class UsersName extends PersonsName {
    function __construct($name, $format=null) {
        global $cfg;
        if (!$format && $cfg)
            $format = $cfg->getClientNameFormat();

        parent::__construct($name, $format);
    }
}


class UserEmail extends UserEmailModel {
    static function ensure($address) {
        $email = static::lookup(array('address'=>$address));
        if (!$email) {
            $email = new static(array('address'=>$address));
            $email->save();
        }
        return $email;
    }
}


class UserAccount extends VerySimpleModel {
    static $meta = array(
        'table' => USER_ACCOUNT_TABLE,
        'pk' => array('id'),
        'joins' => array(
            'user' => array(
                'null' => false,
                'constraint' => array('user_id' => 'User.id')
            ),
        ),
    );

    const LANG_MAILOUTS = 1;            // Language preference for mailouts

    var $_status;
    var $_extra;

    function getStatus() {
        if (!isset($this->_status))
            $this->_status = new UserAccountStatus($this->get('status'));
        return $this->_status;
    }

    function statusChanged($flag, $var) {
        if (($this->hasStatus($flag) && !$var) ||
            (!$this->hasStatus($flag) && $var))
                return true;
    }

    protected function hasStatus($flag) {
        return $this->getStatus()->check($flag);
    }

    protected function clearStatus($flag) {
        return $this->set('status', $this->get('status') & ~$flag);
    }

    protected function setStatus($flag) {
        return $this->set('status', $this->get('status') | $flag);
    }

    function confirm() {
        $this->setStatus(UserAccountStatus::CONFIRMED);
        return $this->save();
    }

    function isConfirmed() {
        return $this->getStatus()->isConfirmed();
    }

    function lock() {
        $this->setStatus(UserAccountStatus::LOCKED);
        return $this->save();
    }

    function unlock() {
        $this->clearStatus(UserAccountStatus::LOCKED);
        return $this->save();
    }

    function isLocked() {
        return $this->getStatus()->isLocked();
    }

    function isActive() {
        return (!$this->isLocked() && $this->isConfirmed());
    }

    function forcePasswdReset() {
        $this->setStatus(UserAccountStatus::REQUIRE_PASSWD_RESET);
        return $this->save();
    }

    function isPasswdResetForced() {
        return $this->hasStatus(UserAccountStatus::REQUIRE_PASSWD_RESET);
    }

    function isPasswdResetEnabled() {
        return !$this->hasStatus(UserAccountStatus::FORBID_PASSWD_RESET);
    }

    function getInfo() {
        return $this->ht;
    }

    function getId() {
        return $this->get('id');
    }

    function getUserId() {
        return $this->get('user_id');
    }

    function getUser() {
        return $this->user;
    }

    function getUserName() {
        return $this->getUser()->getName();
    }

    function getExtraAttr($attr=false, $default=null) {
        if (!isset($this->_extra))
            $this->_extra = JsonDataParser::decode($this->get('extra', ''));

        return $attr ? (@$this->_extra[$attr] ?: $default) : $this->_extra;
    }

    function setExtraAttr($attr, $value) {
        $this->getExtraAttr();
        $this->_extra[$attr] = $value;
    }

    /**
     * Function: getLanguage
     *
     * Returns the language preference for the user or false if no
     * preference is defined. False indicates the browser indicated
     * preference should be used. For requests apart from browser requests,
     * the last language preference of the browser is set in the
     * 'browser_lang' extra attribute upon logins. Send the LANG_MAILOUTS
     * flag to also consider this saved value. Such is useful when sending
     * the user a message (such as an email), and the user's browser
     * preference is not available in the HTTP request.
     *
     * Parameters:
     * $flags - (int) Send UserAccount::LANG_MAILOUTS if the user's
     *      last-known browser preference should be considered. Normally
     *      only the user's saved language preference is considered.
     *
     * Returns:
     * Current or last-known language preference or false if no language
     * preference is currently set or known.
     */
    function getLanguage($flags=false) {
        $lang = $this->get('lang', false);
        if (!$lang && ($flags & UserAccount::LANG_MAILOUTS))
            $lang = $this->getExtraAttr('browser_lang', false);

        return $lang;
    }

    function getTimezone() {
        return $this->timezone;
    }

    function save($refetch=false) {
        // Serialize the extra column on demand
        if (isset($this->_extra)) {
            $this->extra = JsonDataEncoder::encode($this->_extra);
        }
        return parent::save($refetch);
    }

    function hasPassword() {
        return (bool) $this->get('passwd');
    }

    function sendResetEmail() {
        return $this->sendUnlockEmail('pwreset-client') === true;
    }

    function sendConfirmEmail() {
        return $this->sendUnlockEmail('registration-client') === true;
    }

    function setPassword($new) {
        $this->set('passwd', Passwd::hash($new));
        // Clean sessions
        Signal::send('auth.clean', $this->getUser());
    }

    protected function sendUnlockEmail($template) {
        global $ost, $cfg;

        $token = Misc::randCode(48); // 290-bits

        $email = $cfg->getDefaultEmail();
        $content = Page::lookupByType($template);

        if (!$email ||  !$content)
            return new BaseError(sprintf(_S('%s: Unable to retrieve template'),
                $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->getUser(), $info);

        $lang = $this->getLanguage(UserAccount::LANG_MAILOUTS);
        $msg = $ost->replaceTemplateVariables(array(
            'subj' => $content->getLocalName($lang),
            'body' => $content->getLocalBody($lang),
        ), $vars);

        $_config = new Config('pwreset');
        $_config->set($vars['token'], 'c'.$this->getUser()->getId());

        $email->send($this->getUser()->getEmail(),
            Format::striptags($msg['subj']), $msg['body']);

        return true;
    }

    function __toString() {
        return (string) $this->getStatus();
    }

    /*
     * Updates may be done by Staff or by the User if registration
     * options are set to Public
     */
    function update($vars, &$errors) {
        // TODO: Make sure the username is unique

        // Timezone selection is not required. System default is a valid
        // fallback

        // Changing password?
        if ($vars['passwd1'] || $vars['passwd2']) {
            if (!$vars['passwd1'])
                $errors['passwd1'] = __('New password is required');
            else {
                try {
                    self::checkPassword($vars['passwd1']);
                } catch (BadPassword $ex) {
                    $errors['passwd1'] =  $ex->getMessage();
                }
            }
        }

        // Make sure the username is not an email.
        if ($vars['username'] && Validator::is_email($vars['username']))
            $errors['username'] =
                __('Users can always sign in with their email address');

        if ($errors) return false;

        //flags
        $pwreset = $this->statusChanged(UserAccountStatus::REQUIRE_PASSWD_RESET, $vars['pwreset-flag']);
        $locked = $this->statusChanged(UserAccountStatus::LOCKED, $vars['locked-flag']);
        $forbidPwChange = $this->statusChanged(UserAccountStatus::FORBID_PASSWD_RESET, $vars['forbid-pwchange-flag']);

        $info = $this->getInfo();
        foreach ($vars as $key => $value) {
            if (($key != 'id' && $info[$key] && $info[$key] != $value) || ($pwreset && $key == 'pwreset-flag' ||
                    $locked && $key == 'locked-flag' || $forbidPwChange && $key == 'forbid-pwchange-flag')) {
                $type = array('type' => 'edited', 'key' => $key);
                Signal::send('object.edited', $this, $type);
            }
        }

        $this->set('timezone', $vars['timezone']);
        $this->set('username', Format::sanitize($vars['username']));

        if ($vars['passwd1']) {
            $this->setPassword($vars['passwd1']);
            $this->setStatus(UserAccountStatus::CONFIRMED);
            $type = array('type' => 'edited', 'key' => 'password');
            Signal::send('object.edited', $this, $type);
        }

        // Set flags
        foreach (array(
                'pwreset-flag' => UserAccountStatus::REQUIRE_PASSWD_RESET,
                'locked-flag' => UserAccountStatus::LOCKED,
                'forbid-pwchange-flag' => UserAccountStatus::FORBID_PASSWD_RESET
        ) as $ck=>$flag) {
            if ($vars[$ck])
                $this->setStatus($flag);
            else {
                if (($pwreset && $ck == 'pwreset-flag') || ($locked && $ck == 'locked-flag') ||
                    ($forbidPwChange && $ck == 'forbid-pwchange-flag')) {
                        $type = array('type' => 'edited', 'key' => $ck);
                        Signal::send('object.edited', $this, $type);
                }
                $this->clearStatus($flag);
            }
        }

        return $this->save(true);
    }

    static function createForUser($user, $defaults=false) {
        $acct = new static(array('user_id'=>$user->getId()));
        if ($defaults && is_array($defaults)) {
            foreach ($defaults as $k => $v)
                $acct->set($k, $v);
        }
        return $acct;
    }

    static function lookupByUsername($username) {
        if (Validator::is_email($username))
            $user = static::lookup(array('user__emails__address' => $username));
        elseif (Validator::is_userid($username))
            $user = static::lookup(array('username' => $username));

        return $user;
    }

    static function register($user, $vars, &$errors) {

        if (!$user || !$vars)
            return false;

        //Require temp password.
        if ((!$vars['backend'] || $vars['backend'] != 'client')
                && !isset($vars['sendemail'])) {
            if (!$vars['passwd1'])
                $errors['passwd1'] = 'Temporary password required';
            elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
                $errors['passwd2'] = 'Passwords do not match';
            else {
                try {
                    self::checkPassword($vars['passwd1']);
                } catch (BadPassword $ex) {
                    $errors['passwd1'] =  $ex->getMessage();
                }
            }
        }

        if ($errors) return false;

        $account = new UserAccount(array(
            'user_id' => $user->getId(),
            'timezone' => $vars['timezone'],
            'backend' => $vars['backend'],
        ));

        if ($vars['username'] && strcasecmp($vars['username'], $user->getEmail()))
            $account->set('username', Format::sanitize($vars['username']));

        if ($vars['passwd1'] && !$vars['sendemail']) {
            $account->set('passwd', Passwd::hash($vars['passwd1']));
            $account->setStatus(UserAccountStatus::CONFIRMED);
            if ($vars['pwreset-flag'])
                $account->setStatus(UserAccountStatus::REQUIRE_PASSWD_RESET);
            if ($vars['forbid-pwreset-flag'])
                $account->setStatus(UserAccountStatus::FORBID_PASSWD_RESET);
        }
        elseif ($vars['backend'] && $vars['backend'] != 'client') {
            // Auto confirm remote accounts
            $account->setStatus(UserAccountStatus::CONFIRMED);
        }

        $account->save(true);

        if (!$account->isConfirmed() && $vars['sendemail'])
            $account->sendConfirmEmail();

        return $account;
    }

    static function checkPassword($new, $current=null) {
        osTicketClientAuthentication::checkPassword($new, $current);
    }

}

class UserAccountStatus {

    var $flag;

    const CONFIRMED             = 0x0001;
    const LOCKED                = 0x0002;
    const REQUIRE_PASSWD_RESET  = 0x0004;
    const FORBID_PASSWD_RESET   = 0x0008;

    function __construct($flag) {
        $this->flag = $flag;
    }

    function check($flag) {
        return 0 !== ($this->flag & $flag);
    }

    function isLocked() {
        return $this->check(self::LOCKED);
    }

    function isConfirmed() {
        return $this->check(self::CONFIRMED);
    }

    function __toString() {

        if ($this->isLocked())
            return __('Locked (Administrative)');

        if (!$this->isConfirmed())
            return __('Locked (Pending Activation)');

        // ... Other flags here (password reset, etc).

        return __('Active (Registered)');
    }
}

/*
 *  Generic user list.
 */
class UserList extends MailingList {

   function add($user) {
        if (!$user instanceof ITicketUser)
            throw new InvalidArgumentException('User expected');

        return parent::add($user);
    }
}

?>
class.auth.php:
<?php

require_once(INCLUDE_DIR.'class.2fa.php');

interface AuthenticatedUser {
    // Get basic information
    function getId();
    function getUsername();
    function getUserType();


    // Get password reset timestamp
    function getPasswdResetTimestamp();

    //Backend used to authenticate the user
    function getAuthBackend();

    // Get 2FA Backend
    function get2FABackend();

    //Authentication key
    function setAuthKey($key);

    function getAuthKey();

    // logOut the user
    function logOut();

    // Signal method to allow performing extra things when a user is logged
    // into the sysem
    function onLogin($bk);
}

abstract class BaseAuthenticatedUser
implements AuthenticatedUser {
    //Authorization key returned by the backend used to authorize the user
    private $authkey;

    // Get basic information
    abstract function getId();
    abstract function getUsername();
    abstract function getUserType();

    // Get password reset timestamp
    function getPasswdResetTimestamp() {
        return null;
    }

    //Backend used to authenticate the user
    abstract function getAuthBackend();

    // Get 2FA Backend
    abstract function get2FABackend();

    //Authentication key
    function setAuthKey($key) {
        $this->authkey = $key;
    }

    function getAuthKey() {
        return $this->authkey;
    }

    // logOut the user
    function logOut() {

        if ($bk = $this->getAuthBackend())
            return $bk->signOut($this);

        return false;
    }

    // Signal method to allow performing extra things when a user is logged
    // into the sysem
    function onLogin($bk) {}
}

require_once(INCLUDE_DIR.'class.ostsession.php');
require_once(INCLUDE_DIR.'class.usersession.php');

interface AuthDirectorySearch {
    /**
     * Indicates if the backend can be used to search for user information.
     * Lookup is performed to find user information based on a unique
     * identifier.
     */
    function lookup($id);

    /**
     * Indicates if the backend supports searching for usernames. This is
     * distinct from information lookup in that lookup is intended to lookup
     * information based on a unique identifier
     */
    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;
    }

    function attemptAutoRegister() {
        global $cfg;

        if (!$cfg || $cfg->isClientRegistrationMode(['disabled']))
            return false;

        // Attempt to automatically register
        $this_form = UserForm::getUserForm()->getForm($this->getInfo());
        $bk = $this->getBackend();
        $defaults = array(
            'timezone' => $cfg->getDefaultTimezone(),
            'username' => $this->getUsername(),
        );
        if ($bk->supportsInteractiveAuthentication())
            // User can only be authenticated against this backend
            $defaults['backend'] = $bk->getBkId();
        if ($this_form->isValid(function($f) { return !$f->isVisibleToUsers(); })
                && ($U = User::fromVars($this_form->getClean()))
                && ($acct = ClientAccount::createForUser($U, $defaults))
                // Confirm and save the account
                && $acct->confirm()
                // Login, since `tickets.php` will not attempt SSO
                && ($cl = new ClientSession(new EndUser($U)))
                && ($bk->login($cl, $bk)))
            return $cl;
    }
}


/**
 * Authorization backend
 *
 * Abstract service registry to provide authorization backends.
 */
abstract class OAuth2Backend extends ServiceRegistry {
    static $name;
    static $id;
    protected  $config;

    static function register($bk) {
        if (is_string($bk) && class_exists($bk))
            $bk = new $bk();

        if (!is_object($bk)
                || !($bk instanceof OAuth2Backend))
            return false;

        static::$registry[$bk->getBkId()] = $bk;
    }

    static function allRegistered() {
        return self::getRegistry();
    }

    static function getBackend($id) {
        if ($id && ($registry = static::allRegistered())
                && isset($registry[$id]))
            return $registry[$id];
    }
}

abstract class OAuth2AuthorizationBackend  extends OAuth2Backend {
    static protected $registry = array();

    static function allRegistered() {
        return array_merge(self::$registry, parent::allRegistered());
    }

    static function getBackend($id) {
        if (($bk=parent::getBackend($id)))
            return $bk;

        // Overloaded backend load.
        list($auth, $a, $i) = self::parseId($id);
        if ($auth && ($bk=parent::getBackend($auth)))
            return $bk;
    }

    static function parseId($id) {
        return  preg_split('/(?(?<=oauth2):(?!\w+)|:)/', $id);
    }

    abstract function getConfigForm($configId);
    abstract function triggerEmailAuth($id);
    abstract function callback($resp, $ref);
}

abstract class OAuth2AuthenticationBackend  extends OAuth2Backend {
    static protected $registry = array();

    static function allRegistered() {
        return array_merge(self::$registry, parent::allRegistered());
    }

    static function getBackend($id) {
        if (($bk=parent::getBackend($id)))
            return $bk;
    }
}

/**
 * Authentication backend
 *
 * Authentication provides the basis of abstracting the link between the
 * login page with a username and password and the staff member,
 * administrator, or client using the system.
 *
 * The system works by allowing the AUTH_BACKENDS setting from
 * ost-config.php to determine the list of authentication backends or
 * providers and also specify the order they should be evaluated in.
 *
 * The authentication backend should define a authenticate() method which
 * receives a username and optional password. If the authentication
 * succeeds, an instance deriving from <User> should be returned.
 */
abstract class AuthenticationBackend extends ServiceRegistry {
    static $name;
    static $id;
    protected  $config;

    static function register($bk) {
        if (is_string($bk) && class_exists($bk))
            $bk = new $bk();

        if (!is_object($bk)
                || !($bk instanceof AuthenticationBackend))
            return false;

        static::$registry[$bk->getBkId()] = $bk;
    }

    static function allRegistered() {
        return self::getRegistry();
    }

    static function getInteractive() {
        $backends = [];
        foreach (static::allRegistered() as $bk)
            if ($bk->isInteractive())
                $backends[] = $bk;
        return $backends;
    }

    static function getExternal() {
        $backends = [];
        foreach (static::allRegistered() as $bk)
            if ($bk->isExternal())
                $backends[] = $bk;
        return $backends;
    }

    static function getBackend($id) {
        if ($id
                && ($backends = static::allRegistered())
                && isset($backends[$id]))
            return $backends[$id];
    }

    static function lookupBackend($id, $class=null) {
        // Lookup both registries
        if (!($bk = StaffAuthenticationBackend::getBackend($id)))
            $bk = UserAuthenticationBackend::getBackend($id);

        // See if specific type is requesred.
        if ($class && !($bk instanceof $class))
            return null;

        return $bk;
    }

    static function getSearchDirectoryBackend($id) {

        if ($id
                && ($backends = static::getSearchDirectories())
                && isset($backends[$id]))
            return $backends[$id];
    }

    /*
     * Allow the backend to do login audit depending on the result
     * This is mainly used to track failed login attempts
     */
    static function authAudit($result, $credentials=null) {

        if (!$result) return;

        foreach (static::allRegistered() as $bk)
            $bk->audit($result, $credentials);
    }

    static function process($username, $password=null, &$errors=array()) {

        if (!$username)
            return false;

        $backends =  static::getAllowedBackends($username);
        foreach (static::allRegistered() as $bk) {
            if ($backends //Allowed backends
                    && $bk->supportsInteractiveAuthentication()
                    && !in_array($bk->getBkId(), $backends))
                // User cannot be authenticated against this backend
                continue;

            // All backends are queried here, even if they don't support
            // authentication so that extensions like lockouts and audits
            // can be supported.
            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'));

        if ($result && $result instanceof AccessDenied)
            $errors['err'] = $result->reason;

        $info = array('username' => $username, 'password' => $password);
        Signal::send('auth.login.failed', null, $info);
        self::authAudit($result, $info);
    }

    /*
     *  Attempt to process non-interactive sign-on e.g  HTTP-Passthrough
     *
     * $forcedAuth - indicate if authentication is required.
     *
     */
    static function processSignOn(&$errors, $forcedAuth=true) {

        foreach (static::allRegistered() as $bk) {
            // All backends are queried here, even if they don't support
            // authentication so that extensions like lockouts and audits
            // can be supported.
            try {
                $result = $bk->signOn();
                if ($result instanceof AuthenticatedUser) {
                    //Perform further Object specific checks and the actual login
                    if (!$bk->login($result, $bk))
                        continue;

                    return $result;
                }
                elseif ($result instanceof ClientCreateRequest
                        && $bk instanceof UserAuthenticationBackend)
                    return $result;
                elseif ($result instanceof AccessDenied) {
                    break;
                }
            }
            catch (AccessDenied $e) {
                $result = $e;
                break;
            }
        }

        if (!$result && $forcedAuth)
            $result = new  AccessDenied(__('Unknown user'));

        if ($result && $result instanceof AccessDenied)
            $errors['err'] = $result->reason;

        self::authAudit($result);
    }

    static function getSearchDirectories() {
        $backends = array();
        foreach (StaffAuthenticationBackend::allRegistered() as $bk)
            if ($bk instanceof AuthDirectorySearch)
                $backends[$bk->getBkId()] = $bk;

        foreach (UserAuthenticationBackend::allRegistered() as $bk)
            if ($bk instanceof AuthDirectorySearch)
                $backends[$bk->getBkId()] = $bk;

        return array_unique($backends, SORT_REGULAR);
    }

    static function searchUsers($query) {
        $users = array();
        foreach (static::getSearchDirectories() as $bk)
            $users = array_merge($users, $bk->search($query));

        return $users;
    }

    /**
     * Indicates if the backed supports authentication. Useful if the
     * backend is used for logging or lockout only
     */
    function supportsInteractiveAuthentication() {
        return true;
    }

    /**
     * Alias for supportsInteractiveAuthentication()
     *
     */
    function isInteractive() {
        return $this->supportsInteractiveAuthentication();
    }

    /**
     * Indicates of the backend is External Authentication
     */
    function isExternal() {
        return ($this instanceof ExternalAuthentication);
    }

    /**
     * Indicates if the backend supports changing a user's password. This
     * would be done in two fashions. Either the currently-logged in user
     * want to change its own password or a user requests to have their
     * password reset. This requires an administrative privilege which this
     * backend might not possess, so it's defined in supportsPasswordReset()
     */
    function supportsPasswordChange() {
        return false;
    }


    /**
     * Get supported password policies for the backend.
     *
     */
    function getPasswordPolicies($user=null) {
        return PasswordPolicy::allActivePolicies();
    }

    /**
     * Request the backend to update the password for a user. This method is
     * the main entry for password updates so that password policies can be
     * applied to the new password before passing the new password to the
     * backend for updating.
     *
     * Throws:
     * BadPassword — if password does not meet policy requirement
     * PasswordUpdateFailed — if backend failed to update the password
     */
    function setPassword($user, $password, $current=false) {
        foreach ($this->getPasswordPolicies($user) as $P)
            $P->onSet($password, $current);

        $rv = $this->syncPassword($user, $password);
        if ($rv) {
            $info = array('password' => $password, 'current' => $current);
            Signal::send('auth.pwchange', $user, $info);
        }
        return $rv;
    }

    /*
     * Request the backend to check the policies for a just logged
     * in user.
     * Throws: BadPassword & ExpiredPassword - for password related failures
     */
    function checkPolicies($user, $password) {
        // Password policies
        foreach ($this->getPasswordPolicies($user) as $P)
            $P->onLogin($user, $password);
    }

    /**
     * Request the backend to update the user's password with the password
     * given. This method should only be used if the backend advertises
     * supported password updates with the supportsPasswordChange() method.
     *
     * Returns:
     * true if the password was successfully updated and false otherwise.
     */
    protected function syncPassword($user, $password) {
        return false;
    }

    function supportsPasswordReset() {
        return false;
    }

    function signOn() {
        return null;
    }

    protected function validate($auth) {
        return null;
    }

    protected function audit($result, $credentials) {
        return null;
    }

    abstract function authenticate($username, $password);
    abstract function login($user, $bk);
    abstract static function getUser(); //Validates  authenticated users.
    abstract static function getAllowedBackends($userid);
    abstract protected function getAuthKey($user);
    abstract static function signOut($user);
}

/**
 * ExternalAuthenticationBackend
 *
 * External authentication backends are backends such as Google+ which
 * require a redirect to a remote site and a redirect back to osTicket in
 * order for a  user to be authenticated. For such backends, neither the
 * username and password fields nor single sign on alone can be used to
 * authenticate the user.
 */
interface ExternalAuthentication {

    /**
     * Requests the backend to render an external link box. When the user
     * clicks this box, the backend will be prompted to redirect the user to
     * the remote site for authentication there.
     */
    function renderExternalLink();

    /**
     * Function: getServiceName
     *
     * Called to get the service name displayed on login page.
     */
     function getServiceName();

    /**
     * Function: triggerAuth
     *
     * Called when a user clicks the button rendered in the
     * ::renderExternalLink() function. This method should initiate the
     * remote authentication mechanism.
     */
    function triggerAuth();
}

abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
    static protected $registry = array();

    static function allRegistered() {
        return array_merge(self::$registry, parent::allRegistered());
    }

    static function isBackendAllowed($staff, $bk) {

        if (!($backends=self::getAllowedBackends($staff->getId())))
            return true;  //No restrictions

        return in_array($bk->getBkId(), array_map('strtolower', $backends));
    }

    function getPasswordPolicies($user=null) {
        global $cfg;
        $policies = PasswordPolicy::allActivePolicies();
        if ($cfg && ($policy = $cfg->getStaffPasswordPolicy())) {
            foreach ($policies as $P)
                if ($policy == $P->getBkId())
                    return array($P);
        }

        return $policies;
    }

    static function getAllowedBackends($userid) {

        $backends =array();
        //XXX: Only one backend can be specified at the moment.
        $sql = 'SELECT backend FROM '.STAFF_TABLE
              .' WHERE backend IS NOT NULL ';
        if (is_numeric($userid))
            $sql.= ' AND staff_id='.db_input($userid);
        else {
            $sql.= ' AND (username='.db_input($userid) .' OR email='.db_input($userid).')';
        }

        if (($res=db_query($sql, false)) && db_num_rows($res))
            $backends[] = db_result($res);

        return array_filter($backends);
    }

    function login($staff, $bk) {
        global $ost;

        if (!$bk || !($staff instanceof Staff))
            return false;

        // Ensure staff is allowed for realz to be authenticated via the backend.
        if (!static::isBackendAllowed($staff, $bk)
            || !($authkey=$bk->getAuthKey($staff)))
            return false;

        //Log debug info.
        $ost->logDebug(_S('Agent Login'),
            sprintf(_S("%s logged in [%s], via %s"), $staff->getUserName(),
                $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug.

        $agent = Staff::lookup($staff->getId());
        $type = array('type' => 'login');
        Signal::send('person.login', $agent, $type);

        // Check if the agent has 2fa enabled
        $auth2fa = null;
        if (($_2fa = $staff->get2FABackend())
                && ($token=$_2fa->send($staff))) {
            $auth2fa = sprintf('%s:%s:%s',
                    $_2fa->getId(), md5($token.$staff->getId()), time());
        }

        // Tag the authkey.
        $authkey = $bk->getBkId().':'.$authkey;
        // Now set session crap and lets roll baby!
        $authsession = &$_SESSION['_auth']['staff'];
        $authsession = array(); //clear.
        $authsession['id'] = $staff->getId();
        $authsession['key'] =  $authkey;
        $authsession['2fa'] =  $auth2fa;
        // Set TIME_BOMB to regenerate the session 10 seconds after login
        $_SESSION['TIME_BOMB'] = time() + 10;
        // Set session token
        $staff->setSessionToken();
        // Set Auth Key
        $staff->setAuthKey($authkey);
        Signal::send('auth.login.succeeded', $staff);

        if ($bk->supportsInteractiveAuthentication())
            $staff->cancelResetTokens();


        // Update last-used language, login time, etc
        $staff->onLogin($bk);

        return true;
    }

    /* Base signOut
     *
     * Backend should extend the signout and perform any additional signout
     * it requires.
     */

    static function signOut($staff) {
        global $ost;

        $_SESSION['_auth']['staff'] = array();
        unset($_SESSION[':token']['staff']);
        $ost->logDebug(_S('Agent logout'),
                sprintf(_S("%s logged out [%s]"),
                    $staff->getUserName(),
                    $_SERVER['REMOTE_ADDR'])); //Debug.

        $agent = Staff::lookup($staff->getId());
        $type = array('type' => 'logout');
        Signal::send('person.logout', $agent, $type);
        Signal::send('auth.logout', $staff);
    }

    // Called to get authenticated user (if any)
    static function getUser() {

        if (!isset($_SESSION['_auth']['staff'])
                || !$_SESSION['_auth']['staff']['key'])
            return null;

        list($id, $auth) = explode(':', $_SESSION['_auth']['staff']['key']);

        if (!($bk=static::getBackend($id)) //get the backend
                || !($staff = $bk->validate($auth)) //Get AuthicatedUser
                || !($staff instanceof Staff)
                || !$staff->isActive()
                || $staff->getId() != $_SESSION['_auth']['staff']['id'] // check ID
        )
            return null;

        $staff->setAuthKey($_SESSION['_auth']['staff']['key']);

        return $staff;
    }

    function authenticate($username, $password) {
        return false;
    }

    // Generic authentication key for staff's backend is the username
    protected function getAuthKey($staff) {

        if(!($staff instanceof Staff))
            return null;

        return $staff->getUsername();
    }

    protected function validate($authkey) {

        if (($staff = StaffSession::lookup($authkey))
            && $staff->getId()
            && $staff->isActive())
            return $staff;
    }
}

abstract class ExternalStaffAuthenticationBackend
        extends StaffAuthenticationBackend
        implements ExternalAuthentication {

    static $fa_icon = "signin";
    static $sign_in_image_url = false;
    static $service_name = "External";

    function getServiceName() {
        return __(static::$service_name);
    }

    function renderExternalLink() {
        $service = sprintf('%s %s',
                __('Sign in with'),
                $this->getServiceName());
        ?>
        <a class="external-sign-in" title="<?php echo $service; ?>"
                href="login.php?do=ext&amp;bk=<?php echo
                urlencode($this->getBkId()); ?>">
<?php if (static::$sign_in_image_url) { ?>
        <img class="sign-in-image" src="<?php echo static::$sign_in_image_url;
            ?>" alt="<?php echo $service; ?>"/>
<?php } else { ?>
            <div class="external-auth-box">
            <span class="external-auth-icon">
                <i class="icon-<?php echo static::$fa_icon; ?> icon-large icon-fixed-with"></i>
            </span>
            <span class="external-auth-name">
               <?php echo $service; ?>
            </span>
            </div>
<?php } ?>
        </a><?php
    }

    function triggerAuth() {
        $_SESSION['ext:bk:id'] = $this->getBkId();
        // For legacy plugins prior to v1.17
        $_SESSION['ext:bk:class'] = get_class($this);
    }
}
Signal::connect('api', function($dispatcher) {
    $dispatcher->append(
        url('^/auth/ext$', function() {
            if ($id = $_SESSION['ext:bk:id']) {
                $bk = StaffAuthenticationBackend::getBackend($id)
                    ?: UserAuthenticationBackend::getBackend($id);
                if ($bk instanceof ExternalAuthentication)
                    $bk->triggerAuth();
            }
        })
    );
});

abstract class UserAuthenticationBackend  extends AuthenticationBackend {
    static protected $registry = array();

    static function allRegistered() {
        return array_merge(self::$registry, parent::allRegistered());
    }

    function getPasswordPolicies($user=null) {
        global $cfg;
        $policies = PasswordPolicy::allActivePolicies();
        if ($cfg && ($policy = $cfg->getClientPasswordPolicy())) {
            foreach ($policies as $P)
                if ($policy == $P->getId())
                    return array($P);
        }

        return $policies;
    }

    static function getAllowedBackends($userid) {
        $backends = array();
        $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) {
        global $ost;

        if (!$user || !$bk
                || !$bk->getBkId() //Must have ID
                || !($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 user and associated ticket in the SESSION
        $this->setAuthKey($user, $bk, $authkey);
        // Set Session Token
        $user->setSessionToken();
        //The backend used decides the format of the auth key.
        // XXX: encrypt to hide the bk??
        $user->setAuthKey($authkey);
        //Log login info...
        $msg=sprintf(_S('%1$s (%2$s) logged in [%3$s]'
                /* Tokens are <username>, <id>, and <ip> */),
                $user->getUserName(), $user->getId(), $_SERVER['REMOTE_ADDR']);
        $ost->logDebug(_S('User login'), $msg);

        $u = $user->getSessionUser()->getUser();
        $type = array('type' => 'login');
        Signal::send('person.login', $u, $type);

        // Set TIME_BOMB to regenerate the session 10 seconds after login
        $_SESSION['TIME_BOMB'] = time() + 10;

        if ($bk->supportsInteractiveAuthentication() && ($acct=$user->getAccount()))
            $acct->cancelResetTokens();

        // Update last-used language, login time, etc
        $user->onLogin($bk);

        return true;
    }

    function setAuthKey($user, $bk, $key=false) {
        $authkey = $key ?: $bk->getAuthKey($user);

        //Tag the authkey.
        $authkey = $bk->getBkId().':'.$authkey;

        //Set the session goodies
        $authsession = &$_SESSION['_auth']['user'];

        $authsession = array(); //clear.
        $authsession['id'] = $user->getId();
        $authsession['key'] = $authkey;
    }

    function authenticate($username, $password) {
        return false;
    }

    static function signOut($user) {
        global $ost;

        $_SESSION['_auth']['user'] = array();
        unset($_SESSION[':token']['client']);
        $ost->logDebug(_S('User logout'),
            sprintf(_S("%s logged out [%s]" /* Tokens are <username> and <ip> */),
                $user->getUserName(), $_SERVER['REMOTE_ADDR']));

        $u = $user->getSessionUser()->getUser();
        $type = array('type' => 'logout');
        Signal::send('person.logout', $u, $type);
    }

    protected function getAuthKey($user) {
        return  $user->getId();
    }

    static function getUser() {

        if (!isset($_SESSION['_auth']['user'])
                || !$_SESSION['_auth']['user']['key'])
            return null;

        list($id, $auth) = explode(':', $_SESSION['_auth']['user']['key']);

        if (!($bk=static::getBackend($id)) //get the backend
                || !($user=$bk->validate($auth)) //Get AuthicatedUser
                || !($user instanceof AuthenticatedUser) // Make sure it user
                || $user->getId() != $_SESSION['_auth']['user']['id'] // check ID
                )
            return null;

        if (($account=$user->getAccount()) && !$account->isActive())
            return null;

        $user->setAuthKey($_SESSION['_auth']['user']['key']);

        return $user;
    }

    protected function validate($userid) {
        if (!($user = User::lookup($userid)))
            return false;
        elseif (!($account=$user->getAccount()))
            return false;
        elseif (!$account->isActive())
            return false;

        return new ClientSession(new EndUser($user));
    }
}

abstract class ExternalUserAuthenticationBackend
        extends UserAuthenticationBackend
        implements ExternalAuthentication {

    static $fa_icon = "signin";
    static $sign_in_image_url = false;
    static $service_name = "External";

    function getServiceName() {
        return __(static::$service_name);
    }

    function renderExternalLink() {
        $service = sprintf('%s %s',
                __('Sign in with'),
                $this->getServiceName());

        ?>
        <a class="external-sign-in" title="<?php echo $service; ?>"
                href="login.php?do=ext&amp;bk=<?php echo
                urlencode($this->getBkId()); ?>">
<?php if (static::$sign_in_image_url) { ?>
        <img class="sign-in-image" src="<?php echo static::$sign_in_image_url;
            ?>" alt="<?php echo $service; ?>"/>
<?php } else { ?>
            <div class="external-auth-box">
            <span class="external-auth-icon">
                <i class="icon-<?php echo static::$fa_icon; ?> icon-large icon-fixed-with"></i>
            </span>
            <span class="external-auth-name">
                <?php echo $service; ?>
            </span>
            </div>
<?php } ?>
        </a><?php
    }

    function triggerAuth() {
        $_SESSION['ext:bk:id'] = $this->getBkId();
        // Legacy for plugins prior to v1.17
        $_SESSION['ext:bk:class'] = get_class($this);
    }
}

/**
 * This will be an exception in later versions of PHP
 */
class AccessDenied extends Exception {
    function __construct($reason) {
        $this->reason = $reason;
        parent::__construct($reason);
    }
}

/**
 * Simple authentication backend which will lock the login form after a
 * configurable number of attempts
 */
abstract class AuthStrikeBackend extends AuthenticationBackend {

    function authenticate($username, $password=null) {
        return static::authTimeout();
    }

    function signOn() {
        return static::authTimeout();
    }

    static function signOut($user) {
        return false;
    }


    function login($user, $bk) {
        return false;
    }

    static function getUser() {
        return null;
    }

    function supportsInteractiveAuthentication() {
        return false;
    }

    static function getAllowedBackends($userid) {
        return array();
    }

    function getAuthKey($user) {
        return null;
    }

    //Provides audit facility for logins attempts
    function audit($result, $credentials) {

        //Count failed login attempts as a strike.
        if ($result instanceof AccessDenied)
            return static::authStrike($credentials);

    }

    abstract static function authStrike($credentials);
    abstract static function authTimeout();
}

/*
 * Backend to monitor staff's failed login attempts
 */
class StaffAuthStrikeBackend extends  AuthStrikeBackend {
    static $id = "authstrike.staff";

    static function authTimeout() {
        global $ost;

        $cfg = $ost->getConfig();

        $authsession = &$_SESSION['_auth']['staff'];
        if (!isset($authsession['laststrike']))
            return;

        //Veto login due to excessive login attempts.
        if((time()-$authsession['laststrike'])<$cfg->getStaffLoginTimeout()) {
            $authsession['laststrike'] = time(); //reset timer.
            return new AccessDenied(__('Maximum failed login attempts reached'));
        }

        //Timeout is over.
        //Reset the counter for next round of attempts after the timeout.
        $authsession['laststrike']=null;
        $authsession['strikes']=0;
    }

    static function authstrike($credentials) {
        global $ost;

        $cfg = $ost->getConfig();

        $authsession = &$_SESSION['_auth']['staff'];

        $username = $credentials['username'];

        $authsession['strikes']+=1;
        if($authsession['strikes']>$cfg->getStaffMaxLogins()) {
            $authsession['laststrike']=time();
            $timeout = $cfg->getStaffLoginTimeout()/60;
            $alert=_S('Excessive login attempts by an agent?')."\n"
                   ._S('Username').": $username\n"
                   ._S('IP').": {$_SERVER['REMOTE_ADDR']}\n"
                   ._S('Time').": ".date('M j, Y, g:i a T')."\n\n"
                   ._S('Attempts').": {$authsession['strikes']}\n"
                   ._S('Timeout').": ".sprintf(_N('%d minute', '%d minutes', $timeout), $timeout)."\n\n";
            $admin_alert = ($cfg->alertONLoginError() == 1) ? TRUE : FALSE;
            $ost->logWarning(sprintf(_S('Excessive login attempts (%s)'),$username),
                    $alert, $admin_alert);

              if ($username) {
                $agent = Staff::lookup($username);
                $type = array('type' => 'login', 'msg' => sprintf('Excessive login attempts (%s)', $authsession['strikes']));
                Signal::send('person.login', $agent, $type);
              }

            return new AccessDenied(__('Forgot your login info? Contact Admin.'));
        //Log every other third failed login attempt as a warning.
        } elseif($authsession['strikes']%3==0) {
            $alert=_S('Username').": {$username}\n"
                    ._S('IP').": {$_SERVER['REMOTE_ADDR']}\n"
                    ._S('Time').": ".date('M j, Y, g:i a T')."\n\n"
                    ._S('Attempts').": {$authsession['strikes']}";
            $ost->logWarning(sprintf(_S('Failed agent login attempt (%s)'),$username),
                $alert, false);
        }
    }
}
StaffAuthenticationBackend::register('StaffAuthStrikeBackend');

/*
 * Backend to monitor user's failed login attempts
 */
class UserAuthStrikeBackend extends  AuthStrikeBackend {
    static $id = "authstrike.user";

    static function authTimeout() {
        global $ost;

        $cfg = $ost->getConfig();

        $authsession = &$_SESSION['_auth']['user'];
        if (!$authsession['laststrike'])
            return;

        //Veto login due to excessive login attempts.
        if ((time()-$authsession['laststrike']) < $cfg->getStaffLoginTimeout()) {
            $authsession['laststrike'] = time(); //reset timer.
            return new AccessDenied(__("You've reached maximum failed login attempts allowed."));
        }

        //Timeout is over.
        //Reset the counter for next round of attempts after the timeout.
        $authsession['laststrike']=null;
        $authsession['strikes']=0;
    }

    static function authstrike($credentials) {
        global $ost;

        $cfg = $ost->getConfig();

        $authsession = &$_SESSION['_auth']['user'];

        $username = $credentials['username'];
        $password = $credentials['password'];

        $authsession['strikes']+=1;
        if($authsession['strikes']>$cfg->getClientMaxLogins()) {
            $authsession['laststrike'] = time();
            $alert=_S('Excessive login attempts by a user.')."\n".
                    _S('Username').": {$username}\n".
                    _S('IP').": {$_SERVER['REMOTE_ADDR']}\n".
                    _S('Time').": ".date('M j, Y, g:i a T')."\n\n".
                    _S('Attempts').": {$authsession['strikes']}";
            $admin_alert = ($cfg->alertONLoginError() == 1 ? TRUE : FALSE);
            $ost->logError(_S('Excessive login attempts (user)'), $alert, $admin_alert);

            if ($username) {
              $account = UserAccount::lookupByUsername($username);
              $id = UserEmailModel::getIdByEmail($username);
              if ($account)
                  $user = User::lookup($account->user_id);
              elseif ($id)
                $user = User::lookup($id);

              if ($user) {
                $type = array('type' => 'login', 'msg' => sprintf('Excessive login attempts (%s)', $authsession['strikes']));
                Signal::send('person.login', $user, $type);
              }
            }

            return new AccessDenied(__('Access denied'));
        } elseif($authsession['strikes']%3==0) { //Log every third failed login attempt as a warning.
            $alert=_S('Username').": {$username}\n".
                    _S('IP').": {$_SERVER['REMOTE_ADDR']}\n".
                    _S('Time').": ".date('M j, Y, g:i a T')."\n\n".
                    _S('Attempts').": {$authsession['strikes']}";
            $ost->logWarning(_S('Failed login attempt (user)'), $alert, false);
        }

    }
}
UserAuthenticationBackend::register('UserAuthStrikeBackend');


class osTicketStaffAuthentication extends StaffAuthenticationBackend {
    static $name = "Local Authentication";
    static $id = "local";

    function authenticate($username, $password) {
        if (($user = StaffSession::lookup($username)) && $user->getId() &&
                $user->check_passwd($password)) {
            try {
                $this->checkPolicies($user, $password);
            } catch (BadPassword | ExpiredPassword $ex) {
                $user->change_passwd = 1;
            }
            return $user;
        }
    }

    function supportsPasswordChange() {
        return true;
    }

    function syncPassword($staff, $password) {
        $staff->passwd = Passwd::hash($password);
    }

    static function checkPassword($new, $current) {
        PasswordPolicy::checkPassword($new, $current, new self());
    }

}
StaffAuthenticationBackend::register('osTicketStaffAuthentication');

class PasswordResetTokenBackend extends StaffAuthenticationBackend {
    static $id = "pwreset.staff";

    function supportsInteractiveAuthentication() {
        return false;
    }

    function signOn($errors=array()) {
        global $ost;

        if (!isset($_POST['userid']) || !isset($_POST['token']))
            return false;
        elseif (!($_config = new Config('pwreset')))
            return false;

        $staff = StaffSession::lookup($_POST['userid']);
        if (!$staff || !$staff->getId())
            $errors['msg'] = __('Invalid user-id given');
        elseif (!($id = $_config->get($_POST['token']))
                || $id != $staff->getId())
            $errors['msg'] = __('Invalid reset token');
        elseif (!($ts = $_config->lastModified($_POST['token']))
                && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
            $errors['msg'] = __('Invalid reset token');
        elseif (!$staff->forcePasswdRest())
            $errors['msg'] = __('Unable to reset password');
        else
            return $staff;
    }

    function login($staff, $bk) {
        $_SESSION['_staff']['reset-token'] = $_POST['token'];
        Signal::send('auth.pwreset.login', $staff);
        return parent::login($staff, $bk);
    }
}
StaffAuthenticationBackend::register('PasswordResetTokenBackend');

/*
 * AuthToken Authentication Backend
 *
 * Provides auto-login facility for end users with valid link
 *
 * Ticket used to loggin is tracked durring the session this is
 * important in the future when auto-logins will be
 * limited to single ticket view.
 */
class AuthTokenAuthentication extends UserAuthenticationBackend {
    static $name = "Auth Token Authentication";
    static $id = "authtoken";


    function signOn() {
        global $cfg;


        if (!$cfg || !$cfg->isAuthTokenEnabled())
            return null;

        $user = null;
        if ($_GET['auth']) {
            if (($u = TicketUser::lookupByToken($_GET['auth'])))
                $user = new ClientSession($u);
        }
        // Support old ticket based tokens.
        elseif ($_GET['t'] && $_GET['e'] && $_GET['a']) {
            if (($ticket = Ticket::lookupByNumber($_GET['t'], $_GET['e']))
                    // Using old ticket auth code algo - hardcoded here because it
                    // will be removed in ticket class in the upcoming rewrite
                    && strcasecmp((string) $_GET['a'], md5($ticket->getId()
                            .  strtolower($_GET['e']) . SECRET_SALT)) === 0
                    && ($owner = $ticket->getOwner()))
                $user = new ClientSession($owner);
        }

        return $user;
    }

    function supportsInteractiveAuthentication() {
        return false;
    }

    protected function getAuthKey($user) {

        if (!$user)
            return null;

        //Generate authkey based the type of ticket user
        // It's required to validate users going forward.
        $authkey = sprintf('%s%dt%dh%s',  //XXX: Placeholder
                    ($user->isOwner() ? 'o':'c'),
                    $user->getId(),
                    $user->getTicketId(),
                    md5($user->getId().$this->id));

        return $authkey;
    }

    protected function validate($authkey) {

        $regex = '/^(?P<type>\w{1})(?P<id>\d+)t(?P<tid>\d+)h(?P<hash>.*)$/i';
        $matches = array();
        if (!preg_match($regex, $authkey, $matches))
            return false;

        $user = null;
        switch ($matches['type']) {
            case 'c': //Collaborator
                $criteria = array(
                    'user_id' => $matches['id'],
                    'thread__ticket__ticket_id' => $matches['tid']
                );
                if (($c = Collaborator::lookup($criteria))
                        && ($c->getTicketId() == $matches['tid']))
                    $user = new ClientSession($c);
                break;
            case 'o': //Ticket owner
                if (($ticket = Ticket::lookup($matches['tid']))
                        && ($o = $ticket->getOwner())
                        && ($o->getId() == $matches['id']))
                    $user = new ClientSession($o);
                break;
        }

        //Make sure the authkey matches.
        if (!$user || strcmp($this->getAuthKey($user), $authkey))
            return null;

        $user->flagGuest();

        return $user;
    }

}

UserAuthenticationBackend::register('AuthTokenAuthentication');

//Simple ticket lookup backend used to recover ticket access link.
// We're using authentication backend so we can guard aganist brute force
// attempts (which doesn't buy much since the link is emailed)
class AccessLinkAuthentication extends UserAuthenticationBackend {
    static $name = "Ticket Access Link Authentication";
    static $id = "authlink";

    function authenticate($email, $number) {

        if (!($ticket = Ticket::lookupByNumber($number))
                || !($user=User::lookup(array('emails__address' => $email))))
            return false;

        if (!($user = $this->_getTicketUser($ticket, $user)))
            return false;

        $_SESSION['_auth']['user-ticket'] = $number;
        return new ClientSession($user);
    }

    function _getTicketUser($ticket, $user) {
        // Ticket owner?
        if ($ticket->getUserId() == $user->getId())
            $user = $ticket->getOwner();
        // Collaborator?
        elseif (!($user = Collaborator::lookup(array(
                'user_id' => $user->getId(),
                'thread__ticket__ticket_id' => $ticket->getId())
        )))
            return false; //Bro, we don't know you!

        return $user;
    }

    // We are not actually logging in the user....
    function login($user, $bk) {
        global $cfg;

        if (!$cfg->isClientEmailVerificationRequired()) {
            return parent::login($user, $bk);
        }
        return true;
    }

    protected function validate($userid) {
        $number = $_SESSION['_auth']['user-ticket'];

        if (!($ticket = Ticket::lookupByNumber($number)))
            return false;

        if (!($user = User::lookup($userid)))
            return false;

        if (!($user = $this->_getTicketUser($ticket, $user)))
            return false;

        $user = new ClientSession($user);
        $user->flagGuest();
        return $user;
    }

    function supportsInteractiveAuthentication() {
        return false;
    }
}
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->check_passwd($password))
            return false;
        else
            return $client;
    }

    static function checkPassword($new, $current) {
        PasswordPolicy::checkPassword($new, $current, new self());
    }
}
UserAuthenticationBackend::register('osTicketClientAuthentication');

class ClientPasswordResetTokenBackend extends UserAuthenticationBackend {
    static $id = "pwreset.client";

    function supportsInteractiveAuthentication() {
        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 != 'c'.$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 supportsInteractiveAuthentication() {
        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'=>substr($id,1))))
                || !$acct->getId()
                || $id != 'c'.$acct->getUserId()
                || !($client = new ClientSession(new EndUser($acct->getUser()))))
            return false;
        else
            return $client;
    }
}
UserAuthenticationBackend::register('ClientAcctConfirmationTokenBackend');

// ----- Password Policy --------------------------------------

class BadPassword extends Exception {}
class ExpiredPassword extends Exception {}
class PasswordUpdateFailed extends Exception {}

abstract class PasswordPolicy extends ServiceRegistry {
    static $id;
    static $name;

    /**
     * Check a password and throw BadPassword with a meaningful message if
     * the password cannot be accepted.
     */
    abstract function onset($new, $current);

    /*
     * Called on login to enforce policies & check for expired passwords
     */
    abstract function onLogin($user, $password);

    /*
     * Check a password aganist all available policies
     */
    static function checkPassword($new, $current, $bk=null) {
        if ($bk && is_a($bk, 'AuthenticationBackend'))
            $policies = $bk->getPasswordPolicies();
        else
            $policies = self::allActivePolicies();

        foreach ($policies as $P)
            $P->onSet($new, $current);
    }

    static function allActivePolicies() {
        $policies = array();
        foreach (array_reverse(self::getRegistry()) as $P) {
            if (is_string($P) && class_exists($P))
                $P = new $P();
            if ($P instanceof PasswordPolicy)
                $policies[] = $P;
        }
        return $policies;
    }

    static function cleanSessions($model, $user=null) {
        $criteria = array();

        switch (true) {
            case ($model instanceof Staff):
                $criteria['user_id'] = $model->getId();

                if ($user && ($model->getId() == $user->getId()))
                    array_push($criteria,
                        Q::not(array('session_id' => $user->session->session_id)));
                break;
            case ($model instanceof User):
                $regexp = '_auth\|.*"user";[a-z]+:[0-9]+:\{[a-z]+:[0-9]+:"id";[a-z]+:'.$model->getId();
                $criteria['user_id'] = 0;
                $criteria['session_data__regex'] = $regexp;

                if ($user)
                    array_push($criteria,
                        Q::not(array('session_id' => $user->session->session_id)));
                break;
            default:
                return false;
        }

        return DatabaseSessionRecord::objects()->filter($criteria)->delete();
    }
}
Signal::connect('auth.clean', array('PasswordPolicy', 'cleanSessions'));

/*
 * Basic default password policy that ships with osTicket.
 * 
 */
class osTicketPasswordPolicy
extends PasswordPolicy {
    static $id = "basic";
    static $name = /* @trans */ "Default Basic Policy";

    function onLogin($user, $password) {
        global $cfg;

        // Check for possible password expiration
        // Check is only here for legacy reasons - password management
        // policies are now done via plugins.
        if ($cfg && $user
                && ($period=$cfg->getPasswdResetPeriod())
                && ($time=$user->getPasswdResetTimestamp())
                && $time < time()-($period*2629800))
            throw new ExpiredPassword(__('Expired password'));
    }

    function onSet($passwd, $current) {
        $pwdlen = strlen($passwd);
        if ($pwdlen < 6) {
            throw new BadPassword(
                __('Password must be at least 6 characters'));
        } elseif ($pwdlen > 128) {
            throw new BadPassword(
                sprintf('%s - %s', __('Password is too long'), __('maximum of 128 characters allowed')));
        }
        // XXX: Changing case is technicall changing the password
        if (0 === strcasecmp($passwd, $current)) {
            throw new BadPassword(
                __('New password MUST be different from the current password!'));
        }
    }
}
PasswordPolicy::register('osTicketPasswordPolicy');
?>


ajax.users.php:
<?php
/*********************************************************************
    ajax.users.php

    AJAX interface for  users (based on submitted tickets)
    XXX: osTicket doesn't support user accounts at the moment.

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/

if(!defined('INCLUDE_DIR')) die('403');

include_once(INCLUDE_DIR.'class.ticket.php');
require_once INCLUDE_DIR.'class.note.php';
require_once INCLUDE_DIR.'ajax.tickets.php';

class UsersAjaxAPI extends AjaxController {

    /* Assumes search by basic info for now */
    function search($type = null, $fulltext=false) {

        if(!isset($_REQUEST['q'])) {
            Http::response(400, __('Query argument is required'));
        }

        $matches = array();
        if (!$_REQUEST['q'])
            return $this->json_encode($matches);

        $q = Format::sanitize($_REQUEST['q']);
        $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
        $users=array();
        $emails=array();
        $matches = array();

        if (strlen(Format::searchable($q)) < 3)
            return $this->encode(array());

        if (!$type || !strcasecmp($type, 'remote')) {
            foreach (AuthenticationBackend::searchUsers($q) as $u) {
                if (!trim($u['email']))
                    // Email is required currently
                    continue;
                $name = new UsersName(array('first' => $u['first'], 'last' => $u['last']));
                $matches[] = array('email' => $u['email'], 'name'=>(string) $name,
                    'info' => "{$u['email']} - $name (remote)",
                    'id' => "auth:".$u['id'], "/bin/true" => $q);
                $emails[] = $u['email'];
            }
        }

        if (!$type || !strcasecmp($type, 'local')) {

            $users = User::objects()
                ->values_flat('id', 'name', 'default_email__address')
                ->limit($limit);

            if ($fulltext) {
                global $ost;
                $users = $ost->searcher->find($q, $users);
                $users->order_by(new SqlCode('__relevance__'), QuerySet::DESC)
                    ->distinct('id');

                if (!count($emails) && !count($users) && preg_match('`\w$`u', $q)) {
                    // Do wildcard full-text search
                    $_REQUEST['q'] = $q."*";
                    return $this->search($type, $fulltext);
                }
            } else {
                $base = clone $users;
                $users->filter(array('name__contains' => $q));
                $users->union($base->copy()->filter(array(
                                'org__name__contains' => $q)), false);
                $users->union($base->copy()->filter(array(
                                'emails__address__contains' => $q)),  false);
                $users->union($base->copy()->filter(array(
                                'account__username__contains' => $q)), false);
                if (UserForm::getInstance()->getField('phone')) {
                      $users->union($base->copy()->filter(array(
                                'cdata__phone__contains' => $q)), false);
                }
            }

            // Omit already-imported remote users
            if ($emails = array_filter($emails)) {
                $users->union(User::objects()
                    ->values_flat('id', 'name', 'default_email__address')
                    ->filter(array(
                        'emails__address__in' => $emails
                )));
            }

            foreach ($users as $U) {
                list($id, $name, $email) = $U;
                foreach ($matches as $i=>$u) {
                    if ($u['email'] == $email) {
                        unset($matches[$i]);
                        break;
                    }
                }
                $name = Format::htmlchars(new UsersName($name));
                $matches[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name",
                    "id" => $id, "/bin/true" => $q);
            }
            usort($matches, function($a, $b) { return strcmp($a['name'], $b['name']); });
        }

        return $this->json_encode(array_values($matches));

    }

    function preview($id) {
        global $thisstaff;

        if(!$thisstaff)
            Http::response(403, 'Login Required');
        elseif(!($user = User::lookup($id)))
            Http::response(404, 'Unknown user');

        $info = array(
                'title' => '',
                'useredit' => sprintf('#users/%d/edit', $user->getId()),
                );
        ob_start();
        echo sprintf('<div style="width:650px; padding: 2px 2px 0 5px;"
                id="u%d">', $user->getId());
        include(STAFFINC_DIR . 'templates/user.tmpl.php');
        echo '</div>';
        $resp = ob_get_contents();
        ob_end_clean();

        return $resp;

    }

    function editUser($id) {
        global $thisstaff;

        if(!$thisstaff)
            Http::response(403, 'Login Required');
        elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
            Http::response(403, 'Permission Denied');
        elseif(!($user = User::lookup($id)))
            Http::response(404, 'Unknown user');

        $info = array(
            'title' => sprintf(__('Update %s'), Format::htmlchars($user->getName()))
        );
        $forms = $user->getForms();

        include(STAFFINC_DIR . 'templates/user.tmpl.php');
    }

    function updateUser($id) {
        global $thisstaff;

        if(!$thisstaff)
            Http::response(403, 'Login Required');
        elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
            Http::response(403, 'Permission Denied');
        elseif(!($user = User::lookup($id)))
            Http::response(404, 'Unknown user');

        $errors = array();
        $form = UserForm::getUserForm()->getForm($_POST);
        if (!is_string($form->getField('name')->getValue()))
            Http::response(404, 'Invalid Data');

        if ($user->updateInfo($_POST, $errors, true) && !$errors)
             Http::response(201, $user->to_json(),  'application/json');

        $forms = $user->getForms();
        include(STAFFINC_DIR . 'templates/user.tmpl.php');
    }

    static function register($id) {
        global $thisstaff;

        if (!$thisstaff)
            Http::response(403, 'Login Required');
        elseif (!$thisstaff->hasPerm(User::PERM_MANAGE))
            Http::response(403, 'Permission Denied');
        elseif (!($user = User::lookup($id)))
            Http::response(404, 'Unknown user');

        $errors = $info = array();
        if ($_POST) {
            // Register user on post
            if ($user->getAccount())
                $info['error'] = __('User already registered');
            elseif ($user->register($_POST, $errors))
                Http::response(201, 'Account created successfully');

            // Unable to create user.
            $info = Format::htmlchars($_POST);
            if ($errors['err'])
                $info['error'] = $errors['err'];
            else
                $info['error'] = sprintf('%s - %s', __('Unable to register user'), __('Please try again!'));
        }

        include(STAFFINC_DIR . 'templates/user-register.tmpl.php');
    }

    function manage($id, $target=null) {
        global $thisstaff;

        if (!$thisstaff)
            Http::response(403, 'Login Required');
        elseif (!$thisstaff->hasPerm(User::PERM_MANAGE))
            Http::response(403, 'Permission Denied');
        elseif (!($user = User::lookup($id)))
            Http::response(404, 'Unknown user');

        if (!($account = $user->getAccount()))
            return self::register($id);

        $errors = array();
        $info = $account->getInfo();

        if ($_POST) {
            if ($account->update($_POST, $errors))
                Http::response(201, 'Account updated successfully');

            // Unable to update account
            $info = Format::htmlchars($_POST);

            if ($errors['err'])
                $info['error'] = $errors['err'];
            else
                $info['error'] = __('Unable to update account.')
                    .' '.__('Correct any errors below and try again.');
        }

        $info['_target'] = $target;

        include(STAFFINC_DIR . 'templates/user-account.tmpl.php');
    }

    function delete($id) {
        global $thisstaff;

        if (!$thisstaff)
            Http::response(403, 'Login Required');
        elseif (!$thisstaff->hasPerm(User::PERM_DELETE))
            Http::response(403, 'Permission Denied');
        elseif (!($user = User::lookup($id)))
            Http::response(404, 'Unknown user');

        $info = array();
        if ($_POST) {
            if ($user->tickets->count()) {
                if ($_POST['deletetickets']) {
                    if (!$user->deleteAllTickets())
                        $info['error'] = __('You do not have permission to delete a user with tickets!');
                } else {
                    $info['error'] = __('You cannot delete a user with tickets!');
                }
            }

            if (!$info['error'] && $user->delete())
                 Http::response(204, 'User deleted successfully');
            elseif (!$info['error'])
                $info['error'] = sprintf('%s - %s', __('Unable to delete user'), __('Please try again!'));
        }

        include(STAFFINC_DIR . 'templates/user-delete.tmpl.php');
    }

    function getUser($id=false) {

        if(($user=User::lookup(($id) ? $id : $_REQUEST['id'])))
           Http::response(201, $user->to_json(), 'application/json');

        $info = array('error' => sprintf(__('%s: Unknown or invalid ID.'), _N('end user', 'end users', 1)));

        return self::_lookupform(null, $info);
    }

    function lookup() {
        return self::addUser();
    }

    static function addUser() {
        global $thisstaff;

        $info = array();

        if (!AuthenticationBackend::getSearchDirectories())
            $info['lookup'] = 'local';

        if ($_POST) {
            if (!$thisstaff->hasPerm(User::PERM_CREATE))
                Http::response(403, 'Permission Denied');

            $info['title'] = __('Add New User');
            $form = UserForm::getUserForm()->getForm($_POST);
            if (!is_string($form->getField('name')->getValue()))
                Http::response(404, 'Invalid Data');
            if (($user = User::fromForm($form)))
                Http::response(201, $user->to_json(), 'application/json');

            $info['error'] = sprintf('%s - %s', __('Error adding user'), __('Please try again!'));
        }

        return self::_lookupform($form, $info);
    }

    function addRemoteUser($bk, $id) {
        global $thisstaff;

        if (!$thisstaff)
            Http::response(403, 'Login Required');
        elseif (!$thisstaff->hasPerm(User::PERM_CREATE))
            Http::response(403, 'Permission Denied');
        elseif (!$bk || !$id)
            Http::response(422, 'Backend and user id required');
        elseif (!($backend = AuthenticationBackend::getSearchDirectoryBackend($bk))
                || !($user_info = $backend->lookup($id)))
            Http::response(404, 'User not found');

        $form = UserForm::getUserForm()->getForm($user_info);
        $info = array('title' => __(
            /* `remote` users are those in a remore directory such as LDAP */
            'Import Remote User'));
        if (!$user_info)
            $info['error'] = __('Unable to find user in directory');

        include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php');
    }

    function importUsers() {
        global $thisstaff;

        if (!$thisstaff)
            Http::response(403, 'Login Required');
        elseif (!$thisstaff->hasPerm(User::PERM_CREATE))
            Http::response(403, 'Permission Denied');

        $info = array(
            'title' => __('Import Users'),
            'action' => '#users/import',
            'upload_url' => "users.php?do=import-users",
        );

        if ($_POST) {
            $status = User::importFromPost($_POST['pasted']);
            if (is_string($status))
                $info['error'] = $status;
            else
                Http::response(201, "{\"count\": $status}");
        }
        $info += Format::input($_POST);

        include STAFFINC_DIR . 'templates/user-import.tmpl.php';
    }

    function selectUser($id) {
        global $thisstaff;

        if ($id)
            $user = User::lookup($id);

        $info = array('title' => __('Select User'));

        ob_start();
        include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php');
        $resp = ob_get_contents();
        ob_end_clean();
        return $resp;

    }

    static function _lookupform($form=null, $info=array()) {
        global $thisstaff;

        if (!$info or !$info['title']) {
            if ($thisstaff->hasPerm(User::PERM_CREATE))
                $info += array('title' => __('Lookup or create a user'));
            else
                $info += array('title' => __('Lookup a user'));
        }

        ob_start();
        include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php');
        $resp = ob_get_contents();
        ob_end_clean();
        return $resp;
    }

    function searchStaff() {
        global $thisstaff;

        if (!$thisstaff)
            Http::response(403, 'Login required for searching');
        elseif (!$thisstaff->isAdmin())
            Http::response(403,
                'Administrative privilege is required for searching');
        elseif (!isset($_REQUEST['q']))
            Http::response(400, 'Query argument is required');

        $users = array();
        foreach (AuthenticationBackend::getSearchDirectories() as $ab) {
            foreach ($ab->search($_REQUEST['q']) as $u)
                $users[] = $u;
        }

        return $this->json_encode($users);
    }

    function updateOrg($id, $orgId = 0) {
        global $thisstaff;

        if (!$thisstaff)
            Http::response(403, 'Login Required');
        elseif (!($user = User::lookup($id)))
            Http::response(404, 'Unknown user');

        $info = array();
        $info['title'] = sprintf(__('Organization for %s'),
            Format::htmlchars($user->getName()));
        $info['action'] = '#users/'.$user->getId().'/org';
        $info['onselect'] = 'ajax.php/users/'.$user->getId().'/org';

        if ($_POST) {
            if ($_POST['orgid']) { //Existing org.
                if (!($org = Organization::lookup($_POST['orgid'])))
                    $info['error'] = __('Unknown organization selected');
            } else { //Creating new org.
                $form = OrganizationForm::getDefaultForm()->getForm($_POST);
                if (!($org = Organization::fromForm($form)))
                    $info['error'] = __('Unable to create organization.')
                        .' '.__('Correct any errors below and try again.');
            }

            if ($org && $user->setOrganization($org))
                Http::response(201, $org->to_json(), 'application/json');
            elseif (! $info['error'])
                $info['error'] = __('Unable to add user to organization.')
                    .' '.__('Correct any errors below and try again.');

        } elseif ($orgId)
            $org = Organization::lookup($orgId);
        elseif ($org = $user->getOrganization()) {
            $info['title'] = sprintf(__('%s &mdash; Organization'), Format::htmlchars($user->getName()));
            $info['action'] = $info['onselect'] = '';
            $tmpl = 'org.tmpl.php';
        }

        if ($org && $user->getOrgId() && $org->getId() != $user->getOrgId())
            $info['warning'] = __("Are you sure you want to change the user's organization?");

        $tmpl = $tmpl ?: 'org-lookup.tmpl.php';

        ob_start();
        include(STAFFINC_DIR . "templates/$tmpl");
        $resp = ob_get_contents();
        ob_end_clean();

        return $resp;
    }

    function createNote($id) {
        if (!($user = User::lookup($id)))
            Http::response(404, 'Unknown user');

        require_once INCLUDE_DIR . 'ajax.note.php';
        $ajax = new NoteAjaxAPI();
        return $ajax->createNote('U'.$id);
    }

    function manageForms($user_id) {
        $forms = DynamicFormEntry::forObject($user_id, 'U');
        $info = array('action' => '#users/'.Format::htmlchars($user_id).'/forms/manage');
        include(STAFFINC_DIR . 'templates/form-manage.tmpl.php');
    }

    function updateForms($user_id) {
        global $thisstaff;

        if (!$thisstaff)
            Http::response(403, "Login required");
        elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
            Http::response(403, 'Permission Denied');
        elseif (!($user = User::lookup($user_id)))
            Http::response(404, "No such user");
        elseif (!isset($_POST['forms']))
            Http::response(422, "Send updated forms list");

        // Add new forms
        $forms = DynamicFormEntry::forObject($user_id, 'U');
        foreach ($_POST['forms'] as $sort => $id) {
            $found = false;
            foreach ($forms as $e) {
                if ($e->get('form_id') == $id) {
                    $e->set('sort', $sort);
                    $e->save();
                    $found = true;
                    break;
                }
            }
            // New form added
            if (!$found && ($new = DynamicForm::lookup($id))) {
                $user->addForm($new, $sort);
            }
        }

        // Deleted forms
        foreach ($forms as $idx => $e) {
            if (!in_array($e->get('form_id'), $_POST['forms']))
                $e->delete();
        }

        Http::response(201, 'Successfully managed');
    }

    function exportTickets($id) {
        global $thisstaff;

        if (!$thisstaff)
            Http::response(403, 'Agent login is required');
        elseif (!$id)
            Http::response(403, __('User ID Required'));

        $user = User::lookup($id);
        if (!$user)
            Http::response(403, __('User Not Found'));

        $queue = $user->getTicketsQueue();

        if ($_POST) {
            $api = new TicketsAjaxAPI();
            return $api->queueExport($queue);
        }

        $info = array('action' => "#users/$id/tickets/export");

        include STAFFINC_DIR . 'templates/queue-export.tmpl.php';
    }
}
?>