<?php
/*********************************************************************
    class.staff.php

    Everything about staff.

    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:
**********************************************************************/
include_once(INCLUDE_DIR.'class.ticket.php');
include_once(INCLUDE_DIR.'class.dept.php');
include_once(INCLUDE_DIR.'class.error.php');
include_once(INCLUDE_DIR.'class.team.php');
include_once(INCLUDE_DIR.'class.role.php');
include_once(INCLUDE_DIR.'class.passwd.php');
include_once(INCLUDE_DIR.'class.user.php');
include_once(INCLUDE_DIR.'class.auth.php');

class Staff extends VerySimpleModel
implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {

    static $meta = array(
        'table' => STAFF_TABLE,
        'pk' => array('staff_id'),
        'joins' => array(
            'dept' => array(
                'constraint' => array('dept_id' => 'Dept.id'),
            ),
            'role' => array(
                'constraint' => array('role_id' => 'Role.id'),
            ),
            'dept_access' => array(
                'reverse' => 'StaffDeptAccess.staff',
            ),
            'teams' => array(
                'reverse' => 'TeamMember.staff',
            ),
        ),
    );

    var $authkey;
    var $departments;
    var $stats = array();
    var $_extra;
    var $passwd_change;
    var $_roles = null;
    var $_teams = null;
    var $_config = null;
    var $_perm;

    function __onload() {

        // WE have to patch info here to support upgrading from old versions.
        $time = null;
        if (isset($this->passwdreset) && $this->passwdreset)
            $time=strtotime($this->passwdreset);
        elseif (isset($this->added) && $this->added)
            $time=strtotime($this->added);

        if ($time)
            $this->passwd_change = time()-$time; //XXX: check timezone issues.
    }

    function get($field, $default=false) {

       // Check primary fields
        try {
            return parent::get($field, $default);
        } catch (Exception $e) {}

        // Autoload config if not loaded already
        if (!isset($this->_config))
            $this->getConfig();

        if (isset($this->_config[$field]))
            return $this->_config[$field];
    }

    function getConfig() {

        if (!isset($this->_config) && $this->getId()) {
            $_config = new Config('staff.'.$this->getId(),
                    // Defaults
                    array(
                        'default_from_name' => '',
                        'datetime_format'   => '',
                        'thread_view_order' => '',
                        'default_ticket_queue_id' => 0,
                        'reply_redirect' => 'Ticket',
                        'img_att_view' => 'download',
                        'editor_spacing' => 'double',
                        ));
            $this->_config = $_config->getInfo();
        }

        return $this->_config;
    }

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

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

    static function getVarScope() {
      return array(
        'dept' => array('class' => 'Dept', 'desc' => __('Department')),
        'email' => __('Email Address'),
        'name' => array(
          'class' => 'PersonsName', 'desc' => __('Agent name'),
        ),
        'mobile' => __('Mobile Number'),
        'phone' => __('Phone Number'),
        'signature' => __('Signature'),
        'timezone' => "Agent's configured timezone",
        'username' => 'Access username',
      );
    }

    function getVar($tag) {
        switch ($tag) {
        case 'mobile':
            return Format::phone($this->ht['mobile']);
        case 'phone':
            return Format::phone($this->ht['phone']);
        }
    }

    static function getSearchableFields() {
        return array(
            'email' => new TextboxField(array(
                'label' => __('Email Address'),
            )),
        );
    }

    static function supportsCustomData() {
        return false;
    }

    function getHashtable() {
        $base = $this->ht;
        unset($base['teams']);
        unset($base['dept_access']);

        if ($this->getConfig())
            $base += $this->getConfig();

        return $base;
    }

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

    // AuthenticatedUser implementation...
    // TODO: Move to an abstract class that extends Staff
    function getUserType() {
        return 'staff';
    }

    function getAuthBackend() {
        list($bk, ) = explode(':', $this->getAuthKey());

        // If administering a user other than yourself, fallback to the
        // agent's declared backend, if any
        if (!$bk && $this->backend)
            $bk = $this->backend;

        return StaffAuthenticationBackend::getBackend($bk);
    }

    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;
    }

    /*compares user password*/
    function check_passwd($password, $autoupdate=true) {

        /*bcrypt based password match*/
        if(Passwd::cmp($password, $this->getPasswd()))
            return true;

        //Fall back to MD5
        if(!$password || strcmp($this->getPasswd(), MD5($password)))
            return false;

        //Password is a MD5 hash: rehash it (if enabled) otherwise force passwd change.
        $this->passwd = Passwd::hash($password);

        if(!$autoupdate || !$this->save())
            $this->forcePasswdRest();

        return true;
    }

    function cmp_passwd($password) {
        return $this->check_passwd($password, false);
    }

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

    function forcePasswdRest() {
        $this->change_passwd = 1;
        return $this->save();
    }

    /* check if passwd reset is due. */
    function isPasswdResetDue() {
        global $cfg;
        return ($cfg && $cfg->getPasswdResetPeriod()
                    && $this->passwd_change>($cfg->getPasswdResetPeriod()*30*24*60*60));
    }

    function setPassword($new, $current=false) {
        global $thisstaff;

        // Allow the backend to update the password. This is the preferred
        // method as it allows for integration with password policies and
        // also allows for remotely updating the password where possible and
        // supported.
        if (!($bk = $this->getAuthBackend())
            || !$bk instanceof AuthBackend
        ) {
            // Fallback to osTicket authentication token udpates
            $bk = new osTicketAuthentication();
        }

        // And now for the magic
        if (!$bk->supportsPasswordChange()) {
            throw new PasswordUpdateFailed(
                __('Authentication backend does not support password updates'));
        }
        // Backend should throw PasswordUpdateFailed directly
        $rv = $bk->setPassword($this, $new, $current);

        // Successfully updated authentication tokens
        $this->change_passwd = 0;
        $this->cancelResetTokens();
        $this->passwdreset = SqlFunction::NOW();

        // Clean sessions
        Signal::send('auth.clean', $this, $thisstaff);

        return $rv;
    }

    function canAccess($something) {
        if ($something instanceof RestrictedAccess)
            return $something->checkStaffPerm($this);

        return true;
    }

    function isPasswdChangeDue() {
        return $this->isPasswdResetDue();
    }

    function getRefreshRate() {
        return $this->auto_refresh_rate;
    }

    function getPageLimit() {
        return $this->max_page_size;
    }

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

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

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

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

    function getPasswd() {
        return $this->passwd;
    }

    function getName() {
        return new AgentsName(array('first' => $this->ht['firstname'], 'last' => $this->ht['lastname']));
    }

    function getAvatarAndName() {
        return $this->getAvatar().Format::htmlchars((string) $this->getName());
    }

    function getFirstName() {
        return $this->firstname;
    }

    function getLastName() {
        return $this->lastname;
    }

    function getSignature() {
        return $this->signature;
    }

    function getDefaultTicketQueueId() {
        return $this->default_ticket_queue_id;
    }

    function getDefaultSignatureType() {
        return $this->default_signature_type;
    }

    function getReplyFromNameType() {
        return $this->default_from_name;
    }

    function getDefaultPaperSize() {
        return $this->default_paper_size;
    }

    function getReplyRedirect() {
        return $this->reply_redirect;
    }

    function getImageAttachmentView() {
        return $this->img_att_view;
    }

    function editorSpacing() {
        return $this->editor_spacing;
    }

    function forcePasswdChange() {
        return $this->change_passwd;
    }

    function getDepartments() {
        // TODO: Cache this in the agent's session as it is unlikely to
        //       change while logged in

        if (!isset($this->departments)) {

            // Departments the staff is "allowed" to access...
            // based on the group they belong to + user's primary dept + user's managed depts.
            $sql='SELECT DISTINCT d.id FROM '.STAFF_TABLE.' s '
                .' LEFT JOIN '.STAFF_DEPT_TABLE.' g ON (s.staff_id=g.staff_id) '
                .' INNER JOIN '.DEPT_TABLE.' d ON (LOCATE(CONCAT("/", s.dept_id, "/"), d.path) OR d.manager_id=s.staff_id OR LOCATE(CONCAT("/", g.dept_id, "/"), d.path)) '
                .' WHERE s.staff_id='.db_input($this->getId());
            $depts = array();
            if (($res=db_query($sql)) && db_num_rows($res)) {
                while(list($id)=db_fetch_row($res))
                    $depts[] = (int) $id;
            }

            /* ORM method — about 2.0ms slower
            $q = Q::any(array(
                'path__contains' => '/'.$this->dept_id.'/',
                'manager_id' => $this->getId(),
            ));
            // Add in extended access
            foreach ($this->dept_access->depts->values_flat('dept_id') as $row) {
                // Skip primary dept
                if ($row[0] == $this->dept_id)
                    continue;
                $q->add(new Q(array('path__contains'=>'/'.$row[0].'/')));
            }

            $dept_ids = Dept::objects()
                ->filter($q)
                ->distinct('id')
                ->values_flat('id');

            foreach ($dept_ids as $row)
                $depts[] = $row[0];
            */

            $this->departments = $depts;
        }

        return $this->departments;
    }

    function getDepts() {
        return $this->getDepartments();
    }

    function getManagedDepartments() {

        return ($depts=Dept::getDepartments(
                    array('manager' => $this->getId())
                    ))?array_keys($depts):array();
    }

    function getDeptId() {
        return $this->dept_id;
    }

    function getDept() {
        return $this->dept;
    }

    function setDepartmentId($dept_id, $eavesdrop=false) {
        // Grant access to the current department
        $old = $this->dept_id;
        if ($eavesdrop) {
            $da = new StaffDeptAccess(array(
                'dept_id' => $old,
                'role_id' => $this->role_id,
            ));
            $da->setAlerts(true);
            $this->dept_access->add($da);
        }

        // Drop extended access to new department
        $this->dept_id = $dept_id;
        if ($da = $this->dept_access->findFirst(array(
            'dept_id' => $dept_id))
        ) {
            $this->dept_access->remove($da);
        }

        $this->save();
    }

    function usePrimaryRoleOnAssignment() {
        return $this->getExtraAttr('def_assn_role', true);
    }

    function getLanguage() {
        return (isset($this->lang)) ? $this->lang : false;
    }

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

    function getLocale() {
        //XXX: isset is required here to avoid possible crash when upgrading
        // installation where locale column doesn't exist yet.
        return isset($this->locale) ? $this->locale : 0;
    }

    function getRoles() {
        if (!isset($this->_roles)) {
            $this->_roles = array($this->dept_id => $this->role);
            foreach($this->dept_access as $da)
                $this->_roles[$da->dept_id] = $da->role;
        }

        return $this->_roles;
    }

    function getRole($dept=null, $assigned=false) {

        if (is_null($dept))
            return $this->role;

       if (is_numeric($dept))
          $deptId = $dept;
       elseif($dept instanceof Dept)
          $deptId = $dept->getId();
       else
          return null;

        $roles = $this->getRoles();
        if (isset($roles[$deptId]))
            return $roles[$deptId];

        // Default to primary role.
        if ($assigned && $this->usePrimaryRoleOnAssignment())
            return $this->role;

        // Ticket Create & View only access
        $perms = JSONDataEncoder::encode(array(
                    Ticket::PERM_CREATE => 1));
        return new Role(array('permissions' => $perms));
    }

    function hasPerm($perm, $global=true) {
        if ($global)
            return $this->getPermission()->has($perm);
        if ($this->getRole()->hasPerm($perm))
            return true;
        foreach ($this->dept_access as $da)
            if ($da->role->hasPerm($perm))
                return true;
        return false;
    }

    function canSearchEverything() {
        return $this->hasPerm(SearchBackend::PERM_EVERYTHING);
    }

    function canManageTickets() {
        return $this->hasPerm(Ticket::PERM_DELETE, false)
                || $this->hasPerm(Ticket::PERM_TRANSFER, false)
                || $this->hasPerm(Ticket::PERM_ASSIGN, false)
                || $this->hasPerm(Ticket::PERM_CLOSE, false);
    }

    function isManager() {
        return (($dept=$this->getDept()) && $dept->getManagerId()==$this->getId());
    }

    function isStaff() {
        return TRUE;
    }

    function isActive() {
        return $this->isactive;
    }

    function getStatus() {
        return $this->isActive() ? __('Active') : __('Locked');
    }

    function isVisible() {
         return $this->isvisible;
    }

    function onVacation() {
        return $this->onvacation;
    }

    function isAvailable() {
        return ($this->isActive() && !$this->onVacation());
    }

    function showAssignedOnly() {
        return $this->assigned_only;
    }

    function isAccessLimited() {
        return $this->showAssignedOnly();
    }

    function isAdmin() {
        return $this->isadmin;
    }

    function isTeamMember($teamId) {
        return ($teamId && in_array($teamId, $this->getTeams()));
    }

    function canAccessDept($dept) {

        if (!$dept instanceof Dept)
            return false;

        return (!$this->isAccessLimited()
                && in_array($dept->getId(), $this->getDepts()));
    }

    function getTeams() {

        if (!isset($this->_teams)) {
            $this->_teams = array();
            foreach ($this->teams as $team)
                 $this->_teams[] = (int) $team->team_id;
        }

        return $this->_teams;
    }

    function getTicketsVisibility($exclude_archived=false) {
        // -- Open and assigned to me
        $assigned = Q::any(array(
            'staff_id' => $this->getId(),
        ));
        $assigned->add(array('thread__referrals__agent__staff_id' => $this->getId()));
        $childRefAgent = Q::all(new Q(array('child_thread__object_type' => 'C',
            'child_thread__referrals__agent__staff_id' => $this->getId())));
        $assigned->add($childRefAgent);
        // -- Open and assigned to a team of mine
        if (($teams = array_filter($this->getTeams()))) {
            $assigned->add(array('team_id__in' => $teams));
            $assigned->add(array('thread__referrals__team__team_id__in' => $teams));
            $childRefTeam = Q::all(new Q(array('child_thread__object_type' => 'C',
                'child_thread__referrals__team__team_id__in' => $teams)));
            $assigned->add($childRefTeam);
        }
        $visibility = Q::any(new Q(array('status__state'=>'open', $assigned)));
        // -- If access is limited to assigned only, return assigned
        if ($this->isAccessLimited())
            return $visibility;
        // -- Routed to a department of mine
        if (($depts=$this->getDepts()) && count($depts)) {
            $in_dept = Q::any(array(
                'dept_id__in' => $depts,
                'thread__referrals__dept__id__in' => $depts,
            ));
            if ($exclude_archived) {
                $in_dept = Q::all(array(
                    'status__state__in' => ['open', 'closed'],
                    $in_dept,
                ));
            }
            $visibility->add($in_dept);
            $childRefDept = Q::all(new Q(array('child_thread__object_type' => 'C',
                'child_thread__referrals__dept__id__in' => $depts)));
            $visibility->add($childRefDept);
        }
        return $visibility;
    }

    function applyVisibility($query, $exclude_archived=false) {
        return $query->filter($this->getTicketsVisibility($exclude_archived));
    }

    /* stats */
    function resetStats() {
        $this->stats = array();
    }

    function getTasksStats() {

        if (!$this->stats['tasks'])
            $this->stats['tasks'] = Task::getStaffStats($this);

        return  $this->stats['tasks'];
    }

    function getNumAssignedTasks() {
        return ($stats=$this->getTasksStats()) ? $stats['assigned'] : 0;
    }

    function getNumClosedTasks() {
        return ($stats=$this->getTasksStats()) ? $stats['closed'] : 0;
    }

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

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

    function setExtraAttr($attr, $value, $commit=true) {
        $this->getExtraAttr();
        $this->_extra[$attr] = $value;
        $this->extra = JsonDataEncoder::encode($this->_extra);

        if ($commit) {
            $this->save();
        }
    }

    function getPermission() {
        if (!isset($this->_perm)) {
            $this->_perm = new RolePermission($this->permissions);
        }
        return $this->_perm;
    }

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

    function onLogin($bk) {
        // Update last apparent language preference
        $this->setExtraAttr('browser_lang',
            Internationalization::getCurrentLanguage(),
            false);

        $this->lastlogin = SqlFunction::NOW();
        $this->save();
    }

    //Staff profile update...unfortunately we have to separate it from admin update to avoid potential issues
    function updateProfile($vars, &$errors) {
        global $cfg;

        $vars['firstname']=Format::striptags($vars['firstname']);
        $vars['lastname']=Format::striptags($vars['lastname']);

        if (isset($this->staff_id) && $this->getId() != $vars['id'])
            $errors['err']=__('Internal error occurred');

        if(!$vars['firstname'])
            $errors['firstname']=__('First name is required');

        if(!$vars['lastname'])
            $errors['lastname']=__('Last name is required');

        if(!$vars['email'] || !Validator::is_valid_email($vars['email']))
            $errors['email']=__('Valid email is required');
        elseif(Email::getIdByEmail($vars['email']))
            $errors['email']=__('Already in-use as system email');
        elseif (($uid=static::getIdByEmail($vars['email']))
                && (!isset($this->staff_id) || $uid!=$this->getId()))
            $errors['email']=__('Email already in use by another agent');

        if($vars['phone'] && !Validator::is_phone($vars['phone']))
            $errors['phone']=__('Valid phone number is required');

        if($vars['mobile'] && !Validator::is_phone($vars['mobile']))
            $errors['mobile']=__('Valid phone number is required');

        if($vars['default_signature_type']=='mine' && !$vars['signature'])
            $errors['default_signature_type'] = __("You don't have a signature");

        // Update the user's password if requested
        if ($vars['passwd1']) {
            try {
                $this->setPassword($vars['passwd1'], $vars['cpasswd']);
            }
            catch (BadPassword $ex) {
                $errors['passwd1'] = $ex->getMessage();
            }
            catch (PasswordUpdateFailed $ex) {
                // TODO: Add a warning banner or crash the update
            }
        }

        $vars['onvacation'] = isset($vars['onvacation']) ? 1 : 0;
        $this->firstname = $vars['firstname'];
        $this->lastname = $vars['lastname'];
        $this->email = $vars['email'];
        $this->phone = Format::phone($vars['phone']);
        $this->phone_ext = $vars['phone_ext'];
        $this->mobile = Format::phone($vars['mobile']);
        $this->signature = Format::sanitize($vars['signature']);
        $this->timezone = $vars['timezone'];
        $this->locale = $vars['locale'];
        $this->max_page_size = $vars['max_page_size'];
        $this->auto_refresh_rate = $vars['auto_refresh_rate'];
        $this->default_signature_type = $vars['default_signature_type'];
        $this->default_paper_size = $vars['default_paper_size'];
        $this->lang = $vars['lang'];
        $this->onvacation = $vars['onvacation'];

        if (isset($vars['avatar_code']))
          $this->setExtraAttr('avatar', $vars['avatar_code']);

        if ($errors)
            return false;

        $_SESSION['::lang'] = null;
        TextDomain::configureForUser($this);

        // Update the config information
        $_config = new Config('staff.'.$this->getId());
        $_config->updateAll(array(
                    'datetime_format' => $vars['datetime_format'],
                    'default_from_name' => $vars['default_from_name'],
                    'thread_view_order' => $vars['thread_view_order'],
                    'default_ticket_queue_id' => $vars['default_ticket_queue_id'],
                    'reply_redirect' => ($vars['reply_redirect'] == 'Queue') ? 'Queue' : 'Ticket',
                    'img_att_view' => ($vars['img_att_view'] == 'inline') ? 'inline' : 'download',
                    'editor_spacing' => ($vars['editor_spacing'] == 'double') ? 'double' : 'single'
                    )
                );
        $this->_config = $_config->getInfo();

        return $this->save();
    }

    function updateTeams($membership, &$errors) {
        $dropped = array();
        foreach ($this->teams as $TM)
            $dropped[$TM->team_id] = 1;

        reset($membership);
        while(list(, list($team_id, $alerts)) = each($membership)) {
            $member = $this->teams->findFirst(array('team_id' => $team_id));
            if (!$member) {
                $this->teams->add($member = new TeamMember(array(
                    'team_id' => $team_id,
                )));
            }
            $member->setAlerts($alerts);
            if (!$errors)
                $member->save();
            unset($dropped[$member->team_id]);
        }
        if (!$errors && $dropped) {
            $member = $this->teams
                ->filter(array('team_id__in' => array_keys($dropped)))
                ->delete();
            $this->teams->reset();
        }
        return true;
    }

    function delete() {
        global $thisstaff;

        if (!$thisstaff || $this->getId() == $thisstaff->getId())
            return false;

        if (!parent::delete())
            return false;

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

        // DO SOME HOUSE CLEANING
        //Move remove any ticket assignments...TODO: send alert to Dept. manager?
        Ticket::objects()
            ->filter(array('staff_id' => $this->getId()))
            ->update(array('staff_id' => 0));

        //Update the poster and clear staff_id on ticket thread table.
        ThreadEntry::objects()
            ->filter(array('staff_id' => $this->getId()))
            ->update(array(
                'staff_id' => 0,
                'poster' => $this->getName()->getOriginal(),
            ));

        // Cleanup Team membership table.
        TeamMember::objects()
            ->filter(array('staff_id'=>$this->getId()))
            ->delete();

        // Cleanup staff dept access
        StaffDeptAccess::objects()
            ->filter(array('staff_id'=>$this->getId()))
            ->delete();

        return true;
    }

    /**** Static functions ********/
    static function lookup($var) {
        if (is_array($var))
            return parent::lookup($var);
        elseif (is_numeric($var))
            return parent::lookup(array('staff_id'=>$var));
        elseif (Validator::is_email($var))
            return parent::lookup(array('email'=>$var));
        elseif (is_string($var))
            return parent::lookup(array('username'=>$var));
        else
            return null;
    }

    static function getStaffMembers($criteria=array()) {
        global $cfg;

        $members = static::objects();

        if (isset($criteria['available'])) {
            $members = $members->filter(array(
                'onvacation' => 0,
                'isactive' => 1,
            ));
        }

        $members = self::nsort($members);

        $users=array();
        foreach ($members as $M) {
            $users[$M->getId()] = $M->getName();
        }

        return $users;
    }

    static function getAvailableStaffMembers() {
        return self::getStaffMembers(array('available'=>true));
    }

    static function getsortby($path='', $format=null) {
        global $cfg;

        $format = $format ?: $cfg->getAgentNameFormat();
        switch ($format) {
        case 'last':
        case 'lastfirst':
        case 'legal':
            $fields = array("{$path}lastname", "{$path}firstname");
            break;
        default:
            $fields = array("${path}firstname", "${path}lastname");
        }

        return $fields;
    }

    static function nsort(QuerySet $qs, $path='', $format=null) {
        $fields = self::getsortby($path, $format);
        $qs->order_by($fields);
        return $qs;
    }

    static function getIdByUsername($username) {
        $row = static::objects()->filter(array('username' => $username))
            ->values_flat('staff_id')->first();
        return $row ? $row[0] : 0;
    }

    static function getIdByEmail($email) {
        $row = static::objects()->filter(array('email' => $email))
            ->values_flat('staff_id')->first();
        return $row ? $row[0] : 0;
    }


    static function create($vars=false) {
        $staff = new static($vars);
        $staff->created = SqlFunction::NOW();
        return $staff;
    }

    function cancelResetTokens() {
        // TODO: Drop password-reset tokens from the config table for
        //       this user id
        $sql = 'DELETE FROM '.CONFIG_TABLE.' WHERE `namespace`="pwreset"
            AND `value`='.db_input($this->getId());
        db_query($sql, false);
        unset($_SESSION['_staff']['reset-token']);
    }

    function sendResetEmail($template='pwreset-staff', $log=true) {
        global $ost, $cfg;

        $content = Page::lookupByType($template);
        $token = Misc::randCode(48); // 290-bits

        if (!$content)
            return new BaseError(/* @trans */ 'Unable to retrieve password reset email template');

        $vars = array(
            'url' => $ost->getConfig()->getBaseUrl(),
            'token' => $token,
            'staff' => $this,
            'recipient' => $this,
            'reset_link' => sprintf(
                "%s/scp/pwreset.php?token=%s",
                $ost->getConfig()->getBaseUrl(),
                $token),
        );
        $vars['link'] = &$vars['reset_link'];

        if (!($email = $cfg->getAlertEmail()))
            $email = $cfg->getDefaultEmail();

        $info = array('email' => $email, 'vars' => &$vars, 'log'=>$log);
        Signal::send('auth.pwreset.email', $this, $info);

        if ($info['log'])
            $ost->logWarning(_S('Agent Password Reset'), sprintf(
             _S('Password reset was attempted for agent: %1$s<br><br>
                Requested-User-Id: %2$s<br>
                Source-Ip: %3$s<br>
                Email-Sent-To: %4$s<br>
                Email-Sent-Via: %5$s'),
                $this->getName(),
                $_POST['userid'],
                $_SERVER['REMOTE_ADDR'],
                $this->getEmail(),
                $email->getEmail()
            ), false);

        $lang = $this->lang ?: $this->getExtraAttr('browser_lang');
        $msg = $ost->replaceTemplateVariables(array(
            'subj' => $content->getLocalName($lang),
            'body' => $content->getLocalBody($lang),
        ), $vars);

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

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

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

        $importer = new CsvImporter($stream);
        $imported = 0;
        $fields = array(
            'firstname' => new TextboxField(array(
                'label' => __('First Name'),
            )),
            'lastname' => new TextboxField(array(
                'label' => __('Last Name'),
            )),
            'email' => new TextboxField(array(
                'label' => __('Email Address'),
                'configuration' => array(
                    'validator' => 'email',
                ),
            )),
            'username' => new TextboxField(array(
                'label' => __('Username'),
                'validators' => function($self, $value) {
                    if (!Validator::is_username($value))
                        $self->addError('Not a valid username');
                },
            )),
        );
        $form = new SimpleForm($fields);

        try {
            db_autocommit(false);
            $errors = array();
            $records = $importer->importCsv($form->getFields(), $defaults);
            foreach ($records as $data) {
                if (!isset($data['email']) || !isset($data['username']))
                    throw new ImportError('Both `username` and `email` fields are required');

                if ($agent = self::lookup(array('username' => $data['username']))) {
                    // TODO: Update the user
                }
                elseif ($agent = self::create($data, $errors)) {
                    if ($callback)
                        $callback($agent, $data);
                    $agent->save();
                }
                else {
                    throw new ImportError(sprintf(__('Unable to import (%s): %s'),
                        Format::htmlchars($data['username']),
                        print_r(Format::htmlchars($errors), true)
                    ));
                }
                $imported++;
            }
            db_autocommit(true);
        }
        catch (Exception $ex) {
            db_rollback();
            return $ex->getMessage();
        }
        return $imported;
    }

    function save($refetch=false) {
        if ($this->dirty)
            $this->updated = SqlFunction::NOW();
        return parent::save($refetch || $this->dirty);
    }

    function update($vars, &$errors) {
        $vars['username']=Format::striptags($vars['username']);
        $vars['firstname']=Format::striptags($vars['firstname']);
        $vars['lastname']=Format::striptags($vars['lastname']);

        if (isset($this->staff_id) && $this->getId() != $vars['id'])
            $errors['err']=__('Internal error occurred');

        if(!$vars['firstname'])
            $errors['firstname']=__('First name required');
        if(!$vars['lastname'])
            $errors['lastname']=__('Last name required');

        $error = '';
        if(!$vars['username'] || !Validator::is_username($vars['username'], $error))
            $errors['username']=($error) ? $error : __('Username is required');
        elseif (($uid=static::getIdByUsername($vars['username']))
                && (!isset($this->staff_id) || $uid!=$this->getId()))
            $errors['username']=__('Username already in use');

        if(!$vars['email'] || !Validator::is_valid_email($vars['email']))
            $errors['email']=__('Valid email is required');
        elseif(Email::getIdByEmail($vars['email']))
            $errors['email']=__('Already in use system email');
        elseif (($uid=static::getIdByEmail($vars['email']))
                && (!isset($this->staff_id) || $uid!=$this->getId()))
            $errors['email']=__('Email already in use by another agent');

        if($vars['phone'] && !Validator::is_phone($vars['phone']))
            $errors['phone']=__('Valid phone number is required');

        if($vars['mobile'] && !Validator::is_phone($vars['mobile']))
            $errors['mobile']=__('Valid phone number is required');

        if(!$vars['dept_id'])
            $errors['dept_id']=__('Department is required');
        if(!$vars['role_id'])
            $errors['role_id']=__('Role for primary department is required');

        $dept = Dept::lookup($vars['dept_id']);
        if($dept && !$dept->isActive())
          $errors['dept_id'] = sprintf(__('%s selected must be active'), __('Department'));

        // Ensure we will still have an administrator with access
        if ($vars['isadmin'] !== '1' || $vars['islocked'] === '1') {
            $sql = 'select count(*), max(staff_id) from '.STAFF_TABLE
                .' WHERE isadmin=1 and isactive=1';
            if (($res = db_query($sql))
                    && (list($count, $sid) = db_fetch_row($res))) {
                if ($count == 1 && $sid == $uid) {
                    $errors['isadmin'] = __(
                        'Cowardly refusing to remove or lock out the only active administrator'
                    );
                }
            }
        }

        // Update the local permissions
        $this->updatePerms($vars['perms'], $errors);

        //checkboxes
        $vars['isadmin'] = isset($vars['isadmin']) ? 1 : 0;
        $vars['islocked'] = isset($vars['islocked']) ? 0 : 1;
        $vars['isvisible'] = isset($vars['isvisible']) ? 1 : 0;
        $vars['onvacation'] = isset($vars['onvacation']) ? 1 : 0;
        $vars['assigned_only'] = isset($vars['assigned_only']) ? 1 : 0;

        $this->isadmin = $vars['isadmin'];
        $this->isactive = $vars['islocked'];
        $this->isvisible = $vars['isvisible'];
        $this->onvacation = $vars['onvacation'];
        $this->assigned_only = $vars['assigned_only'];
        $this->role_id = $vars['role_id'];
        $this->username = $vars['username'];
        $this->firstname = $vars['firstname'];
        $this->lastname = $vars['lastname'];
        $this->email = $vars['email'];
        $this->backend = $vars['backend'];
        $this->phone = Format::phone($vars['phone']);
        $this->phone_ext = $vars['phone_ext'];
        $this->mobile = Format::phone($vars['mobile']);
        $this->notes = Format::sanitize($vars['notes']);

        // Set staff password if exists
        if (!$vars['welcome_email'] && $vars['passwd1']) {
            $this->setPassword($vars['passwd1'], null);
            $this->change_passwd = $vars['change_passwd'] ? 1 : 0;
        }

        if ($errors)
            return false;

        if ($this->save()) {
            // Update some things for ::updateAccess to inspect
            $this->setDepartmentId($vars['dept_id']);

            // Format access update as [array(dept_id, role_id, alerts?)]
            $access = array();
            if (isset($vars['dept_access'])) {
                foreach (@$vars['dept_access'] as $dept_id) {
                    $access[] = array($dept_id, $vars['dept_access_role'][$dept_id],
                        @$vars['dept_access_alerts'][$dept_id]);
                }
            }
            $this->updateAccess($access, $errors);
            $this->setExtraAttr('def_assn_role',
                isset($vars['assign_use_pri_role']), false);

            // Format team membership as [array(team_id, alerts?)]
            $teams = array();
            if (isset($vars['teams'])) {
                foreach (@$vars['teams'] as $team_id) {
                    $teams[] = array($team_id, @$vars['team_alerts'][$team_id]);
                }
            }
            $this->updateTeams($teams, $errors);

            if ($vars['welcome_email'])
                $this->sendResetEmail('registration-staff', false);
            return true;
        }

        if (isset($this->staff_id)) {
            $errors['err']=sprintf(__('Unable to update %s.'), __('this agent'))
               .' '.__('Internal error occurred');
        } else {
            $errors['err']=sprintf(__('Unable to create %s.'), __('this agent'))
               .' '.__('Internal error occurred');
        }
        return false;
    }

    /**
     * Parameters:
     * $access - (<array($dept_id, $role_id, $alerts)>) a list of the complete,
     *      extended access for this agent. Any the agent currently has, which
     *      is not listed will be removed.
     * $errors - (<array>) list of error messages from the process, which will
     *      be indexed by the dept_id number.
     */
    function updateAccess($access, &$errors) {
        reset($access);
        $dropped = array();
        foreach ($this->dept_access as $DA)
            $dropped[$DA->dept_id] = 1;
        while (list(, list($dept_id, $role_id, $alerts)) = each($access)) {
            unset($dropped[$dept_id]);
            if (!$role_id || !Role::lookup($role_id))
                $errors['dept_access'][$dept_id] = __('Select a valid role');
            if (!$dept_id || !($dept=Dept::lookup($dept_id)))
                $errors['dept_access'][$dept_id] = __('Select a valid department');
            if ($dept_id == $this->getDeptId())
                $errors['dept_access'][$dept_id] = sprintf(__('Agent already has access to %s'), __('this department'));
            $da = $this->dept_access->findFirst(array('dept_id' => $dept_id));
            if (!isset($da)) {
                $da = new StaffDeptAccess(array(
                    'dept_id' => $dept_id, 'role_id' => $role_id
                ));
                $this->dept_access->add($da);
                $type = array('type' => 'edited',
                              'key' => sprintf('%s Department Access Added', $dept->getName()));
                Signal::send('object.edited', $this, $type);
            }
            else {
                $da->role_id = $role_id;
            }
            $da->setAlerts($alerts);
            if (!$errors)
                $da->save();
        }
        if (!$errors && $dropped) {
            $this->dept_access
                ->filter(array('dept_id__in' => array_keys($dropped)))
                ->delete();
            $this->dept_access->reset();
            foreach (array_keys($dropped) as $dept_id) {
                $deptName = Dept::getNameById($dept_id);
                $type = array('type' => 'edited',
                              'key' => sprintf('%s Department Access Removed', $deptName));
                Signal::send('object.edited', $this, $type);
            }
        }
        return !$errors;
    }

    function updatePerms($vars, &$errors=array()) {
        if (!$vars) {
            $this->permissions = '';
            return;
        }
        $permissions = $this->getPermission();
        foreach ($vars as $k => $val) {
             if (!array_key_exists($val, $permissions->perms)) {
                 $type = array('type' => 'edited', 'key' => $val);
                 Signal::send('object.edited', $this, $type);
             }
         }

        foreach (RolePermission::allPermissions() as $g => $perms) {
            foreach ($perms as $k => $v) {
                if (!in_array($k, $vars) && array_key_exists($k, $permissions->perms)) {
                     $type = array('type' => 'edited', 'key' => $k);
                     Signal::send('object.edited', $this, $type);
                 }
                $permissions->set($k, in_array($k, $vars) ? 1 : 0);
            }
        }
        $this->permissions = $permissions->toJson();
        return true;
    }

    static function export($criteria=null, $filename='') {
        include_once(INCLUDE_DIR.'class.error.php');

        $agents = Staff::objects();
        // Sort based on name formating
        $agents = self::nsort($agents);
        Export::agents($agents, $filename);
    }

}

interface RestrictedAccess {
    function checkStaffPerm($staff);
}

class StaffDeptAccess extends VerySimpleModel {
    static $meta = array(
        'table' => STAFF_DEPT_TABLE,
        'pk' => array('staff_id', 'dept_id'),
        'select_related' => array('dept', 'role'),
        'joins' => array(
            'dept' => array(
                'constraint' => array('dept_id' => 'Dept.id'),
            ),
            'staff' => array(
                'constraint' => array('staff_id' => 'Staff.staff_id'),
            ),
            'role' => array(
                'constraint' => array('role_id' => 'Role.id'),
            ),
        ),
    );

    const FLAG_ALERTS =     0x0001;

    function isAlertsEnabled() {
        return $this->flags & self::FLAG_ALERTS != 0;
    }

    function setFlag($flag, $value) {
        if ($value)
            $this->flags |= $flag;
        else
            $this->flags &= ~$flag;
    }

    function setAlerts($value) {
        $this->setFlag(self::FLAG_ALERTS, $value);
    }
}

/**
 * This form is used to administratively change the password. The
 * ChangePasswordForm is used for an agent to change their own password.
 */
class PasswordResetForm
extends AbstractForm {
    function buildFields() {
        return array(
            'welcome_email' => new BooleanField(array(
                'default' => true,
                'configuration' => array(
                    'desc' => __('Send the agent a password reset email'),
                ),
            )),
            'passwd1' => new PasswordField(array(
                'placeholder' => __('New Password'),
                'required' => true,
                'configuration' => array(
                    'classes' => 'span12',
                ),
                'visibility' => new VisibilityConstraint(
                    new Q(array('welcome_email' => false)),
                    VisibilityConstraint::HIDDEN
                ),
            )),
            'passwd2' => new PasswordField(array(
                'placeholder' => __('Confirm Password'),
                'required' => true,
                'configuration' => array(
                    'classes' => 'span12',
                ),
                'visibility' => new VisibilityConstraint(
                    new Q(array('welcome_email' => false)),
                    VisibilityConstraint::HIDDEN
                ),
            )),
            'change_passwd' => new BooleanField(array(
                'default' => true,
                'configuration' => array(
                    'desc' => __('Require password change at next login'),
                    'classes' => 'form footer',
                ),
                'visibility' => new VisibilityConstraint(
                    new Q(array('welcome_email' => false)),
                    VisibilityConstraint::HIDDEN
                ),
            )),
        );
    }

    function validate($clean) {
        if ($clean['passwd1'] != $clean['passwd2'])
            $this->getField('passwd1')->addError(__('Passwords do not match'));
    }
}

class PasswordChangeForm
extends AbstractForm {
    function buildFields() {
        $fields = array(
            'current' => new PasswordField(array(
                'placeholder' => __('Current Password'),
                'required' => true,
                'configuration' => array(
                    'autofocus' => true,
                ),
            )),
            'passwd1' => new PasswordField(array(
                'label' => __('Enter a new password'),
                'placeholder' => __('New Password'),
                'required' => true,
            )),
            'passwd2' => new PasswordField(array(
                'placeholder' => __('Confirm Password'),
                'required' => true,
            )),
        );

        // When using the password reset system, the current password is not
        // required for agents.
        if (isset($_SESSION['_staff']['reset-token'])) {
            unset($fields['current']);
            $fields['passwd1']->set('configuration', array('autofocus' => true));
        }
        else {
            $fields['passwd1']->set('layout',
                new GridFluidCell(12, array('style' => 'padding-top: 20px'))
            );
        }
        return $fields;
    }

    function getInstructions() {
        return __('Confirm your current password and enter a new password to continue');
    }

    function validate($clean) {
        if ($clean['passwd1'] != $clean['passwd2'])
            $this->getField('passwd1')->addError(__('Passwords do not match'));
    }
}

class ResetAgentPermissionsForm
extends AbstractForm {
    function buildFields() {
        $permissions = array();
        foreach (RolePermission::allPermissions() as $g => $perms) {
            foreach ($perms as $k => $v) {
                if (!$v['primary'])
                    continue;
                $permissions[$g][$k] = "{$v['title']} — {$v['desc']}";
            }
        }
        return array(
            'clone' => new ChoiceField(array(
                'default' => 0,
                'choices' =>
                    array(0 => '— '.__('Clone an existing agent').' —')
                    + Staff::getStaffMembers(),
                'configuration' => array(
                    'classes' => 'span12',
                ),
            )),
            'perms' => new ChoiceField(array(
                'choices' => $permissions,
                'widget' => 'TabbedBoxChoicesWidget',
                'configuration' => array(
                    'multiple' => true,
                ),
            )),
        );
    }

    function getClean($validate = true) {
        $clean = parent::getClean();
        // Index permissions as ['ticket.edit' => 1]
        $clean['perms'] = array_keys($clean['perms']);
        return $clean;
    }

    function render($staff=true, $title=false, $options=array()) {
        return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php'));
    }
}

class ChangeDepartmentForm
extends AbstractForm {
    function buildFields() {
        return array(
            'dept_id' => new ChoiceField(array(
                'default' => 0,
                'required' => true,
                'label' => __('Primary Department'),
                'choices' =>
                    array(0 => '— '.__('Primary Department').' —')
                    + Dept::getDepartments(),
                'configuration' => array(
                    'classes' => 'span12',
                ),
            )),
            'role_id' => new ChoiceField(array(
                'default' => 0,
                'required' => true,
                'label' => __('Primary Role'),
                'choices' =>
                    array(0 => '— '.__('Corresponding Role').' —')
                    + Role::getRoles(),
                'configuration' => array(
                    'classes' => 'span12',
                ),
            )),
            'eavesdrop' => new BooleanField(array(
                'configuration' => array(
                    'desc' => __('Maintain access to current primary department'),
                    'classes' => 'form footer',
                ),
            )),
            // alerts?
        );
    }

    function getInstructions() {
        return __('Change the primary department and primary role of the selected agents');
    }

    function getClean($validate = true) {
        $clean = parent::getClean();
        $clean['eavesdrop'] = $clean['eavesdrop'] ? 1 : 0;
        return $clean;
    }

    function render($staff=true, $title=false, $options=array()) {
        return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php'));
    }
}

class StaffQuickAddForm
extends AbstractForm {
    static $layout = 'GridFormLayout';

    function buildFields() {
        global $cfg;

        return array(
            'firstname' => new TextboxField(array(
                'required' => true,
                'configuration' => array(
                    'placeholder' => __("First Name"),
                    'autofocus' => true,
                ),
                'layout' => new GridFluidCell(6),
            )),
            'lastname' => new TextboxField(array(
                'required' => true,
                'configuration' => array(
                    'placeholder' => __("Last Name"),
                ),
                'layout' => new GridFluidCell(6),
            )),
            'email' => new TextboxField(array(
                'required' => true,
                'configuration' => array(
                    'validator' => 'email',
                    'placeholder' => __('Email Address — e.g. me@mycompany.com'),
                    'length' => 128,
                  ),
            )),
            'dept_id' => new ChoiceField(array(
                'label' => __('Department'),
                'required' => true,
                'choices' => Dept::getDepartments(),
                'default' => $cfg->getDefaultDeptId(),
                'layout' => new GridFluidCell(6),
            )),
            'role_id' => new ChoiceField(array(
                'label' => __('Primary Role'),
                'required' => true,
                'choices' =>
                    array(0 => __('Select Role'))
                    + Role::getRoles(),
                'layout' => new GridFluidCell(6),
            )),
            'isadmin' => new BooleanField(array(
                'label' => __('Account Type'),
                'configuration' => array(
                    'desc' => __('Agent has access to the admin panel'),
                ),
                'layout' => new GridFluidCell(6),
            )),
            'welcome_email' => new BooleanField(array(
                'configuration' => array(
                    'desc' => __('Send a welcome email with login information'),
                ),
                'default' => true,
                'layout' => new GridFluidCell(12, array('style' => 'padding-top: 50px')),
            )),
            'passwd1' => new PasswordField(array(
                'required' => true,
                'configuration' => array(
                    'placeholder' => __("Temporary Password"),
                ),
                'visibility' => new VisibilityConstraint(
                    new Q(array('welcome_email' => false))
                ),
                'layout' => new GridFluidCell(6),
            )),
            'passwd2' => new PasswordField(array(
                'required' => true,
                'configuration' => array(
                    'placeholder' => __("Confirm Password"),
                ),
                'visibility' => new VisibilityConstraint(
                    new Q(array('welcome_email' => false))
                ),
                'layout' => new GridFluidCell(6),
            )),
            // TODO: Add role_id drop-down
        );
    }

    function getClean($validate = true) {
        $clean = parent::getClean();
        list($clean['username'],) = preg_split('/[^\w.-]/u', $clean['email'], 2);
        if (mb_strlen($clean['username']) < 3 || Staff::lookup($clean['username']))
            $clean['username'] = mb_strtolower($clean['firstname']);


        // Inherit default dept's role as primary role
        $clean['assign_use_pri_role'] = true;

        // Default permissions
        $clean['perms'] = array(
            User::PERM_CREATE,
            User::PERM_EDIT,
            User::PERM_DELETE,
            User::PERM_MANAGE,
            User::PERM_DIRECTORY,
            Organization::PERM_CREATE,
            Organization::PERM_EDIT,
            Organization::PERM_DELETE,
            FAQ::PERM_MANAGE,
        );
        return $clean;
    }
}
