diff --git a/include/class.thread.php b/include/class.thread.php index cae40fa3b0ab1c419b1ce2443a91c3481e6e57e2..705ba5ec5c623b40844d648b5cef6ccbfc945e5b 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -3,7 +3,7 @@ class.thread.php Ticket thread - TODO: move thread related logic here from class.ticket.php + XXX: Please DO NOT add any ticket related logic! use ticket class. Peter Rotich <peter@osticket.com> Copyright (c) 2006-2013 osTicket @@ -16,6 +16,188 @@ **********************************************************************/ include_once(INCLUDE_DIR.'class.ticket.php'); +//Ticket thread. +class Thread { + + var $id; // same as ticket ID. + var $ticket; + + function Thread($ticket) { + + $this->ticket = $ticket; + + $this->id = 0; + + $this->load(); + } + + function load() { + + if(!$this->getTicketId()) + return null; + + $sql='SELECT ticket.ticket_id as id ' + .' ,count(DISTINCT attach.attach_id) as attachments ' + .' ,count(DISTINCT message.id) as messages ' + .' ,count(DISTINCT response.id) as responses ' + .' ,count(DISTINCT note.id) as notes ' + .' FROM '.TICKET_TABLE.' ticket ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (' + .'ticket.ticket_id=attach.ticket_id) ' + .' LEFT JOIN '.TICKET_THREAD_TABLE.' message ON (' + ."ticket.ticket_id=message.ticket_id AND message.thread_type = 'M') " + .' LEFT JOIN '.TICKET_THREAD_TABLE.' response ON (' + ."ticket.ticket_id=response.ticket_id AND response.thread_type = 'R') " + .' LEFT JOIN '.TICKET_THREAD_TABLE.' note ON ( ' + ."ticket.ticket_id=note.ticket_id AND note.thread_type = 'N') " + .' WHERE ticket.ticket_id='.db_input($this->getTicketId()) + .' GROUP BY ticket.ticket_id'; + + if(!($res=db_query($sql)) || !db_num_rows($res)) + return false; + + $this->ht = db_fetch_array($res); + + $this->id = $this->ht['id']; + + return true; + } + + function getId() { + return $this->id; + } + + function getTicketId() { + return $this->getTicket()?$this->getTicket()->getId():0; + } + + function getTicket() { + return $this->ticket; + } + + function getNumAttachments() { + return $this->ht['attachments']; + } + + function getNumMessages() { + return $this->ht['messages']; + } + + function getNumResponses() { + return $this->ht['responses']; + } + + function getNumNotes() { + return $this->ht['notes']; + } + + function getCount() { + return $this->getNumMessages() + $this->getNumResponses(); + } + + function getMessages() { + return $this->getEntries('M'); + } + + function getResponses() { + return $this->getEntries('R'); + } + + function getNotes() { + return $this->getEntries('N'); + } + + function getEntries($type, $order='ASC') { + + if(!$order || !in_array($order, array('DESC','ASC'))) + $order='ASC'; + + $sql='SELECT thread.* ' + .' ,count(DISTINCT attach.attach_id) as attachments ' + .' FROM '.TICKET_THREAD_TABLE.' thread ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach + ON (thread.ticket_id=attach.ticket_id + AND thread.id=attach.ref_id + AND thread.thread_type=attach.ref_type) ' + .' WHERE thread.ticket_id='.db_input($this->getTicketId()); + + if($type && is_array($type)) + $sql.=' AND thread.thread_type IN('.implode(',', db_input($type)).')'; + elseif($type) + $sql.=' AND thread.thread_type='.db_input($type); + + $sql.=' GROUP BY thread.id ' + .' ORDER BY thread.created '.$order; + + $entries = array(); + if(($res=db_query($sql)) && db_num_rows($res)) + while($rec=db_fetch_array($res)) + $entries[] = $rec; + + return $entries; + } + + function getEntry($id) { + return ThreadEntry::lookup($id, $this->getTicketId()); + } + + function addNote($vars, &$errors) { + + //Add ticket Id. + $vars['ticketId'] = $this->getTicketId(); + + return Note::create($vars, $errors); + } + + function addMessage($vars, &$errors) { + + $vars['ticketId'] = $this->getTicketId(); + $vars['staffId'] = 0; + + return Message::create($vars, $errors); + } + + function addResponse($vars, &$errors) { + + $vars['ticketId'] = $this->getTicketId(); + + return Response::create($vars, $errors); + } + + function deleteAttachments() { + + $deleted=0; + // Clear reference table + $res=db_query('DELETE FROM '.TICKET_ATTACHMENT_TABLE.' WHERE ticket_id='.db_input($this->getTicketId())); + if ($res && db_affected_rows()) + $deleted = AttachmentFile::deleteOrphans(); + + return $deleted; + } + + function delete() { + + $res=db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getTicketId())); + if(!$res || !db_affected_rows()) + return false; + + $this->deleteAttachments(); + + return true; + } + + /* static */ + function lookup($ticket) { + + return ($ticket + && is_object($ticket) + && ($thread = new Thread($ticket)) + && $thread->getId() + )?$thread:null; + } +} + + Class ThreadEntry { var $id; @@ -23,7 +205,10 @@ Class ThreadEntry { var $staff; var $ticket; - + + var $attachments; + + function ThreadEntry($id, $type='', $ticketId=0) { $this->load($id, $type, $ticketId); } @@ -33,12 +218,14 @@ Class ThreadEntry { if(!$id && !($id=$this->getId())) return false; - $sql='SELECT thread.* ' + $sql='SELECT thread.*, info.* ' .' ,count(DISTINCT attach.attach_id) as attachments ' .' FROM '.TICKET_THREAD_TABLE.' thread ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach - ON (thread.ticket_id=attach.ticket_id - AND thread.id=attach.ref_id + .' LEFT JOIN '.TICKET_EMAIL_INFO_TABLE.' info + ON (thread.id=info.message_id) ' + .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach + ON (thread.ticket_id=attach.ticket_id + AND thread.id=attach.ref_id AND thread.thread_type=attach.ref_type) ' .' WHERE thread.id='.db_input($id); @@ -49,7 +236,7 @@ Class ThreadEntry { $sql.=' AND thread.ticket_id='.db_input($ticketId); $sql.=' GROUP BY thread.id '; - + if(!($res=db_query($sql)) || !db_num_rows($res)) return false; @@ -57,6 +244,7 @@ Class ThreadEntry { $this->id = $this->ht['id']; $this->staff = $this->ticket = null; + $this->attachments = array(); return true; } @@ -122,13 +310,186 @@ Class ThreadEntry { } function getStaff() { - + if(!$this->staff && $this->getStaffId()) $this->staff = Staff::lookup($this->getStaffId()); return $this->staff; } + function getEmailHeader() { + return $this->ht['headers']; + } + + function isAutoResponse() { + return $this->getEmailHeader()?TicketFilter::isAutoResponse($this->getEmailHeader()):false; + } + + //Web uploads - caller is expected to format, validate and set any errors. + function uploadFiles($files) { + + if(!$files || !is_array($files)) + return false; + + $uploaded=array(); + foreach($files as $file) { + if($file['error'] && $file['error']==UPLOAD_ERR_NO_FILE) + continue; + + if(!$file['error'] + && ($id=AttachmentFile::upload($file)) + && $this->saveAttachment($id)) + $uploaded[]=$id; + else { + if(!$file['error']) + $error = 'Unable to upload file - '.$file['name']; + elseif(is_numeric($file['error'])) + $error ='Error #'.$file['error']; //TODO: Transplate to string. + else + $error = $file['error']; + /* + Log the error as an internal note. + XXX: We're doing it here because it will eventually become a thread post comment (hint: comments coming!) + XXX: logNote must watch for possible loops + */ + $this->getTicket()->logNote('File Upload Error', $error, 'SYSTEM', false); + } + + } + + return $uploaded; + } + + function importAttachments($attachments) { + + if(!$attachments || !is_array($attachments)) + return null; + + $files = array(); + foreach($attachments as $attachment) + if(($id=$this->importAttachment($attachment))) + $files[] = $id; + + return $files; + } + + /* Emailed & API attachments handler */ + function importAttachment($attachment) { + + if(!$attachment || !is_array($attachment)) + return null; + + $id=0; + if (!$attachment['error'] && ($id=$this->saveAttachment($attachment))) + $files[] = $id; + else { + $error = $attachment['error']; + + if(!$error) + $error = 'Unable to import attachment - '.$attachment['name']; + + $this->getTicket()->logNote('File Import Error', $error, 'SYSTEM', false); + } + + return $id; + } + + /* + Save attachment to the DB. + @file is a mixed var - can be ID or file hashtable. + */ + function saveAttachment($file) { + + if(!($fileId=is_numeric($file)?$file:AttachmentFile::save($file))) + return 0; + + $sql ='INSERT INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() ' + .' ,file_id='.db_input($fileId) + .' ,ticket_id='.db_input($this->getTicketId()) + .' ,ref_id='.db_input($this->getId()) + .' ,ref_type='.db_input($this->getType()); + + return (db_query($sql) && ($id=db_insert_id()))?$id:0; + } + + function saveAttachments($files) { + $ids=array(); + foreach($files as $file) + if(($id=$this->saveAttachment($file))) + $ids[] = $id; + + return $ids; + } + + function getAttachments() { + + if($this->attachments) + return $this->attachments; + + //XXX: inner join the file table instead? + $sql='SELECT a.attach_id, f.id as file_id, f.size, f.hash as file_hash, f.name ' + .' FROM '.FILE_TABLE.' f ' + .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) ' + .' WHERE a.ticket_id='.db_input($this->getTicketId()) + .' AND a.ref_id='.db_input($this->getId()) + .' AND a.ref_type='.db_input($this->getType()); + + $this->attachments = array(); + if(($res=db_query($sql)) && db_num_rows($res)) { + while($rec=db_fetch_array($res)) + $this->attachments[] = $rec; + } + + return $this->attachments; + } + + function getAttachmentsLinks($file='attachment.php', $target='', $separator=' ') { + + $str=''; + foreach($this->getAttachments() as $attachment ) { + /* The hash can be changed but must match validation in @file */ + $hash=md5($attachment['file_id'].session_id().$attachment['file_hash']); + $size = ''; + if($attachment['size']) + $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size'])); + + $str.=sprintf('<a class="Icon file" href="%s?id=%d&h=%s" target="%s">%s</a>%s %s', + $file, $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); + } + + return $str; + } + + + /* Returns file names with id as key */ + function getFiles() { + + $files = array(); + foreach($this->getAttachments() as $attachment) + $files[$attachment['file_id']] = $attachment['name']; + + return $files; + } + + + /* save email info + * TODO: Refactor it to include outgoing emails on responses. + */ + + function saveEmailInfo($vars) { + + if(!$vars || !$vars['mid']) + return 0; + + $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE + .' SET message_id='.db_input($this->getId()) //TODO: change it to thread_id + .', email_mid='.db_input($vars['mid']) //TODO: change it to mid. + .', headers='.db_input($vars['header']); + + return db_query($sql)?db_insert_id():0; + } + + /* variables */ function asVar() { @@ -164,12 +525,59 @@ Class ThreadEntry { /* static calls */ function lookup($id, $tid=0, $type='') { - return ($id - && is_numeric($id) - && ($e = new ThreadEntry($id, $type, $tid)) + return ($id + && is_numeric($id) + && ($e = new ThreadEntry($id, $type, $tid)) && $e->getId()==$id )?$e:null; } + + //new entry ... we're trusting the caller to check validity of the data. + function create($vars) { + + //Must have... + if(!$vars['ticketId'] || !$vars['type'] || !in_array($vars['type'], array('M','R','N'))) + return false; + + $sql=' INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() ' + .' ,thread_type='.db_input($vars['type']) + .' ,ticket_id='.db_input($vars['ticketId']) + .' ,title='.db_input(Format::sanitize($vars['title'])) + .' ,body='.db_input(Format::sanitize($vars['body'])) + .' ,staff_id='.db_input($vars['staffId']) + .' ,poster='.db_input($vars['poster']) + .' ,source='.db_input($vars['source']); + + if(isset($vars['pid'])) + $sql.=' ,pid='.db_input($vars['pid']); + + if($vars['ip_address']) + $sql.=' ,ip_address='.db_input($vars['ip_address']); + + //echo $sql; + if(!db_query($sql) || !($entry=self::lookup(db_insert_id(), $vars['ticketId']))) + return false; + + /************* ATTACHMENTS *****************/ + + //Upload/save attachments IF ANY + if($vars['files']) //expects well formatted and VALIDATED files array. + $entry->uploadFiles($vars['files']); + + //Emailed or API attachments + if($vars['attachments']) + $entry->importAttachments($vars['attachments']); + + //Canned attachments... + if($vars['cannedattachments'] && is_array($vars['cannedattachments'])) + $entry->saveAttachments($vars['cannedattachments']); + + return $entry; + } + + function add($vars) { + return ($entry=self::create($vars))?$entry->getId():0; + } } /* Message - Ticket thread entry of type message */ @@ -183,11 +591,30 @@ class Message extends ThreadEntry { return $this->getTitle(); } - function lookup($id, $tid, $type='M') { - - return ($id + function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + function add($vars, &$errors) { + + if(!$vars || !is_array($vars) || !$vars['ticketId']) + $errors['err'] = 'Missing or invalid data'; + elseif(!$vars['message']) + $errors['message'] = 'Message required'; + + if($errors) return false; + + $vars['type'] = 'M'; + $vars['body'] = $vars['message']; + + return ThreadEntry::add($vars); + } + + function lookup($id, $tid=0, $type='M') { + + return ($id && is_numeric($id) - && ($m = new Message($id, $tid)) + && ($m = new Message($id, $tid)) && $m->getId()==$id )?$m:null; } @@ -208,11 +635,33 @@ class Response extends ThreadEntry { return $this->getStaff(); } - function lookup($id, $tid, $type='R') { - - return ($id + function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + function add($vars, &$errors) { + + if(!$vars || !is_array($vars) || !$vars['ticketId']) + $errors['err'] = 'Missing or invalid data'; + elseif(!$vars['response']) + $errors['response'] = 'Response required'; + + if($errors) return false; + + $vars['type'] = 'R'; + $vars['body'] = $vars['response']; + if(!$vars['pid'] && $vars['msgId']) + $vars['pid'] = $vars['msgId']; + + return ThreadEntry::add($vars); + } + + + function lookup($id, $tid=0, $type='R') { + + return ($id && is_numeric($id) - && ($r = new Response($id, $tid)) + && ($r = new Response($id, $tid)) && $r->getId()==$id )?$r:null; } @@ -229,11 +678,33 @@ class Note extends ThreadEntry { return $this->getBody(); } - function lookup($id, $tid, $type='N') { - - return ($id + /* static */ + function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + function add($vars, &$errors) { + + //Check required params. + if(!$vars || !is_array($vars) || !$vars['ticketId']) + $errors['err'] = 'Missing or invalid data'; + elseif(!$vars['note']) + $errors['note'] = 'Note required'; + + if($errors) return false; + + //TODO: use array_intersect_key when we move to php 5 to extract just what we need. + $vars['type'] = 'N'; + $vars['body'] = $vars['note']; + + return ThreadEntry::add($vars); + } + + function lookup($id, $tid=0, $type='N') { + + return ($id && is_numeric($id) - && ($n = new Note($id, $tid)) + && ($n = new Note($id, $tid)) && $n->getId()==$id )?$n:null; }