Skip to content
Snippets Groups Projects
class.ticket.php 121 KiB
Newer Older
Jared Hancock's avatar
Jared Hancock committed
<?php
/*********************************************************************
    class.ticket.php

    The most important class! Don't play with fire please.

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
Jared Hancock's avatar
Jared Hancock committed
    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.thread.php');
Jared Hancock's avatar
Jared Hancock committed
include_once(INCLUDE_DIR.'class.staff.php');
include_once(INCLUDE_DIR.'class.client.php');
Jared Hancock's avatar
Jared Hancock committed
include_once(INCLUDE_DIR.'class.team.php');
include_once(INCLUDE_DIR.'class.email.php');
include_once(INCLUDE_DIR.'class.dept.php');
include_once(INCLUDE_DIR.'class.topic.php');
include_once(INCLUDE_DIR.'class.lock.php');
include_once(INCLUDE_DIR.'class.file.php');
include_once(INCLUDE_DIR.'class.attachment.php');
include_once(INCLUDE_DIR.'class.banlist.php');
include_once(INCLUDE_DIR.'class.template.php');
include_once(INCLUDE_DIR.'class.variable.php');
Jared Hancock's avatar
Jared Hancock committed
include_once(INCLUDE_DIR.'class.priority.php');
Peter Rotich's avatar
Peter Rotich committed
include_once(INCLUDE_DIR.'class.sla.php');
include_once(INCLUDE_DIR.'class.canned.php');
Jared Hancock's avatar
Jared Hancock committed
require_once(INCLUDE_DIR.'class.dynamic_forms.php');
require_once(INCLUDE_DIR.'class.user.php');
require_once(INCLUDE_DIR.'class.collaborator.php');
Peter Rotich's avatar
Peter Rotich committed
require_once(INCLUDE_DIR.'class.task.php');
require_once(INCLUDE_DIR.'class.faq.php');
class TicketModel extends VerySimpleModel {
    static $meta = array(
        'table' => TICKET_TABLE,
        'pk' => array('ticket_id'),
        'joins' => array(
            'user' => array(
                'constraint' => array('user_id' => 'User.id')
            ),
            'status' => array(
                'constraint' => array('status_id' => 'TicketStatus.id')
            ),
            'lock' => array(
                'constraint' => array('lock_id' => 'Lock.lock_id'),
                'null' => true,
            ),
            'dept' => array(
                'constraint' => array('dept_id' => 'Dept.id'),
            'sla' => array(
                'constraint' => array('sla_id' => 'SlaModel.id'),
                'null' => true,
            ),
Jared Hancock's avatar
Jared Hancock committed
                'constraint' => array('staff_id' => 'Staff.staff_id'),
                'null' => true,
            ),
            'team' => array(
                'constraint' => array('team_id' => 'Team.team_id'),
                'null' => true,
            ),
            'topic' => array(
                'constraint' => array('topic_id' => 'Topic.topic_id'),
                'null' => true,
            ),
                'reverse' => 'Thread.ticket',
                'list' => false,
                'null' => true,
            ),
            'cdata' => array(
                'reverse' => 'TicketCData.ticket',
                'list' => false,
            ),
        )
    );

    const PERM_CREATE   = 'ticket.create';
    const PERM_EDIT     = 'ticket.edit';
    const PERM_ASSIGN   = 'ticket.assign';
    const PERM_TRANSFER = 'ticket.transfer';
    const PERM_REPLY    = 'ticket.reply';
    const PERM_CLOSE    = 'ticket.close';
    const PERM_DELETE   = 'ticket.delete';


    static protected $perms = array(
            self::PERM_CREATE => array(
Peter Rotich's avatar
Peter Rotich committed
                'title' =>
                /* @trans */ 'Create',
Peter Rotich's avatar
Peter Rotich committed
                'desc'  =>
                /* @trans */ 'Ability to open tickets on behalf of users'),
            self::PERM_EDIT => array(
Peter Rotich's avatar
Peter Rotich committed
                'title' =>
                /* @trans */ 'Edit',
Peter Rotich's avatar
Peter Rotich committed
                'desc'  =>
                /* @trans */ 'Ability to edit tickets'),
            self::PERM_ASSIGN => array(
Peter Rotich's avatar
Peter Rotich committed
                'title' =>
                /* @trans */ 'Assign',
Peter Rotich's avatar
Peter Rotich committed
                'desc'  =>
                /* @trans */ 'Ability to assign tickets to agents or teams'),
            self::PERM_TRANSFER => array(
Peter Rotich's avatar
Peter Rotich committed

                'title' =>
                /* @trans */ 'Transfer',
Peter Rotich's avatar
Peter Rotich committed
                'desc'  =>
                /* @trans */ 'Ability to transfer tickets between departments'),
            self::PERM_REPLY => array(
Peter Rotich's avatar
Peter Rotich committed
                'title' =>
                /* @trans */ 'Post Reply',
Peter Rotich's avatar
Peter Rotich committed
                'desc'  =>
                /* @trans */ 'Ability to post a ticket reply'),
            self::PERM_CLOSE => array(
Peter Rotich's avatar
Peter Rotich committed
                'title' =>
                /* @trans */ 'Close',
Peter Rotich's avatar
Peter Rotich committed
                'desc'  =>
                /* @trans */ 'Ability to close tickets'),
            self::PERM_DELETE => array(
Peter Rotich's avatar
Peter Rotich committed
                'title' =>
                /* @trans */ 'Delete',
Peter Rotich's avatar
Peter Rotich committed
                'desc'  =>
                /* @trans */ 'Ability to delete tickets'),
            );

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

    function getEffectiveDate() {
         return Format::datetime(max(
             strtotime($this->lastmessage),
             strtotime($this->closed),
             strtotime($this->reopened),
             strtotime($this->created)
    }

    function delete() {

        if (($ticket=Ticket::lookup($this->getId())) && @$ticket->delete())
            return true;

        return false;
    }

    static function registerCustomData(DynamicForm $form) {
        if (!isset(static::$meta['joins']['cdata+'.$form->id])) {
            $cdata_class = <<<EOF
class DynamicForm{$form->id} extends DynamicForm {
    static function getInstance() {
        static \$instance;
        if (!isset(\$instance))
            \$instance = static::lookup({$form->id});
        return \$instance;
    }
}
class TicketCdataForm{$form->id} {
    static \$meta = array(
        'view' => true,
        'pk' => array('ticket_id'),
        'joins' => array(
            'ticket' => array(
                'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
            ),
        )
    );
    static function getQuery(\$compiler) {
        return '('.DynamicForm{$form->id}::getCrossTabQuery('T', 'ticket_id').')';
    }
}
EOF;
            eval($cdata_class);
            static::$meta['joins']['cdata+'.$form->id] = array(
                'reverse' => 'TicketCdataForm'.$form->id.'.ticket',
                'null' => true,
            );
            // This may be necessary if the model has already been inspected
            if (static::$meta instanceof ModelMeta)
                static::$meta->processJoin(static::$meta['joins']['cdata+'.$form->id]);
        }
    }

    static function getPermissions() {
        return self::$perms;
    }
RolePermission::register(/* @trans */ 'Tickets', TicketModel::getPermissions(), true);
class TicketCData extends VerySimpleModel {
    static $meta = array(
        'pk' => array('ticket_id'),
        'joins' => array(
            'ticket' => array(
                'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
            ),
            ':priority' => array(
                'constraint' => array('priority' => 'Priority.priority_id'),
                'null' => true,
            ),
        ),
    );
}
TicketCData::$meta['table'] = TABLE_PREFIX . 'ticket__cdata';

class Ticket
implements RestrictedAccess, Threadable, TemplateVariable {
Jared Hancock's avatar
Jared Hancock committed

    var $id;
Jared Hancock's avatar
Jared Hancock committed

    var $lastMsgId;
    var $status;
Jared Hancock's avatar
Jared Hancock committed
    var $dept;  //Dept obj
    var $sla;   // SLA obj
    var $staff; //Staff obj
    var $client; //Client Obj
Jared Hancock's avatar
Jared Hancock committed
    var $team;  //Team obj
    var $topic; //Topic obj
    var $tlock; //TicketLock obj
Jared Hancock's avatar
Jared Hancock committed
        $this->id = 0;
        $this->load($id);
    }
Jared Hancock's avatar
Jared Hancock committed
    function load($id=0) {

Peter Rotich's avatar
Peter Rotich committed
        if (!$id && !($id=$this->getId()))
Jared Hancock's avatar
Jared Hancock committed
            return false;

        $sql='SELECT  ticket.*, thread.id as thread_id, ticket.lock_id, dept.name as dept_name '
Peter Rotich's avatar
Peter Rotich committed
            .' ,count(distinct attach.id) as attachments'
Peter Rotich's avatar
Peter Rotich committed
            .' ,count(distinct task.id) as tasks'
Jared Hancock's avatar
Jared Hancock committed
            .' FROM '.TICKET_TABLE.' ticket '
            .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.id) '
            .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) '
            .' LEFT JOIN '.LOCK_TABLE.' tlock
                ON ( ticket.lock_id=tlock.lock_id AND tlock.expire>NOW()) '
Peter Rotich's avatar
Peter Rotich committed
            .' LEFT JOIN '.TASK_TABLE.' task
                ON ( task.object_id = ticket.ticket_id AND task.object_type="T" ) '
Peter Rotich's avatar
Peter Rotich committed
            .' LEFT JOIN '.THREAD_TABLE.' thread
                ON ( thread.object_id = ticket.ticket_id AND thread.object_type="T" ) '
            .' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry
                ON ( entry.thread_id = thread.id ) '
            .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
                ON ( attach.object_id = entry.id AND attach.`type` = "H") '
Jared Hancock's avatar
Jared Hancock committed
            .' WHERE ticket.ticket_id='.db_input($id)
            .' GROUP BY ticket.ticket_id';

        //echo $sql;
Peter Rotich's avatar
Peter Rotich committed
        if (!($res=db_query($sql)) || !db_num_rows($res))
Jared Hancock's avatar
Jared Hancock committed
            return false;

Jared Hancock's avatar
Jared Hancock committed
        $this->id       = $this->ht['ticket_id'];
        $this->number   = $this->ht['number'];
        $this->_answers = array();
Jared Hancock's avatar
Jared Hancock committed
        $this->loadDynamicData();

Jared Hancock's avatar
Jared Hancock committed
        //Reset the sub classes (initiated ondemand)...good for reloads.
        $this->status= null;
Jared Hancock's avatar
Jared Hancock committed
        $this->staff = null;
        $this->client = null;
Jared Hancock's avatar
Jared Hancock committed
        $this->team  = null;
        $this->dept = null;
        $this->sla = null;
        $this->tlock = null;
        $this->stats = null;
        $this->topic = null;
        $this->collaborators = null;
Jared Hancock's avatar
Jared Hancock committed
        return true;
    }
Jared Hancock's avatar
Jared Hancock committed
    function loadDynamicData() {
        if (!$this->_answers) {
            foreach (DynamicFormEntry::forTicket($this->getId(), true) as $form) {
                foreach ($form->getAnswers() as $answer) {
                    $tag = mb_strtolower($answer->field->name)
                        ?: 'field.' . $answer->field->id;
                        $this->_answers[$tag] = $answer;
        return $this->_answers;
Jared Hancock's avatar
Jared Hancock committed
    function reload() {
        return $this->load();
    }
    function hasState($state) {
        return  (strcasecmp($this->getState(), $state)==0);
    }

Jared Hancock's avatar
Jared Hancock committed
    function isOpen() {
        return $this->hasState('open');
Jared Hancock's avatar
Jared Hancock committed
    }

    function isReopened() {
        return ($this->getReopenDate());
    }

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

Jared Hancock's avatar
Jared Hancock committed
    function isClosed() {
         return $this->hasState('closed');
    }

    function isArchived() {
         return $this->hasState('archived');
    }

    function isDeleted() {
         return $this->hasState('deleted');
Jared Hancock's avatar
Jared Hancock committed
    }

    function isAssigned() {
        return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId()));
    }

    function isOverdue() {
Jared Hancock's avatar
Jared Hancock committed
    function isAnswered() {
       return ($this->ht['isanswered']);
    }

    function isLocked() {
        return null !== $this->getLock();
    function checkStaffPerm($staff, $perm=null) {
        // Must be a valid staff
        if (!$staff instanceof Staff && !($staff=Staff::lookup($staff)))
Jared Hancock's avatar
Jared Hancock committed
            return false;

        // Check access based on department or assignment
        if (!(!$staff->showAssignedOnly()
                    && $staff->canAccessDept($this->getDeptId()))
                // only open tickets can be considered assigned
                && $this->isOpen()
                && $staff->getId() != $this->getStaffId()
                && !$staff->isTeamMember($this->getTeamId()))
        // At this point staff has view access unless a specific permission is
        // requested
        if ($perm === null)
        // Permission check requested -- get role.
        if (!($role=$staff->getRole($this->getDeptId())))
            return false;

        // Check permission based on the effective role
        return $role->hasPerm($perm);
    function checkUserAccess($user) {
        if (!$user || !($user instanceof EndUser))
        //Ticket Owner
        if ($user->getId() == $this->getUserId())
        //Collaborator?
        // 1) If the user was authorized via this ticket.
        if ($user->getTicketId() == $this->getId()
                && !strcasecmp($user->getUserType(), 'collaborator'))
            return true;

        // 2) Query the database to check for expanded access...
        if (Collaborator::lookup(array(
                        'user_id' => $user->getId(),
                        'thread_id' => $this->getThreadId())))
Jared Hancock's avatar
Jared Hancock committed
    //Getters
Jared Hancock's avatar
Jared Hancock committed
        return  $this->id;
    }

    function getOwnerId() {
        return $this->ht['user_id'];
    }

    function getOwner() {

        if (!isset($this->owner)
                && ($u=User::lookup($this->getOwnerId())))
            $this->owner = new TicketOwner(new EndUser($u), $this);

        return $this->owner;
Jared Hancock's avatar
Jared Hancock committed
    function getEmail(){
        if ($o = $this->getOwner())
            return $o->getEmail();
        return null;
    }

    function getReplyToEmail() {
        //TODO: Determine the email to use (once we enable multi-email support)
        return $this->getEmail();
    // Deprecated
    function getOldAuthToken() {
        # XXX: Support variable email address (for CCs)
        return md5($this->getId() . strtolower($this->getEmail()) . SECRET_SALT);
Jared Hancock's avatar
Jared Hancock committed
    function getName(){
        if ($o = $this->getOwner())
            return $o->getName();
Jared Hancock's avatar
Jared Hancock committed
    }

    function getSubject() {
        return (string) $this->_answers['subject'];
Jared Hancock's avatar
Jared Hancock committed
    }

    /* Help topic title  - NOT object -> $topic */
    function getHelpTopic() {

        if(!$this->ht['helptopic'] && ($topic=$this->getTopic()))
            $this->ht['helptopic'] = $topic->getFullName();

    function getCreateDate() {
        return $this->ht['created'];
Jared Hancock's avatar
Jared Hancock committed
    }

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

    function getReopenDate() {

    function getUpdateDate() {
        return $this->ht['updated'];
    function getEffectiveDate() {
        return $this->ht['lastupdate'];
    }

    function getDueDate() {
        return $this->ht['duedate'];
    function getSLADueDate() {
        if ($sla = $this->getSLA()) {
            $dt = new DateTime($this->getCreateDate());

            return $dt
                ->add(new DateInterval('PT' . $sla->getGracePeriod() . 'H'))
                ->format('Y-m-d H:i:s');
        }
    }

    function updateEstDueDate() {
        $estimatedDueDate = $this->getEstDueDate();
        if ($estimatedDueDate != $this->ht['est_duedate']) {
            $sql = 'UPDATE '.TICKET_TABLE.' SET `est_duedate`='.db_input($estimatedDueDate)
                .' WHERE `ticket_id`='.db_input($this->getId());
            db_query($sql);
        }
    }

    function getEstDueDate() {

        if(($duedate=$this->getDueDate()))
            return $duedate;

        //return sla due date (If ANY)
        return $this->getSLADueDate();
    }

    function getCloseDate() {
        return $this->ht['closed'];
    function getStatusId() {
        return $this->ht['status_id'];
    }

    /**
     * setStatusId
     *
     * Forceably set the ticket status ID to the received status ID. No
     * checks are made. Use ::setStatus() to change the ticket status
     */
    // XXX: Use ::setStatus to change the status. This can be used as a
    //      fallback if the logic in ::setStatus fails.
    function setStatusId($id) {
        $sql = 'UPDATE '.TICKET_TABLE.' SET updated=NOW() '.
Peter Rotich's avatar
Peter Rotich committed
               ' ,status_id='.db_input($id) .
               ' WHERE ticket_id='.db_input($this->getId());

Peter Rotich's avatar
Peter Rotich committed
        return (db_query($sql) && db_affected_rows());

        if (!$this->status && $this->getStatusId())
            $this->status = TicketStatus::lookup($this->getStatusId());

        return $this->status;
    }

    function getState() {

        if (!$this->getStatus())
            return '';

        return $this->getStatus()->getState();

    function getDeptId() {
       return $this->ht['dept_id'];

    function getDeptName() {

        if(!$this->ht['dept_name'] && ($dept = $this->getDept()))
            $this->ht['dept_name'] = $dept->getFullName();
Jared Hancock's avatar
Jared Hancock committed
    }

    function getPriorityId() {
        if (($a = $this->_answers['priority'])
                && ($b = $a->getValue()))
            return $b->getId();
        return $cfg->getDefaultPriorityId();
    function getPriority() {
        if (($a = $this->_answers['priority']) && ($b = $a->getValue()))
            return $b->getDesc();
        return '';
Jared Hancock's avatar
Jared Hancock committed
    function getPhoneNumber() {
        return (string)$this->getOwner()->getPhoneNumber();
Jared Hancock's avatar
Jared Hancock committed
    }

    function getSource() {
        return $this->ht['source'];
    }
Jared Hancock's avatar
Jared Hancock committed
    function getIP() {
        return $this->ht['ip_address'];
    }

Peter Rotich's avatar
Peter Rotich committed
    function getHashtable() {
        return $this->ht;
    }

    function getUpdateInfo() {
        global $cfg;
Peter Rotich's avatar
Peter Rotich committed

        $info=array('source'    =>  $this->getSource(),
Peter Rotich's avatar
Peter Rotich committed
                    'topicId'   =>  $this->getTopicId(),
                    'slaId' =>  $this->getSLAId(),
                    'user_id' => $this->getOwnerId(),
                    'duedate'   =>  $this->getDueDate()
                        ? Format::date($this->getDueDate())
                    'time'  =>  $this->getDueDate()?(Format::date($this->getDueDate(), true, 'HH:mm')):'',
Peter Rotich's avatar
Peter Rotich committed
                    );
Peter Rotich's avatar
Peter Rotich committed
        return $info;
    }

        if (!isset($this->tlock) && $this->ht['lock_id'])
            $this->tlock = Lock::lookup($this->ht['lock_id']);

Jared Hancock's avatar
Jared Hancock committed
        return $this->tlock;
    }
Jared Hancock's avatar
Jared Hancock committed
    function acquireLock($staffId, $lockTime) {
Jared Hancock's avatar
Jared Hancock committed
        if(!$staffId or !$lockTime) //Lockig disabled?
            return null;

        //Check if the ticket is already locked.
        if(($lock=$this->getLock()) && !$lock->isExpired()) {
            if($lock->getStaffId()!=$staffId) //someone else locked the ticket.
                return null;

            //Lock already exits...renew it
            $lock->renew($lockTime); //New clock baby.
Jared Hancock's avatar
Jared Hancock committed
            return $lock;
        }
        //No lock on the ticket or it is expired
        $this->tlock = Lock::acquire($staffId, $lockTime); //Create a new lock..

        if ($this->tlock) {
            $sql = 'UPDATE '.TICKET_TABLE.' SET `lock_id` = '
                .db_input($this->tlock->getId())
                .' WHERE `ticket_id` = '. db_input($this->getId());
            db_query($sql);
        }

Jared Hancock's avatar
Jared Hancock committed
        //load and return the newly created lock if any!
        return $this->tlock;
    function releaseLock($staffId=false) {
        if (!($lock = $this->getLock()))
            return false;

        if ($staffId && $lock->staff_id != $staffId)
            return false;

        if (!$lock->delete())
            return false;

        $sql = 'UPDATE '.TICKET_TABLE.' SET `lock_id` = 0 WHERE `ticket_id` = '
            . db_input($this->getId());
        return ($res = db_query($sql)) && db_affected_rows($res);
    }

        if(!$this->dept)
            if(!($this->dept = Dept::lookup($this->getDeptId())))
                $this->dept = $cfg->getDefaultDept();
Jared Hancock's avatar
Jared Hancock committed

        return $this->dept;
    }
    function getUserId() {
        return $this->getOwnerId();
    }
    function getUser() {
        if(!isset($this->user) && $this->getOwner())
            $this->user = new EndUser($this->getOwner());
        return $this->user;

    function getStaffId() {
        return $this->ht['staff_id'];
Jared Hancock's avatar
Jared Hancock committed

        if(!$this->staff && $this->getStaffId())
            $this->staff= Staff::lookup($this->getStaffId());

        return $this->staff;
    }

    function getTeamId() {
        return $this->ht['team_id'];
Jared Hancock's avatar
Jared Hancock committed

        if(!$this->team && $this->getTeamId())
            $this->team = Team::lookup($this->getTeamId());

        return $this->team;
    }

    function getAssignee() {

        if($staff=$this->getStaff())
            return $staff->getName();

        if($team=$this->getTeam())
            return $team->getName();

        return '';
    }

Peter Rotich's avatar
Peter Rotich committed
    function getAssignees() {
        $assignees=array();
Peter Rotich's avatar
Peter Rotich committed
        if($staff=$this->getStaff())
            $assignees[] = $staff->getName();
Peter Rotich's avatar
Peter Rotich committed
        if($team=$this->getTeam())
            $assignees[] = $team->getName();
Peter Rotich's avatar
Peter Rotich committed

        return $assignees;
    }
    function getAssigned($glue='/') {
        $assignees = $this->getAssignees();
        return $assignees?implode($glue, $assignees):'';
    }

    function getTopicId() {
Jared Hancock's avatar
Jared Hancock committed

        if(!$this->topic && $this->getTopicId())
            $this->topic = Topic::lookup($this->getTopicId());
Jared Hancock's avatar
Jared Hancock committed

        return $this->topic;
    }

Jared Hancock's avatar
Jared Hancock committed
    function getSLAId() {
Jared Hancock's avatar
Jared Hancock committed
    }

    function getSLA() {

        if(!$this->sla && $this->getSLAId())
Peter Rotich's avatar
Peter Rotich committed
            $this->sla = SLA::lookup($this->getSLAId());
Jared Hancock's avatar
Jared Hancock committed

        return $this->sla;
    }

    function getLastRespondent() {

Peter Rotich's avatar
Peter Rotich committed
        if (!isset($this->lastrespondent)) {
Peter Rotich's avatar
Peter Rotich committed
            $sql ='SELECT resp.staff_id '
                 .' FROM '.THREAD_ENTRY_TABLE.' resp '
                 .' LEFT JOIN '.THREAD_TABLE.' t ON( t.id=resp.thread_id) '
                 .' LEFT JOIN '.STAFF_TABLE. ' s ON(s.staff_id=resp.staff_id) '
                 .' WHERE  t.object_id='.db_input($this->getId())
                 .'     AND t.object_type="T" AND resp.staff_id>0 AND  resp.`type`="R" '
                 .' ORDER BY resp.created DESC LIMIT 1';

            if(!($res=db_query($sql)) || !db_num_rows($res))
                return null;

            list($id)=db_fetch_row($res);
Peter Rotich's avatar
Peter Rotich committed
            $this->lastrespondent = Staff::lookup($id);
        }
Peter Rotich's avatar
Peter Rotich committed
        return $this->lastrespondent;
Jared Hancock's avatar
Jared Hancock committed

    }

    function getLastMessageDate() {
Jared Hancock's avatar
Jared Hancock committed
    }

    function getLastMsgDate() {
        return $this->getLastMessageDate();
    }

    function getLastResponseDate() {
Jared Hancock's avatar
Jared Hancock committed
    }

    function getLastRespDate() {
        return $this->getLastResponseDate();
    }

Jared Hancock's avatar
Jared Hancock committed
    function getLastMsgId() {
        return $this->lastMsgId;
    }

    function getLastMessage() {
        if (!isset($this->last_message)) {
            if ($this->getLastMsgId())
                $this->last_message = MessageThreadEntry::lookup(
                    $this->getLastMsgId(), $this->getThreadId());
            if (!$this->last_message)
                $this->last_message = $this->getThread()->getLastMessage();
        return $this->last_message;
Peter Rotich's avatar
Peter Rotich committed
    function getNumTasks() {
        return $this->ht['tasks'];
    }

Peter Rotich's avatar
Peter Rotich committed
    function getThreadId() {
        return $this->ht['thread_id'];
    }

Peter Rotich's avatar
Peter Rotich committed
        if (!$this->thread && $this->getThreadId())
            $this->thread = TicketThread::lookup($this->getThreadId());
Jared Hancock's avatar
Jared Hancock committed
    }

    function getThreadCount() {
        return $this->getClientThread()->count();
Jared Hancock's avatar
Jared Hancock committed
    }

    function getNumMessages() {
        return $this->getThread()->getNumMessages();
Jared Hancock's avatar
Jared Hancock committed
    }

    function getNumResponses() {
        return $this->getThread()->getNumResponses();
Jared Hancock's avatar
Jared Hancock committed
    }

    function getNumNotes() {
        return $this->getThread()->getNumNotes();
    function getMessages() {
        return $this->getThreadEntries(array('M'));
    function getResponses() {
        return $this->getThreadEntries(array('R'));
    function getNotes() {
        return $this->getThreadEntries(array('N'));
    function getClientThread() {
        return $this->getThreadEntries(array('M', 'R'));
    function getThreadEntry($id) {
        return $this->getThread()->getEntry($id);
    function getThreadEntries($type=false) {
Peter Rotich's avatar
Peter Rotich committed
        $entries = $this->getThread()->getEntries();
Peter Rotich's avatar
Peter Rotich committed
            $entries->filter(array('type__in' => $type));

        return $entries;
    //UserList of recipients  (owner + collaborators)
    function getRecipients() {

        if (!isset($this->recipients)) {
            $list = new UserList();
            $list->add($this->getOwner());
            if ($collabs = $this->getActiveCollaborators()) {
                foreach ($collabs as $c)
                    $list->add($c);
            }
            $this->recipients = $list;
        }

        return $this->recipients;
    }

    function hasClientEditableFields() {
        $forms = DynamicFormEntry::forTicket($this->getId());
        foreach ($forms as $form) {
            foreach ($form->getFields() as $field) {
                if ($field->isEditableToUsers())
                    return true;
            }
        }
    }
    function getMissingRequiredFields() {
        $returnArray = array();
        $forms=DynamicFormEntry::forTicket($this->getId());
        foreach ($forms as $form) {
            foreach ($form->getFields() as $field) {
                if ($field->isRequiredForClose()) {
                    if (!($field->answer->get('value'))) {
                        array_push($returnArray, $field->get('label'));
                    }
                }
            }
        }
        return $returnArray;
    }

    function getMissingRequiredField() {
        $fields = $this->getMissingRequiredFields();
        return $fields[0];
    }

    function addCollaborator($user, $vars, &$errors) {
        if (!$user || $user->getId()==$this->getOwnerId())
Peter Rotich's avatar
Peter Rotich committed
            return null;
                'threadId' => $this->getThreadId(),
                'userId' => $user->getId()), $vars);
        if (!($c=Collaborator::add($vars, $errors)))
            return null;

        $this->collaborators = null;
        $this->recipients = null;
        return $c;
    }

    //XXX: Ugly for now
    function updateCollaborators($vars, &$errors) {

        //Deletes
        if($vars['del'] && ($ids=array_filter($vars['del']))) {
            $collabs = array();
            foreach ($ids as $k => $cid) {
                if (($c=Collaborator::lookup($cid))
                        && $c->getTicketId() == $this->getId()
Peter Rotich's avatar
Peter Rotich committed
                     $collabs[] = $c;
            $this->logNote(_S('Collaborators Removed'),
Peter Rotich's avatar
Peter Rotich committed
                    implode("<br>", $collabs), $thisstaff, false);
        }

        //statuses
        $cids = null;
        if($vars['cid'] && ($cids=array_filter($vars['cid']))) {
            $this->getThread()->collaborators->filter(array(
                'thread_id' => $this->getThreadId(),
                'id__in' => $cids
            ))->update(array(
                'updated' => SqlFunction::NOW(),
                'isactive' => 1,
            ));
        if ($cids) {
            $this->getThread()->collaborators->filter(array(
                'thread_id' => $this->getThreadId(),
                Q::not(array('id__in' => $cids))
            ))->update(array(
                'updated' => SqlFunction::NOW(),
                'isactive' => 0,
            ));
        }
        unset($this->ht['active_collaborators']);
        $this->collaborators = null;

        return true;
    }

    function getAuthToken($user, $algo=1) {

        //Format: // <user type><algo id used>x<pack of uid & tid><hash of the algo>
        $authtoken = sprintf('%s%dx%s',
                ($user->getId() == $this->getOwnerId() ? 'o' : 'c'),
                $algo,
                Base32::encode(pack('VV',$user->getId(), $this->getId())));

        switch($algo) {
            case 1:
                $authtoken .= substr(base64_encode(
                            md5($user->getId().$this->getCreateDate().$this->getId().SECRET_SALT, true)), 8);
                break;
            default:
                return null;
        }

        return $authtoken;
    }

    function sendAccessLink($user) {
        global $ost;

        if (!($email = $ost->getConfig()->getDefaultEmail())
            || !($content = Page::lookupByType('access-link')))
            return;

        $vars = array(
            'url' => $ost->getConfig()->getBaseUrl(),
            'ticket' => $this,
            'user' => $user,
            'recipient' => $user,
        );

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

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


Jared Hancock's avatar
Jared Hancock committed
    /* -------------------- Setters --------------------- */
    function setLastMsgId($msgid) {
        return $this->lastMsgId=$msgid;
    }
    function setLastMessage($message) {
        $this->last_message = $message;
        $this->setLastMsgId($message->getId());
    }
Jared Hancock's avatar
Jared Hancock committed

    //DeptId can NOT be 0. No orphans please!
Jared Hancock's avatar
Jared Hancock committed
        //Make sure it's a valid department//
        if(!($dept=Dept::lookup($deptId)) || $dept->getId()==$this->getDeptId())
Jared Hancock's avatar
Jared Hancock committed
            return false;

Jared Hancock's avatar
Jared Hancock committed
        $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), dept_id='.db_input($deptId)
            .' WHERE ticket_id='.db_input($this->getId());

        return (db_query($sql) && db_affected_rows());
    }
Jared Hancock's avatar
Jared Hancock committed
    //Set staff ID...assign/unassign/release (id can be 0)
Peter Rotich's avatar
Peter Rotich committed
    function setStaffId($staffId) {

        if(!is_numeric($staffId)) return false;
        $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), staff_id='.db_input($staffId)
            .' WHERE ticket_id='.db_input($this->getId());
Peter Rotich's avatar
Peter Rotich committed
        if (!db_query($sql)  || !db_affected_rows())
            return false;

        $this->staff = null;
        $this->ht['staff_id'] = $staffId;

Peter Rotich's avatar
Peter Rotich committed
        return true;
Jared Hancock's avatar
Jared Hancock committed
    }

    function setSLAId($slaId) {
        if ($slaId == $this->getSLAId()) return true;
        $rv = db_query(
Jared Hancock's avatar
Jared Hancock committed
             'UPDATE '.TICKET_TABLE.' SET sla_id='.db_input($slaId)
            .' WHERE ticket_id='.db_input($this->getId()))
            && db_affected_rows();
        if ($rv) {
            $this->ht['sla_id'] = $slaId;
            $this->sla = null;
        }
        return $rv;
Jared Hancock's avatar
Jared Hancock committed
    }
    /**
     * Selects the appropriate service-level-agreement plan for this ticket.
     * When tickets are transfered between departments, the SLA of the new
Jared Hancock's avatar
Jared Hancock committed
     * department should be applied to the ticket. This would be useful,
Jared Hancock's avatar
Jared Hancock committed
     * for instance, if the ticket is transferred to a different department
     * which has a shorter grace period, the ticket should be considered
     * overdue in the shorter window now that it is owned by the new
     * department.
     *
     * $trump - if received, should trump any other possible SLA source.
     *          This is used in the case of email filters, where the SLA
     *          specified in the filter should trump any other SLA to be
     *          considered.
     */
    function selectSLAId($trump=null) {
        global $cfg;
        # XXX Should the SLA be overridden if it was originally set via an
Jared Hancock's avatar
Jared Hancock committed
        #     email filter? This method doesn't consider such a case
Jared Hancock's avatar
Jared Hancock committed
            $slaId = $trump;
        } elseif ($this->getDept() && $this->getDept()->getSLAId()) {
Jared Hancock's avatar
Jared Hancock committed
            $slaId = $this->getDept()->getSLAId();
        } elseif ($this->getTopic() && $this->getTopic()->getSLAId()) {
Jared Hancock's avatar
Jared Hancock committed
            $slaId = $this->getTopic()->getSLAId();
        } else {
            $slaId = $cfg->getDefaultSLAId();
        }
Jared Hancock's avatar
Jared Hancock committed
        return ($slaId && $this->setSLAId($slaId)) ? $slaId : false;
    }

    //Set team ID...assign/unassign/release (id can be 0)
Peter Rotich's avatar
Peter Rotich committed
    function setTeamId($teamId) {
Peter Rotich's avatar
Peter Rotich committed
        if(!is_numeric($teamId)) return false;
Peter Rotich's avatar
Peter Rotich committed
        $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), team_id='.db_input($teamId)
            .' WHERE ticket_id='.db_input($this->getId());
Peter Rotich's avatar
Peter Rotich committed
        return (db_query($sql)  && db_affected_rows());
Jared Hancock's avatar
Jared Hancock committed
    }

    //Status helper.

    function setStatus($status, $comments='', &$errors=array(), $set_closing_agent=true) {
        global $thisstaff;
        if ($thisstaff && !($role=$thisstaff->getRole($this->getDeptId())))
        if ($status && is_numeric($status))
            $status = TicketStatus::lookup($status);
        if (!$status || !$status instanceof TicketStatus)
            return false;

        // Double check permissions (when changing status)
        if ($role && $this->getStatusId()) {
            switch ($status->getState()) {
            case 'closed':
                if (!($role->hasPerm(TicketModel::PERM_CLOSE)))
                    return false;
                break;
            case 'deleted':
                // XXX: intercept deleted status and do hard delete
                if ($role->hasPerm(TicketModel::PERM_DELETE))
                    return $this->delete($comments);
                // Agent doesn't have permission to delete  tickets
        if ($this->getStatusId() == $status->getId())
            return true;

        $sql = 'UPDATE '.TICKET_TABLE.' SET updated=NOW() '.
               ' ,status_id='.db_input($status->getId());

        //TODO: move this up.
        $ecb = null;
        switch($status->getState()) {
Jared Hancock's avatar
Jared Hancock committed
            case 'closed':
                if ($this->getMissingRequiredFields()) {
                    $errors['err'] = sprintf(__(
                        'This ticket is missing data on %s one or more required fields %s and cannot be closed'),
                    '', '');
                    return false;
                }
                $sql.=', closed=NOW(), lastupdate=NOW(), duedate=NULL ';
                if ($thisstaff && $set_closing_agent)
                    $sql.=', staff_id='.db_input($thisstaff->getId());

                $ecb = function($t) {
                    $t->reload();
                    $t->logEvent('closed');
                    $t->deleteDrafts();
                };
                break;
            case 'open':
                // TODO: check current status if it allows for reopening
                if ($this->isClosed()) {
                    $sql .= ',closed=NULL, lastupdate=NOW(), reopened=NOW() ';
                    $ecb = function ($t) {
                        $t->logEvent('reopened', 'closed');
                    };
                }

                // If the ticket is not open then clear answered flag
                if (!$this->isOpen())
                    $sql .= ', isanswered = 0 ';
Jared Hancock's avatar
Jared Hancock committed
                break;
        $sql.=' WHERE ticket_id='.db_input($this->getId());

        if (!db_query($sql) || !db_affected_rows())
            return false;

        // Log status change b4 reload — if currently has a status. (On new
        // ticket, the ticket is opened and thereafter the status is set to
        // the requested status).
        if ($current_status = $this->getStatus()) {
            $note = sprintf(__('Status changed from %1$s to %2$s by %3$s'),
                    $this->getStatus(),
                    $status,
                    $thisstaff ?: 'SYSTEM');

            $alert = false;
            if ($comments) {
                $note .= sprintf('<hr>%s', $comments);
                // Send out alerts if comments are included
                $alert = true;
            }
            $this->logNote(__('Status Changed'), $note, $thisstaff, $alert);
        // Log events via callback
        if ($ecb) $ecb($this);

        return true;
Jared Hancock's avatar
Jared Hancock committed
    }

    function setState($state, $alerts=false) {

        switch(strtolower($state)) {
            case 'open':
                return $this->setStatus('open');
                break;
            case 'closed':
                return $this->setStatus('closed');
                break;
            case 'answered':
                return $this->setAnsweredState(1);
                break;
            case 'unanswered':
                return $this->setAnsweredState(0);
                break;
            case 'overdue':
                return $this->markOverdue();
                break;
Peter Rotich's avatar
Peter Rotich committed
            case 'notdue':
                return $this->clearOverdue();
                break;
            case 'unassined':
                return $this->unassign();
Jared Hancock's avatar
Jared Hancock committed
        }

        return false;
    }




    function setAnsweredState($isanswered) {

        $sql='UPDATE '.TICKET_TABLE.' SET isanswered='.db_input($isanswered)
            .' WHERE ticket_id='.db_input($this->getId());

        return (db_query($sql) && db_affected_rows());
    }

    function reopen() {
        global $cfg;
        if (!$this->isClosed())
            return false;

        // Set status to open based on current closed status settings
        // If the closed status doesn't have configured "reopen" status then use the
        // the default ticket status.
        if (!($status=$this->getStatus()->getReopenStatus()))
            $status = $cfg->getDefaultTicketStatusId();

        return $status ? $this->setStatus($status, 'Reopened') : false;
Jared Hancock's avatar
Jared Hancock committed
    }

    function onNewTicket($message, $autorespond=true, $alertstaff=true) {
        global $cfg;

        //Log stuff here...
Jared Hancock's avatar
Jared Hancock committed
        if(!$autorespond && !$alertstaff) return true; //No alerts to send.

        /* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/
Jared Hancock's avatar
Jared Hancock committed
        $this->reload(); //get the new goodies.
        if(!$cfg
                || !($dept=$this->getDept())
                || !($tpl = $dept->getTemplate())
                || !($email=$dept->getAutoRespEmail())) {
                return false;  //bail out...missing stuff.
        }
        $options = array();
        if ($message instanceof ThreadEntry) {
            $options += array(
                'inreplyto'=>$message->getEmailMessageId(),
                'references'=>$message->getEmailReferences(),
                'thread'=>$message
            );
        }
        else {
            $options += array(
                'thread' => $this->getThread(),
            );
        }
Jared Hancock's avatar
Jared Hancock committed
        //Send auto response - if enabled.
        if($autorespond
                && $cfg->autoRespONNewTicket()
Jared Hancock's avatar
Jared Hancock committed
                &&  ($msg=$tpl->getAutoRespMsgTemplate())) {
            $msg = $this->replaceVars($msg->asArray(),
                    array('message' => $message,
                          'recipient' => $this->getOwner(),
                          'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
                    );

            $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'],
Jared Hancock's avatar
Jared Hancock committed
        //Send alert to out sleepy & idle staff.
                && ($email=$dept->getAlertEmail())
Jared Hancock's avatar
Jared Hancock committed
                && ($msg=$tpl->getNewTicketAlertMsgTemplate())) {
            $msg = $this->replaceVars($msg->asArray(), array('message' => $message));
Jared Hancock's avatar
Jared Hancock committed
            $recipients=$sentlist=array();
            //Exclude the auto responding email just incase it's from staff member.
            if ($message instanceof ThreadEntry && $message->isAutoReply())
                $sentlist[] = $this->getEmail();

Jared Hancock's avatar
Jared Hancock committed
            //Alert admin??
            if($cfg->alertAdminONNewTicket()) {
                $alert = $this->replaceVars($msg, array('recipient' => 'Admin'));
                $email->sendAlert($cfg->getAdminEmail(), $alert['subj'], $alert['body'], null, $options);
Jared Hancock's avatar
Jared Hancock committed
                $sentlist[]=$cfg->getAdminEmail();
            }
Jared Hancock's avatar
Jared Hancock committed
            //Only alerts dept members if the ticket is NOT assigned.
            if($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()) {
                if(($members=$dept->getMembersForAlerts()))
Jared Hancock's avatar
Jared Hancock committed
                    $recipients=array_merge($recipients, $members);
            }
Jared Hancock's avatar
Jared Hancock committed
            if($cfg->alertDeptManagerONNewTicket() && $dept && ($manager=$dept->getManager()))
                $recipients[]= $manager;
            // Account manager
            if ($cfg->alertAcctManagerONNewMessage()
                    && ($org = $this->getOwner()->getOrganization())
                    && ($acct_manager = $org->getAccountManager())) {
                if ($acct_manager instanceof Team)
                    $recipients = array_merge($recipients, $acct_manager->getMembers());
                else
                    $recipients[] = $acct_manager;
            }

            foreach( $recipients as $k=>$staff) {
                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                $alert = $this->replaceVars($msg, array('recipient' => $staff));
                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
Peter Rotich's avatar
Peter Rotich committed
                $sentlist[] = $staff->getEmail();
Jared Hancock's avatar
Jared Hancock committed
        return true;
    }

    function onOpenLimit($sendNotice=true) {

        //Log the limit notice as a warning for admin.
        $msg=sprintf(_S('Maximum open tickets (%1$d) reached for %2$s'),
            $cfg->getMaxOpenTickets(), $this->getEmail());
        $ost->logWarning(sprintf(_S('Maximum Open Tickets Limit (%s)'),$this->getEmail()),
            $msg);
        if(!$sendNotice || !$cfg->sendOverLimitNotice())
            return true;

        //Send notice to user.
        if(($dept = $this->getDept())
            && ($tpl=$dept->getTemplate())
            && ($msg=$tpl->getOverlimitMsgTemplate())
            && ($email=$dept->getAutoRespEmail())) {
            $msg = $this->replaceVars($msg->asArray(),
                        array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
            $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body']);
        $user = $this->getOwner();
        //Alert admin...this might be spammy (no option to disable)...but it is helpful..I think.
        $alert=sprintf(__('Maximum open tickets reached for %s.'), $this->getEmail())."\n"
              .sprintf(__('Open tickets: %d'), $user->getNumOpenTickets())."\n"
              .sprintf(__('Max allowed: %d'), $cfg->getMaxOpenTickets())
              ."\n\n".__("Notice sent to the user.");
        $ost->alertAdmin(__('Overlimit Notice'), $alert);
Peter Rotich's avatar
Peter Rotich committed
    function onResponse($response, $options=array()) {
        db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1, lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId()));
        $this->reload();
Peter Rotich's avatar
Peter Rotich committed
        $vars = array_merge($options,
                array(
                    'activity' => _S('New Response'),
                    'threadentry' => $response));

        $this->onActivity($vars);
    /*
     * Notify collaborators on response or new message
     *
     */

    function  notifyCollaborators($entry, $vars = array()) {
        if (!$entry instanceof ThreadEntry
                || !($recipients=$this->getRecipients())
                || !($dept=$this->getDept())
                || !($tpl=$dept->getTemplate())
                || !($msg=$tpl->getActivityNoticeMsgTemplate())
                || !($email=$dept->getEmail()))
            return;

        //Who posted the entry?
        if ($entry instanceof Message) {
            $poster = $entry->getUser();
            // Skip the person who sent in the message
            $skip[$entry->getUserId()] = 1;
            // Skip all the other recipients of the message
            foreach ($entry->getAllEmailRecipients() as $R) {
                foreach ($recipients as $R2) {
                    if (0 === strcasecmp($R2->getEmail(), $R->mailbox.'@'.$R->host)) {
            $poster = $entry->getStaff();
            // Skip the ticket owner
            $skip[$this->getUserId()] = 1;
        $vars = array_merge($vars, array(
                    'message' => (string) $entry,
                    'poster' => $poster ?: _S('A collaborator'),
        $msg = $this->replaceVars($msg->asArray(), $vars);
        $attachments = $cfg->emailAttachments()?$entry->getAttachments():array();
        $options = array('inreplyto' => $entry->getEmailMessageId(),
                         'thread' => $entry);
        foreach ($recipients as $recipient) {
            // Skip folks who have already been included on this part of
            // the conversation
            if (isset($skip[$recipient->getUserId()]))
                continue;
            $notice = $this->replaceVars($msg, array('recipient' => $recipient));
            $email->send($recipient, $notice['subj'], $notice['body'], $attachments,
    function onMessage($message, $autorespond=true) {
Jared Hancock's avatar
Jared Hancock committed
        global $cfg;

        db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastupdate=NOW(),lastmessage=NOW() WHERE ticket_id='.db_input($this->getId()));
        // Auto-assign to closing staff or last respondent
        // If the ticket is closed and auto-claim is not enabled then put the
        // ticket back to unassigned pool.
        if ($this->isClosed() && !$cfg->autoClaimTickets()) {
            $this->setStaffId(0);
        } elseif(!($staff=$this->getStaff()) || !$staff->isAvailable()) {
            // Ticket has no assigned staff -  if auto-claim is enabled then
            // try assigning it to the last respondent (if available)
            // otherwise leave the ticket unassigned.
            if ($cfg->autoClaimTickets() //Auto claim is enabled.
                    && ($lastrep=$this->getLastRespondent())
                    && $lastrep->isAvailable()) {
Jared Hancock's avatar
Jared Hancock committed
                $this->setStaffId($lastrep->getId()); //direct assignment;
            } else {
                $this->setStaffId(0); //unassign - last respondent is not available.
            }
        }

        // Reopen if closed AND reopenable
        // We're also checking autorespond flag because we don't want to
        // reopen closed tickets on auto-reply from end user. This is not to
        // confused with autorespond on new message setting
        if ($autorespond && $this->isClosed() && $this->isReopenable())
        // Figure out the user
        if ($this->getOwnerId() == $message->getUserId())
            $user = new TicketOwner(
                    User::lookup($message->getUserId()), $this);
        else
            $user = Collaborator::lookup(array(
                    'user_id' => $message->getUserId(),
                    'thread_id' => $this->getThreadId()));

        /**********   double check auto-response  ************/
        if (!$user)
            $autorespond=false;
        elseif ($autorespond && (Email::getIdByEmail($user->getEmail())))
Jared Hancock's avatar
Jared Hancock committed
            $autorespond=false;
        elseif ($autorespond && ($dept=$this->getDept()))
Jared Hancock's avatar
Jared Hancock committed
            $autorespond=$dept->autoRespONNewMessage();


        if(!$autorespond
                || !$cfg->autoRespONNewMessage()
                || !$message) return;  //no autoresp or alerts.

Jared Hancock's avatar
Jared Hancock committed
        $this->reload();
        $dept = $this->getDept();
        $email = $dept->getAutoRespEmail();
Jared Hancock's avatar
Jared Hancock committed
        //If enabled...send confirmation to user. ( New Message AutoResponse)
        if($email
                && ($tpl=$dept->getTemplate())
                && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) {
            $msg = $this->replaceVars($msg->asArray(),
                            array(
                                'recipient' => $user,
                                'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
                'inreplyto'=>$message->getEmailMessageId(),
                'thread'=>$message);
            $email->sendAutoReply($user, $msg['subj'], $msg['body'],
Peter Rotich's avatar
Peter Rotich committed
    function onActivity($vars, $alert=true) {
        global $cfg, $thisstaff;

        //TODO: do some shit

        if (!$alert // Check if alert is enabled
                || !$cfg->alertONNewActivity()
                || !($dept=$this->getDept())
                || !($email=$cfg->getAlertEmail())
                || !($tpl = $dept->getTemplate())
                || !($msg=$tpl->getNoteAlertMsgTemplate()))
            return;

        // Alert recipients
        $recipients=array();

        //Last respondent.
        if ($cfg->alertLastRespondentONNewActivity())
            $recipients[] = $this->getLastRespondent();

        // Assigned staff / team
        if ($cfg->alertAssignedONNewActivity()) {

            if (isset($vars['assignee'])
                    && $vars['assignee'] instanceof Staff)
                 $recipients[] = $vars['assignee'];
            elseif ($this->isOpen() && ($assignee = $this->getStaff()))
                $recipients[] = $assignee;

            if ($team = $this->getTeam())
                $recipients = array_merge($recipients, $team->getMembers());
        }

        // Dept manager
        if ($cfg->alertDeptManagerONNewActivity() && $dept && $dept->getManagerId())
            $recipients[] = $dept->getManager();

        $options = array();
        $staffId = $thisstaff ? $thisstaff->getId() : 0;
        if ($vars['threadentry'] && $vars['threadentry'] instanceof ThreadEntry) {
            $options = array(
                'inreplyto' => $vars['threadentry']->getEmailMessageId(),
                'references' => $vars['threadentry']->getEmailReferences(),
                'thread' => $vars['threadentry']);

            // Activity details
            if (!$vars['comments'])
                $vars['comments'] = $vars['threadentry'];

            // Staff doing the activity
            $staffId = $vars['threadentry']->getStaffId() ?: $staffId;
        }

        $msg = $this->replaceVars($msg->asArray(),
                array(
                    'note' => $vars['threadentry'], // For compatibility
Peter Rotich's avatar
Peter Rotich committed
                    'activity' => $vars['activity'],
                    'comments' => $vars['comments']));

        $isClosed = $this->isClosed();
        $sentlist=array();
        foreach ($recipients as $k=>$staff) {
            if (!is_object($staff)
                    // Don't bother vacationing staff.
                    || !$staff->isAvailable()
                    // No need to alert the poster!
                    || $staffId == $staff->getId()
                    // No duplicates.
                    || isset($sentlist[$staff->getEmail()])
                    // Make sure staff has access to ticket
                    || ($isClosed && !$this->checkStaffPerm($staff))
Peter Rotich's avatar
Peter Rotich committed
                    )
                continue;
            $alert = $this->replaceVars($msg, array('recipient' => $staff));
            $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
            $sentlist[$staff->getEmail()] = 1;
        }

    }

    function onAssign($assignee, $comments, $alert=true) {
Jared Hancock's avatar
Jared Hancock committed
        global $cfg, $thisstaff;
Jared Hancock's avatar
Jared Hancock committed

        if($this->isClosed()) $this->reopen(); //Assigned tickets must be open - otherwise why assign?

        //Assignee must be an object of type Staff or Team
        if(!$assignee || !is_object($assignee)) return false;

Jared Hancock's avatar
Jared Hancock committed
        $this->reload();

        $comments = $comments ?: _S('Ticket assignment');
        $assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)');
        //Log an internal note - no alerts on the internal note.
        $note = $this->logNote(
            sprintf(_S('Ticket Assigned to %s'), $assignee->getName()),
            $comments, $assigner, false);
Jared Hancock's avatar
Jared Hancock committed

        //See if we need to send alerts
        if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts!

        $dept = $this->getDept();
        if(!$dept
                || !($tpl = $dept->getTemplate())
                || !($email = $dept->getAlertEmail()))

        //recipients
        $recipients=array();
        if ($assignee instanceof Staff) {
            if ($cfg->alertStaffONAssignment())
                $recipients[] = $assignee;
        } elseif (($assignee instanceof Team) && $assignee->alertsEnabled()) {
            if ($cfg->alertTeamMembersONAssignment() && ($members=$assignee->getMembers()))
                $recipients = array_merge($recipients, $members);
            elseif ($cfg->alertTeamLeadONAssignment() && ($lead=$assignee->getTeamLead()))
                $recipients[] = $lead;
        }
Jared Hancock's avatar
Jared Hancock committed

        //Get the message template
        if ($recipients
                && ($msg=$tpl->getAssignedAlertMsgTemplate())) {
            $msg = $this->replaceVars($msg->asArray(),
                        array('comments' => $comments,
                              'assignee' => $assignee,
                              'assigner' => $assigner
Jared Hancock's avatar
Jared Hancock committed
            //Send the alerts.
            $sentlist=array();
                'inreplyto'=>$note->getEmailMessageId(),
                'references'=>$note->getEmailReferences(),
                'thread'=>$note);
            foreach( $recipients as $k=>$staff) {
                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                $alert = $this->replaceVars($msg, array('recipient' => $staff));
                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
Peter Rotich's avatar
Peter Rotich committed
                $sentlist[] = $staff->getEmail();
Jared Hancock's avatar
Jared Hancock committed
   function onOverdue($whine=true, $comments="") {
Jared Hancock's avatar
Jared Hancock committed
        global $cfg;

        if($whine && ($sla=$this->getSLA()) && !$sla->alertOnOverdue())
            $whine = false;
Jared Hancock's avatar
Jared Hancock committed

        //check if we need to send alerts.
        if(!$whine
                || !$cfg->alertONOverdueTicket()
                || !($dept = $this->getDept()))
Jared Hancock's avatar
Jared Hancock committed
            return true;

        //Get the message template
        if(($tpl = $dept->getTemplate())
                && ($msg=$tpl->getOverdueAlertMsgTemplate())
                && ($email = $dept->getAlertEmail())) {
            $msg = $this->replaceVars($msg->asArray(),
                array('comments' => $comments));
Jared Hancock's avatar
Jared Hancock committed

            //recipients
            $recipients=array();
            //Assigned staff or team... if any
            if($this->isAssigned() && $cfg->alertAssignedONOverdueTicket()) {
Jared Hancock's avatar
Jared Hancock committed
                if($this->getStaffId())
                    $recipients[]=$this->getStaff();
                elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
                    $recipients=array_merge($recipients, $members);
            } elseif($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) {
                //Only alerts dept members if the ticket is NOT assigned.
                if ($members = $dept->getMembersForAlerts())
                    $recipients = array_merge($recipients, $members);
Jared Hancock's avatar
Jared Hancock committed
            }
            //Always alert dept manager??
            if($cfg->alertDeptManagerONOverdueTicket() && $dept && ($manager=$dept->getManager()))
                $recipients[]= $manager;

            $sentlist=array();
            foreach( $recipients as $k=>$staff) {
                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                $alert = $this->replaceVars($msg, array('recipient' => $staff));
                $email->sendAlert($staff, $alert['subj'], $alert['body'], null);
Peter Rotich's avatar
Peter Rotich committed
                $sentlist[] = $staff->getEmail();
    // TemplateVariable interface
    function asVar() {
       return $this->getNumber();
    function getVar($tag) {
Jared Hancock's avatar
Jared Hancock committed
        global $cfg;

        if($tag && is_callable(array($this, 'get'.ucfirst($tag))))
            return call_user_func(array($this, 'get'.ucfirst($tag)));
        switch(mb_strtolower($tag)) {
            case 'phone_number':
                return $this->getPhoneNumber();
                break;
            case 'auth_token':
                return $this->getOldAuthToken();
                break;
            case 'client_link':
                return sprintf('%s/view.php?t=%s',
                        $cfg->getBaseUrl(), $this->getNumber());
                break;
            case 'staff_link':
                return sprintf('%s/scp/tickets.php?id=%d', $cfg->getBaseUrl(), $this->getId());
                break;
            case 'create_date':
                return new FormattedDate($this->getCreateDate());
                break;
             case 'due_date':
                if ($due = $this->getEstDueDate())
                    return new FormattedDate($due);
                break;
M. Hagen's avatar
M. Hagen committed
            case 'close_date':
                if ($this->isClosed())
                    return new FormattedDate($this->getCloseDate());
                break;
                return new FormattedDate($this->last_update);
            case 'user':
                return $this->getOwner();
Jared Hancock's avatar
Jared Hancock committed
            default:
                if (isset($this->_answers[$tag]))
                    // The answer object is retrieved here which will
                    // automatically invoke the toString() method when the
                    // answer is coerced into text
                    return $this->_answers[$tag];
    static function getVarScope() {
        $base = array(
            'assigned' => __('Assigned agent and/or team'),
            'close_date' => array(
                'class' => 'FormattedDate', 'desc' => __('Date Closed'),
            ),
            'create_date' => array(
                'class' => 'FormattedDate', 'desc' => __('Date created'),
                'class' => 'Dept', 'desc' => __('Department'),
            'due_date' => array(
                'class' => 'FormattedDate', 'desc' => __('Due Date'),
            'email' => __('Default email address of ticket owner'),
            'name' => array(
                'class' => 'PersonsName', 'desc' => __('Name of ticket owner'),
            ),
            'number' => __('Ticket number'),
            'phone' => __('Phone number of ticket owner'),
            'priority' => array(
                'class' => 'Priority', 'desc' => __('Priority'),
            ),
            'recipients' => array(
                'class' => 'UserList', 'desc' => __('List of all recipient names'),
            'source' => __('Source'),
            'status' => array(
                'class' => 'TicketStatus', 'desc' => __('Status'),
            ),
            'staff' => array(
                'class' => 'Staff', 'desc' => __('Assigned/closing agent'),
            ),
            'subject' => 'Subject',
            'team' => array(
                'class' => 'Team', 'desc' => __('Assigned/closing team'),
            ),
            'thread' => array(
                'class' => 'TicketThread', 'desc' => __('Ticket Thread'),
                'class' => 'Topic', 'desc' => __('Help topic'),
            ),
            // XXX: Isn't lastreponse and lastmessage more useful
            'last_update' => array(
                'class' => 'FormattedDate', 'desc' => __('Time of last update'),
            ),
            'user' => array(
                'class' => 'User', 'desc' => __('Ticket owner'),
            ),
        );

        $extra = VariableReplacer::compileFormScope(TicketForm::getInstance());
        return $base + $extra;
    }

    //Replace base variables.
    function replaceVars($input, $vars = array()) {
        global $ost;

        $vars = array_merge($vars, array('ticket' => $this));

        return $ost->replaceTemplateVariables($input, $vars);
Jared Hancock's avatar
Jared Hancock committed
    }

    function markUnAnswered() {
        return (!$this->isAnswered() || $this->setAnsweredState(0));
    }

    function markAnswered() {
        return ($this->isAnswered() || $this->setAnsweredState(1));
    }

    function markOverdue($whine=true) {
Jared Hancock's avatar
Jared Hancock committed
        global $cfg;
Jared Hancock's avatar
Jared Hancock committed
        if($this->isOverdue())
            return true;

        $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=1, updated=NOW() '
Jared Hancock's avatar
Jared Hancock committed
            .' WHERE ticket_id='.db_input($this->getId());

        if(!db_query($sql) || !db_affected_rows())
            return false;

        $this->logEvent('overdue');
Jared Hancock's avatar
Jared Hancock committed
        $this->onOverdue($whine);
Jared Hancock's avatar
Jared Hancock committed
        return true;
    }

Peter Rotich's avatar
Peter Rotich committed
    function clearOverdue() {

Peter Rotich's avatar
Peter Rotich committed
            return true;

        //NOTE: Previously logged overdue event is NOT annuled.

Peter Rotich's avatar
Peter Rotich committed
        $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=0, updated=NOW() ';
Peter Rotich's avatar
Peter Rotich committed
        //clear due date if it's in the past
        if($this->getDueDate() && Misc::db2gmtime($this->getDueDate()) <= Misc::gmtime())
Peter Rotich's avatar
Peter Rotich committed
            $sql.=', duedate=NULL';

        //Clear SLA if est. due date is in the past
        if($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime())
Peter Rotich's avatar
Peter Rotich committed
        $sql.=' WHERE ticket_id='.db_input($this->getId());

        return (db_query($sql) && db_affected_rows());
    }

    //Dept Tranfer...with alert.. done by staff
Jared Hancock's avatar
Jared Hancock committed
    function transfer($deptId, $comments, $alert = true) {
Jared Hancock's avatar
Jared Hancock committed
        global $cfg, $thisstaff;
        if (!$this->checkStaffPerm($thisstaff, TicketModel::PERM_TRANSFER))
Jared Hancock's avatar
Jared Hancock committed
            return false;

        $currentDept = $this->getDeptName(); //Current department

        if(!$deptId || !$this->setDeptId($deptId))
            return false;
        if($this->isClosed()) $this->reopen();

        $this->reload();
Peter Rotich's avatar
Peter Rotich committed
        $dept = $this->getDept();
Jared Hancock's avatar
Jared Hancock committed
        if(!$this->getSLAId() || $this->getSLA()->isTransient())
Peter Rotich's avatar
Peter Rotich committed
        // Make sure the new department allows assignment to the
        // currently assigned agent (if any)
        if ($this->isAssigned()
                && ($staff=$this->getStaff())
                && $dept->assignMembersOnly()
                && !$dept->isMember($staff)) {
            $this->setStaffId(0);
        }

        /*** log the transfer comments as internal note - with alerts disabled - ***/
        $title=sprintf(_S('Ticket transferred from %1$s to %2$s'),
            $currentDept, $this->getDeptName());
        $note = $this->logNote($title, $comments, $thisstaff, false);
        $this->logEvent('transferred');
        //Send out alerts if enabled AND requested
Peter Rotich's avatar
Peter Rotich committed
        if (!$alert || !$cfg->alertONTransfer())
            return true; //no alerts!!
         if (($email = $dept->getAlertEmail())
                     && ($tpl = $dept->getTemplate())
                     && ($msg=$tpl->getTransferAlertMsgTemplate())) {
            $msg = $this->replaceVars($msg->asArray(),
                array('comments' => $comments, 'staff' => $thisstaff));
Jared Hancock's avatar
Jared Hancock committed
            $recipients=array();
            //Assigned staff or team... if any
            if($this->isAssigned() && $cfg->alertAssignedONTransfer()) {
                if($this->getStaffId())
                    $recipients[]=$this->getStaff();
                elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
                    $recipients = array_merge($recipients, $members);
Jared Hancock's avatar
Jared Hancock committed
            } elseif($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) {
                //Only alerts dept members if the ticket is NOT assigned.
                if(($members=$dept->getMembersForAlerts()))
                    $recipients = array_merge($recipients, $members);
Jared Hancock's avatar
Jared Hancock committed
            }

            //Always alert dept manager??
            if($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager()))
                $recipients[]= $manager;
Jared Hancock's avatar
Jared Hancock committed
            $sentlist=array();
                'inreplyto'=>$note->getEmailMessageId(),
                'references'=>$note->getEmailReferences(),
                'thread'=>$note);
            foreach( $recipients as $k=>$staff) {
                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
                $alert = $this->replaceVars($msg, array('recipient' => $staff));
                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
Peter Rotich's avatar
Peter Rotich committed
                $sentlist[] = $staff->getEmail();
Peter Rotich's avatar
Peter Rotich committed
    function claim() {
        global $thisstaff;

        if (!$thisstaff || !$this->isOpen() || $this->isAssigned())
            return false;

        $dept = $this->getDept();
        if ($dept->assignMembersOnly() && !$dept->isMember($thisstaff))
            return false;

        $comments = sprintf(_S('Ticket claimed by %s'), $thisstaff->getName());

        return $this->assignToStaff($thisstaff->getId(), $comments, false);
    }

Jared Hancock's avatar
Jared Hancock committed
    function assignToStaff($staff, $note, $alert=true) {

        if(!is_object($staff) && !($staff=Staff::lookup($staff)))
            return false;
        if (!$staff->isAvailable() || !$this->setStaffId($staff->getId()))
Jared Hancock's avatar
Jared Hancock committed
            return false;

        $this->onAssign($staff, $note, $alert);
        $this->logEvent('assigned');
Jared Hancock's avatar
Jared Hancock committed

        return true;
    }

    function assignToTeam($team, $note, $alert=true) {

        if(!is_object($team) && !($team=Team::lookup($team)))
            return false;

        if (!$team->isActive() || !$this->setTeamId($team->getId()))
Jared Hancock's avatar
Jared Hancock committed
            return false;

        //Clear - staff if it's a closed ticket
        //  staff_id is overloaded -> assigned to & closed by.
        if($this->isClosed())
            $this->setStaffId(0);

        $this->onAssign($team, $note, $alert);
        $this->logEvent('assigned');
Jared Hancock's avatar
Jared Hancock committed

        return true;
    }

    //Assign ticket to staff or team - overloaded ID.
    function assign($assignId, $note, $alert=true) {
        global $thisstaff;

        $rv=0;
        $id=preg_replace("/[^0-9]/", "", $assignId);
Jared Hancock's avatar
Jared Hancock committed
        if($assignId[0]=='t') {
            $rv=$this->assignToTeam($id, $note, $alert);
        } elseif($assignId[0]=='s' || is_numeric($assignId)) {
Peter Rotich's avatar
Peter Rotich committed
            $alert=($alert && $thisstaff && $thisstaff->getId()==$id)?false:$alert; //No alerts on self assigned tickets!!!
Jared Hancock's avatar
Jared Hancock committed
            //We don't care if a team is already assigned to the ticket - staff assignment takes precedence
            $rv=$this->assignToStaff($id, $note, $alert);
        }

        return $rv;
    }
Jared Hancock's avatar
Jared Hancock committed
    //unassign primary assignee
    function unassign() {

        if(!$this->isAssigned()) //We can't release what is not assigned buddy!
            return true;

Peter Rotich's avatar
Peter Rotich committed
        //We can only unassigned OPEN tickets.
        if($this->isClosed())
            return false;
Peter Rotich's avatar
Peter Rotich committed
        //Unassign staff (if any)
        if($this->getStaffId() && !$this->setStaffId(0))
            return false;

        //unassign team (if any)
        if($this->getTeamId() && !$this->setTeamId(0))
            return false;
Peter Rotich's avatar
Peter Rotich committed
        $this->reload();

        return true;
    }
Jared Hancock's avatar
Jared Hancock committed
    function release() {
        return $this->unassign();
    }

    //Change ownership
    function changeOwner($user) {
        global $thisstaff;

        if (!$user
                || ($user->getId() == $this->getOwnerId())
                || !($this->checkStaffPerm($thisstaff,
                        TicketModel::PERM_EDIT)))
            return false;

        $sql ='UPDATE '.TICKET_TABLE.' SET updated = NOW() '
            .', user_id = '.db_input($user->getId())
            .' WHERE ticket_id = '.db_input($this->getId());

        if (!db_query($sql) || !db_affected_rows())
            return false;

        $this->ht['user_id'] = $user->getId();
        $this->user = null;
        $this->collaborators = null;
        $this->recipients = null;

        //Log an internal note
        $note = sprintf(_S('%s changed ticket ownership to %s'),
                $thisstaff->getName(), $user->getName());

        //Remove the new owner from list of collaborators
        $c = Collaborator::lookup(array(
                    'user_id' => $user->getId(),
                    'thread_id' => $this->getThreadId()));
        if ($c && $c->delete())
            $note.= ' '._S('(removed as collaborator)');
        $this->logNote('Ticket ownership changed', $note);
Jared Hancock's avatar
Jared Hancock committed
    //Insert message from client
    function postMessage($vars, $origin='', $alerts=true) {
Jared Hancock's avatar
Jared Hancock committed
        global $cfg;
        if ($origin)
            $vars['origin'] = $origin;
        if(isset($vars['ip']))
            $vars['ip_address'] = $vars['ip'];
        elseif(!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
            $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
        $errors = array();
        if(!($message = $this->getThread()->addMessage($vars, $errors)))
            return null;
        $this->setLastMessage($message);
        //Add email recipients as collaborators...
        if ($vars['recipients']
                && (strtolower($origin) != 'email' || ($cfg && $cfg->addCollabsViaEmail()))
                //Only add if we have a matched local address
            //New collaborators added by other collaborators are disable --
            // requires staff approval.
            $info = array(
                    'isactive' => ($message->getUserId() == $this->getUserId())? 1: 0);
            $collabs = array();
            foreach ($vars['recipients'] as $recipient) {
                // Skip virtual delivered-to addresses
                if (strcasecmp($recipient['source'], 'delivered-to') === 0)
                    continue;

                if (($user=User::fromVars($recipient)))
                    if ($c=$this->addCollaborator($user, $info, $errors))
                        $collabs[] = sprintf('%s%s',
                            (string) $c,
                            $recipient['source']
                                ? " ".sprintf(_S('via %s'), $recipient['source'])
                                : ''
                            );
            }
            //TODO: Can collaborators add others?
            if ($collabs) {
                //TODO: Change EndUser to name of  user.
                $this->logNote(_S('Collaborators added by end user'),
                        implode("<br>", $collabs), _S('End User'), false);
Loading
Loading full blame...