diff --git a/bootstrap.php b/bootstrap.php index 424f3637c1bc5cc9ecde66edd30fd07555490562..8985717d43a30697bbb4f67033880b6225cab6e6 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -89,12 +89,15 @@ class Bootstrap { define('FAQ_CATEGORY_TABLE',$prefix.'faq_category'); define('DRAFT_TABLE',$prefix.'draft'); + + define('THREAD_TABLE', $prefix.'thread'); + define('THREAD_ENTRY_TABLE', $prefix.'thread_entry'); + define('THREAD_ENTRY_ATTACHMENT_TABLE', $prefix.'thread_entry_attachment'); + define('THREAD_ENTRY_EMAIL_TABLE', $prefix.'thread_entry_email'); + define('TICKET_TABLE',$prefix.'ticket'); - define('TICKET_THREAD_TABLE',$prefix.'ticket_thread'); - define('TICKET_ATTACHMENT_TABLE',$prefix.'ticket_attachment'); define('TICKET_LOCK_TABLE',$prefix.'ticket_lock'); define('TICKET_EVENT_TABLE',$prefix.'ticket_event'); - define('TICKET_EMAIL_INFO_TABLE',$prefix.'ticket_email_info'); define('TICKET_COLLABORATOR_TABLE', $prefix.'ticket_collaborator'); define('TICKET_STATUS_TABLE', $prefix.'ticket_status'); define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority'); diff --git a/include/ajax.reports.php b/include/ajax.reports.php index bdaa7d9fe3fb828fe7428ed7a2c4cb03101d328d..d432fee88ba7166aff44dba6e0385d89adfedb51 100644 --- a/include/ajax.reports.php +++ b/include/ajax.reports.php @@ -111,9 +111,11 @@ class OverviewReportAjaxAPI extends AjaxController { FORMAT(AVG(DATEDIFF(B2.created, B1.created)),1) AS ResponseTime FROM '.$info['table'].' T1 LEFT JOIN '.TICKET_TABLE.' T2 ON (T2.'.$info['pk'].'=T1.'.$info['pk'].') - LEFT JOIN '.TICKET_THREAD_TABLE.' B2 ON (B2.ticket_id = T2.ticket_id - AND B2.thread_type="R") - LEFT JOIN '.TICKET_THREAD_TABLE.' B1 ON (B2.pid = B1.id) + LEFT JOIN '.THREAD_TABLE.' B0 ON (B0.object_id=T2.ticket_id + AND B0.object_type="T") + LEFT JOIN '.THREAD_ENTRY_TABLE.' B2 ON (B2.thread_id = B0.id + AND B2.`type`="R") + LEFT JOIN '.THREAD_ENTRY_TABLE.' B1 ON (B2.pid = B1.id) LEFT JOIN '.STAFF_TABLE.' S1 ON (S1.staff_id=B2.staff_id) WHERE '.$info['filter'].' AND B1.created BETWEEN '.$start.' AND '.$stop.' GROUP BY T1.'.$info['pk'].' diff --git a/include/class.api.php b/include/class.api.php index 0fd2c17e3f0ab552513c775e5f2f93ee52bc1e7f..9fe2142044062ef86033447ddf72a009a142bf6f 100644 --- a/include/class.api.php +++ b/include/class.api.php @@ -348,9 +348,9 @@ class ApiXmlDataParser extends XmlDataParser { $value['body'] = Format::utf8encode($value['body'], $value['encoding']); if (!strcasecmp($value['type'], 'text/html')) - $value = new HtmlThreadBody($value['body']); + $value = new HtmlThreadEntryBody($value['body']); else - $value = new TextThreadBody($value['body']); + $value = new TextThreadEntryBody($value['body']); } else if ($key == "attachments") { if(!isset($value['file'][':text'])) @@ -393,9 +393,9 @@ class ApiJsonDataParser extends JsonDataParser { $data = Format::parseRfc2397($value, 'utf-8'); if (isset($data['type']) && $data['type'] == 'text/html') - $value = new HtmlThreadBody($data['data']); + $value = new HtmlThreadEntryBody($data['data']); else - $value = new TextThreadBody($data['data']); + $value = new TextThreadEntryBody($data['data']); } else if ($key == "attachments") { foreach ($value as &$info) { diff --git a/include/class.attachment.php b/include/class.attachment.php index d7f2d7f668cbd920cbc25dee19abf8364b5862ca..8bd1c806332fb06cc04e8ecef3e54cb097bd155f 100644 --- a/include/class.attachment.php +++ b/include/class.attachment.php @@ -19,27 +19,24 @@ require_once(INCLUDE_DIR.'class.file.php'); class Attachment { var $id; var $file_id; - var $ticket_id; var $info; - function Attachment($id,$tid=0) { + function Attachment($id, $tid=0) { - $sql='SELECT * FROM '.TICKET_ATTACHMENT_TABLE.' WHERE attach_id='.db_input($id); + $sql = ' SELECT * FROM '.THREAD_ENTRY_ATTACHMENT_TABLE.' WHERE id='.db_input($id); if($tid) - $sql.=' AND ticket_id='.db_input($tid); + $sql.=' AND thread_entry_id='.db_input($tid); if(!($res=db_query($sql)) || !db_num_rows($res)) return false; $this->ht=db_fetch_array($res); - $this->id=$this->ht['attach_id']; + $this->id=$this->ht['id']; $this->file_id=$this->ht['file_id']; - $this->ticket_id=$this->ht['ticket_id']; $this->file=null; - $this->ticket=null; return true; } @@ -48,17 +45,6 @@ class Attachment { return $this->id; } - function getTicketId() { - return $this->ticket_id; - } - - function getTicket() { - if(!$this->ticket && $this->getTicketId()) - $this->ticket = Ticket::lookup($this->getTicketId()); - - return $this->ticket; - } - function getFileId() { return $this->file_id; } @@ -84,23 +70,25 @@ class Attachment { /* Static functions */ function getIdByFileHash($hash, $tid=0) { - $sql='SELECT attach_id FROM '.TICKET_ATTACHMENT_TABLE.' a ' + $sql='SELECT a.id FROM '.THREAD_ENTRY_ATTACHMENT_TABLE.' a ' .' INNER JOIN '.FILE_TABLE.' f ON(f.id=a.file_id) ' .' WHERE f.`key`='.db_input($hash); if($tid) - $sql.=' AND a.ticket_id='.db_input($tid); + $sql.=' AND a.thread_entry_id='.db_input($tid); return db_result(db_query($sql)); } - function lookup($var,$tid=0) { - $id=is_numeric($var)?$var:self::getIdByFileHash($var,$tid); + function lookup($var, $tid=0) { - return ($id && is_numeric($id) - && ($attach = new Attachment($id,$tid)) - && $attach->getId()==$id)?$attach:null; - } + $id = is_numeric($var) ? $var : self::getIdByFileHash($var, $tid); + return ($id + && is_numeric($id) + && ($attach = new Attachment($id, $tid)) + && $attach->getId()==$id + ) ? $attach : null; + } } class GenericAttachments { diff --git a/include/class.export.php b/include/class.export.php index 6f8ec713f3437a2cb21a264a80aa4637966ddd7e..51aec30250c6921671b3f76fbdaf0c9a3323b55d 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -316,8 +316,8 @@ class DatabaseExporter { GROUP_DEPT_TABLE, TEAM_TABLE, TEAM_MEMBER_TABLE, FAQ_TABLE, FAQ_TOPIC_TABLE, FAQ_CATEGORY_TABLE, DRAFT_TABLE, CANNED_TABLE, TICKET_TABLE, ATTACHMENT_TABLE, - TICKET_THREAD_TABLE, TICKET_ATTACHMENT_TABLE, TICKET_PRIORITY_TABLE, - TICKET_LOCK_TABLE, TICKET_EVENT_TABLE, TICKET_EMAIL_INFO_TABLE, + THREAD_TABLE, THREAD_ENTRY_TABLE, THREAD_ENTRY_EMAIL_TABLE, + TICKET_LOCK_TABLE, TICKET_EVENT_TABLE, TICKET_PRIORITY_TABLE, EMAIL_TABLE, EMAIL_TEMPLATE_TABLE, EMAIL_TEMPLATE_GRP_TABLE, FILTER_TABLE, FILTER_RULE_TABLE, SLA_TABLE, API_KEY_TABLE, TIMEZONE_TABLE, SESSION_TABLE, PAGE_TABLE, diff --git a/include/class.file.php b/include/class.file.php index 15f4ea17a0f8dc294672bfa4d695a7a12dc1ff09..7fe6a769942d88d4bd96545496f44d19d2b97a94 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -29,11 +29,14 @@ class AttachmentFile { if(!$id && !($id=$this->getId())) return false; - $sql='SELECT id, f.type, size, name, `key`, signature, ft, bk, f.created, ' - .' count(DISTINCT a.object_id) as canned, count(DISTINCT t.ticket_id) as tickets ' + $sql='SELECT f.id, f.type, size, name, `key`, signature, ft, bk, f.created, ' + .' count(DISTINCT a.object_id) as canned, ' + .' count(DISTINCT t.id) as entries ' .' FROM '.FILE_TABLE.' f ' - .' LEFT JOIN '.ATTACHMENT_TABLE.' a ON(a.file_id=f.id) ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' t ON(t.file_id=f.id) ' + .' LEFT JOIN '.ATTACHMENT_TABLE.' a + ON(a.file_id=f.id) ' + .' LEFT JOIN '.THREAD_ENTRY_ATTACHMENT_TABLE.' t + ON(t.file_id = f.id) ' .' WHERE f.id='.db_input($id) .' GROUP BY f.id'; if(!($res=db_query($sql)) || !db_num_rows($res)) @@ -57,8 +60,8 @@ class AttachmentFile { return $this->getHashtable(); } - function getNumTickets() { - return $this->ht['tickets']; + function getNumEntries() { + return $this->ht['entries']; } function isCanned() { @@ -66,7 +69,7 @@ class AttachmentFile { } function isInUse() { - return ($this->getNumTickets() || $this->isCanned()); + return ($this->getNumEntries() || $this->isCanned()); } function getId() { @@ -577,7 +580,7 @@ class AttachmentFile { // files attached to tickets or other things in the attachment // table and are not logos $sql = 'SELECT id FROM '.FILE_TABLE.' WHERE id NOT IN (' - .'SELECT file_id FROM '.TICKET_ATTACHMENT_TABLE + .'SELECT file_id FROM '.THREAD_ENTRY_ATTACHMENT_TABLE .' UNION ' .'SELECT file_id FROM '.ATTACHMENT_TABLE .") AND `ft` = 'T' AND TIMESTAMPDIFF(DAY, `created`, CURRENT_TIMESTAMP) > 1"; diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 1210bffa3f3bfd924169e87939d369c229a4a9ce..77725c0729e9100733aacfc3c6d58feebb64d032 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -515,7 +515,7 @@ class MailFetcher { foreach ($struct->parameters as $p) { if (strtolower($p->attribute) == 'report-type' && $p->value == 'delivery-status') { - return new TextThreadBody( $this->getPart( + return new TextThreadEntryBody( $this->getPart( $mid, 'text/plain', $this->charset, $struct, false, 1)); } } @@ -555,19 +555,19 @@ class MailFetcher { if ($cfg->isHtmlThreadEnabled()) { if ($html=$this->getPart($mid, 'text/html', $this->charset)) - $body = new HtmlThreadBody($html); + $body = new HtmlThreadEntryBody($html); elseif ($text=$this->getPart($mid, 'text/plain', $this->charset)) - $body = new TextThreadBody($text); + $body = new TextThreadEntryBody($text); } elseif ($text=$this->getPart($mid, 'text/plain', $this->charset)) - $body = new TextThreadBody($text); + $body = new TextThreadEntryBody($text); elseif ($html=$this->getPart($mid, 'text/html', $this->charset)) - $body = new TextThreadBody( + $body = new TextThreadEntryBody( Format::html2text(Format::safe_html($html), 100, false)); if (!isset($body)) - $body = new TextThreadBody(''); + $body = new TextThreadEntryBody(''); if ($cfg->stripQuotedReply()) $body->stripQuotedReply($cfg->getReplySeparator()); diff --git a/include/class.mailparse.php b/include/class.mailparse.php index 4038f1226fa53b3a038bbea912ada03e3ceaf108..c2b51d7f07b46523bebc22292388d8bc494ecc62 100644 --- a/include/class.mailparse.php +++ b/include/class.mailparse.php @@ -304,19 +304,19 @@ class Mail_Parse { if ($cfg && $cfg->isHtmlThreadEnabled()) { if ($html=$this->getPart($this->struct,'text/html')) - $body = new HtmlThreadBody($html); + $body = new HtmlThreadEntryBody($html); elseif ($text=$this->getPart($this->struct,'text/plain')) - $body = new TextThreadBody($text); + $body = new TextThreadEntryBody($text); } elseif ($text=$this->getPart($this->struct,'text/plain')) - $body = new TextThreadBody($text); + $body = new TextThreadEntryBody($text); elseif ($html=$this->getPart($this->struct,'text/html')) - $body = new TextThreadBody( + $body = new TextThreadEntryBody( Format::html2text(Format::safe_html($html), 100, false)); if (!isset($body)) - $body = new TextThreadBody(''); + $body = new TextThreadEntryBody(''); if ($cfg && $cfg->stripQuotedReply()) $body->stripQuotedReply($cfg->getReplySeparator()); diff --git a/include/class.search.php b/include/class.search.php index d4bb2a723c298ab3e267f446b68e277644cc73ba..87cabbbb968c201ac8ac67ccba8a9093f4f0aa20 100644 --- a/include/class.search.php +++ b/include/class.search.php @@ -374,8 +374,7 @@ class MysqlSearchBackend extends SearchBackend { }; // THREADS ---------------------------------- - - $sql = "SELECT A1.`id`, A1.`title`, A1.`body`, A1.`format` FROM `".TICKET_THREAD_TABLE."` A1 + $sql = "SELECT A1.`id`, A1.`title`, A1.`body`, A1.`format` FROM `".THREAD_ENTRY_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='H') WHERE A2.`object_id` IS NULL AND (A1.poster <> 'SYSTEM') AND (LENGTH(A1.`title`) + LENGTH(A1.`body`) > 0) @@ -384,7 +383,7 @@ class MysqlSearchBackend extends SearchBackend { return false; while ($row = db_fetch_row($res)) { - $body = ThreadBody::fromFormattedText($row[2], $row[3]); + $body = ThreadEntryBody::fromFormattedText($row[2], $row[3]); $body = $body->getSearchable(); $title = Format::searchable($row[1]); if (!$body && !$title) diff --git a/include/class.staff.php b/include/class.staff.php index 4fa79eb6a9a62e83065c1c7b80d92cd9b6b1832a..048db0984691bb4837a9bbf9f3480757a1cb91f1 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -564,6 +564,11 @@ implements AuthenticatedUser { if (!parent::delete()) return false; + //Update the poster and clear staff_id on ticket thread table. + db_query('UPDATE '.THREAD_ENTRY_TABLE + .' SET staff_id=0, poster= '.db_input($this->getName()->getOriginal()) + .' WHERE staff_id='.db_input($this->getId())); + // DO SOME HOUSE CLEANING //Move remove any ticket assignments...TODO: send alert to Dept. manager? db_query('UPDATE '.TICKET_TABLE.' SET staff_id=0 WHERE staff_id='.db_input($this->getId())); diff --git a/include/class.thread.php b/include/class.thread.php index 7a95ebd3da025a6cd0e08e66aaa35a0fb2a4a748..81b9f49411a771cd9495a3a508e23ed31c418bd4 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -2,7 +2,7 @@ /********************************************************************* class.thread.php - Ticket thread + Thread of things! XXX: Please DO NOT add any ticket related logic! use ticket class. Peter Rotich <peter@osticket.com> @@ -20,127 +20,91 @@ include_once(INCLUDE_DIR.'class.draft.php'); //Ticket thread. class Thread { - var $id; // same as ticket ID. - var $ticket; - - function Thread($ticket) { - - $this->ticket = $ticket; - - $this->id = 0; + var $ht; - $this->load(); + function Thread($id) { + $this->load($id); } - function load() { + function load($id=0) { - if(!$this->getTicketId()) + if (!$id && !($id=$this->getId())) 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; + $sql='SELECT thread.* ' + .' ,count(DISTINCT attach.id) as attachments ' + .' ,count(DISTINCT entry.id) as entries ' + .' FROM '.THREAD_TABLE.' thread ' + .' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry + ON (entry.thread_id = thread.id) ' + .' LEFT JOIN '.THREAD_ENTRY_ATTACHMENT_TABLE.' attach + ON (attach.thread_entry_id=entry.id) ' + .' WHERE thread.id='.db_input($id) + .' GROUP BY thread.id'; - $this->ht = db_fetch_array($res); + $this->ht = array(); + if (($res=db_query($sql)) && db_num_rows($res)) + $this->ht = db_fetch_array($res); - $this->id = $this->ht['id']; + return ($this->ht); + } - return true; + function reload() { + return $this->load(); } function getId() { - return $this->id; + return $this->ht['id']; } - function getTicketId() { - return $this->getTicket()?$this->getTicket()->getId():0; + function getObjectId() { + return $this->ht['object_id']; } - function getTicket() { - return $this->ticket; + function getObjectType() { + return $this->ht['object_type']; } 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 getNumEntries() { + return $this->ht['entries']; } function getEntries($type, $order='ASC') { - if(!$order || !in_array($order, array('DESC','ASC'))) + if (!$order || !in_array($order, array('DESC','ASC'))) $order='ASC'; - $sql='SELECT thread.* + $sql='SELECT entry.* , COALESCE(user.name, IF(staff.staff_id, CONCAT_WS(" ", staff.firstname, staff.lastname), NULL)) as name ' - .' ,count(DISTINCT attach.attach_id) as attachments ' - .' FROM '.TICKET_THREAD_TABLE.' thread ' + .' ,count(DISTINCT attach.id) as attachments ' + .' FROM '.THREAD_ENTRY_TABLE.' entry ' .' LEFT JOIN '.USER_TABLE.' user - ON (thread.user_id=user.id) ' + ON (entry.user_id=user.id) ' .' LEFT JOIN '.STAFF_TABLE.' staff - ON (thread.staff_id=staff.staff_id) ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach - ON (thread.ticket_id=attach.ticket_id - AND thread.id=attach.ref_id) ' - .' WHERE thread.ticket_id='.db_input($this->getTicketId()); + ON (entry.staff_id=staff.staff_id) ' + .' LEFT JOIN '.THREAD_ENTRY_ATTACHMENT_TABLE.' attach + ON (attach.thread_entry_id = entry.id) ' + .' WHERE entry.thread_id='.db_input($this->getId()); if($type && is_array($type)) - $sql.=' AND thread.thread_type IN('.implode(',', db_input($type)).')'; + $sql.=' AND entry.`type` IN ('.implode(',', db_input($type)).')'; elseif($type) - $sql.=' AND thread.thread_type='.db_input($type); + $sql.=' AND entry.`type` = '.db_input($type); - $sql.=' GROUP BY thread.id ' - .' ORDER BY thread.created '.$order; + $sql.=' GROUP BY entry.id ' + .' ORDER BY entry.created '.$order; $entries = array(); if(($res=db_query($sql)) && db_num_rows($res)) { while($rec=db_fetch_array($res)) { - $rec['body'] = ThreadBody::fromFormattedText($rec['body'], $rec['format']); + $rec['body'] = ThreadEntryBody::fromFormattedText($rec['body'], $rec['format']); $entries[] = $rec; } } @@ -149,71 +113,51 @@ class Thread { } 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); + return ThreadEntry::lookup($id, $this->getId()); } - function addResponse($vars, &$errors) { - - $vars['ticketId'] = $this->getTicketId(); - $vars['userId'] = 0; - - 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(); + $sql = 'DELETE FROM '.THREAD_ENTRY_ATTACHMENT_TABLE. ' a ' + . 'INNER JOIN '.THREAD_ENTRY_TABLE.' e + ON(e.id = a.thread_entry_id) ' + . ' WHERE e.thread_id='.db_input($this->getId()); + + $deleted=0; + if (($res=db_query($sql)) && ($deleted=db_affected_rows())) + AttachmentFile::deleteOrphans(); return $deleted; } function delete() { - $sql = 'UPDATE '.TICKET_EMAIL_INFO_TABLE.' mid - INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.thread_id) - SET mid.headers = null WHERE thread.ticket_id = ' - .db_input($this->getTicketId()); - db_query($sql); + //Self delete + $sql = 'DELETE FROM '.THREAD_TABLE.' WHERE + id='.db_input($this->getId()); - $res=db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getTicketId())); - if(!$res || !db_affected_rows()) + if (!db_query($sql) || !db_affected_rows()) return false; + // Clear email meta data (header..etc) + $sql = 'UPDATE '.THREAD_ENTRY_EMAIL_TABLE.' email ' + . 'INNER JOIN '.THREAD_ENTRY_TABLE.' entry + ON (entry.id = email.thread_entry_id) ' + . 'SET email.headers = null ' + . 'WHERE entry.thread_id = '.db_input($this->getId()); + db_query($sql); + + // Mass delete entries $this->deleteAttachments(); + $sql = 'DELETE FROM '.THREAD_ENTRY_TABLE + . ' WHERE thread_id='.db_input($this->getId()); + db_query($sql); return true; } - /* static */ - function lookup($ticket) { - - return ($ticket - && is_object($ticket) - && ($thread = new Thread($ticket)) - && $thread->getId() - )?$thread:null; - } - function getVar($name) { switch ($name) { case 'original': @@ -226,6 +170,27 @@ class Thread { break; } } + + static function create($vars) { + + if (!$vars || !$vars['object_id'] || !$vars['object_type']) + return false; + + $sql = 'INSERT INTO '.THREAD_TABLE.' SET created=NOW() ' + .', object_id='.db_input($vars['object_id']) + .', object_type='.db_input($vars['object_type']); + + return db_query($sql) ? db_insert_id() : 0; + } + + static function lookup($id) { + + return ($id + && ($thread = new Thread($id)) + && $thread->getId() + ) + ? $thread : null; + } } @@ -234,46 +199,41 @@ Class ThreadEntry { var $id; var $ht; - var $staff; - var $ticket; - + var $thread; var $attachments; - - function ThreadEntry($id, $type='', $ticketId=0) { - $this->load($id, $type, $ticketId); + function ThreadEntry($id, $threadId=0, $type='') { + $this->load($id, $threadId, $type); } - function load($id=0, $type='', $ticketId=0) { + function load($id=0, $threadId=0, $type='') { - if(!$id && !($id=$this->getId())) + if (!$id && !($id=$this->getId())) return false; - $sql='SELECT thread.*, info.email_mid, info.headers ' - .' ,count(DISTINCT attach.attach_id) as attachments ' - .' FROM '.TICKET_THREAD_TABLE.' thread ' - .' LEFT JOIN '.TICKET_EMAIL_INFO_TABLE.' info - ON (thread.id=info.thread_id) ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach - ON (thread.ticket_id=attach.ticket_id - AND thread.id=attach.ref_id) ' - .' WHERE thread.id='.db_input($id); + $sql='SELECT entry.*, email.mid, email.headers ' + .' ,count(DISTINCT attach.id) as attachments ' + .' FROM '.THREAD_ENTRY_TABLE.' entry ' + .' LEFT JOIN '.THREAD_ENTRY_EMAIL_TABLE.' email + ON (email.thread_entry_id=entry.id) ' + .' LEFT JOIN '.THREAD_ENTRY_ATTACHMENT_TABLE.' attach + ON (attach.thread_entry_id=entry.id) ' + .' WHERE entry.id='.db_input($id); - if($type) - $sql.=' AND thread.thread_type='.db_input($type); + if ($type) + $sql.=' AND entry.type='.db_input($type); - if($ticketId) - $sql.=' AND thread.ticket_id='.db_input($ticketId); + if ($threadId) + $sql.=' AND entry.thread_id='.db_input($threadId); - $sql.=' GROUP BY thread.id '; + $sql.=' GROUP BY entry.id '; - if(!($res=db_query($sql)) || !db_num_rows($res)) + if (!($res=db_query($sql)) || !db_num_rows($res)) return false; $this->ht = db_fetch_array($res); $this->id = $this->ht['id']; - $this->staff = $this->ticket = null; $this->attachments = array(); return true; @@ -292,7 +252,7 @@ Class ThreadEntry { } function getType() { - return $this->ht['thread_type']; + return $this->ht['type']; } function getSource() { @@ -308,20 +268,20 @@ Class ThreadEntry { } function getBody() { - return ThreadBody::fromFormattedText($this->ht['body'], $this->ht['format']); + return ThreadEntryBody::fromFormattedText($this->ht['body'], $this->ht['format']); } function setBody($body) { global $cfg; - if (!$body instanceof ThreadBody) { + if (!$body instanceof ThreadEntryBody) { if ($cfg->isHtmlThreadEnabled()) - $body = new HtmlThreadBody($body); + $body = new HtmlThreadEntryBody($body); else - $body = new TextThreadBody($body); + $body = new TextThreadEntryBody($body); } - $sql='UPDATE '.TICKET_THREAD_TABLE.' SET updated=NOW()' + $sql='UPDATE '.THREAD_ENTRY_TABLE.' SET updated=NOW()' .',format='.db_input($body->getType()) .',body='.db_input((string) $body) .' WHERE id='.db_input($this->getId()); @@ -340,12 +300,8 @@ Class ThreadEntry { return $this->ht['attachments']; } - function getTicketId() { - return $this->ht['ticket_id']; - } - function getEmailMessageId() { - return $this->ht['email_mid']; + return $this->ht['mid']; } function getEmailHeaderArray() { @@ -398,12 +354,16 @@ Class ThreadEntry { } - function getTicket() { + function getThreadId() { + return $this->ht['thread_id']; + } + + function getThread() { - if(!$this->ticket && $this->getTicketId()) - $this->ticket = Ticket::lookup($this->getTicketId()); + if(!$this->thread && $this->getThreadId()) + $this->thread = Thread::lookup($this->getThreadId()); - return $this->ticket; + return $this->thread; } function getStaffId() { @@ -548,26 +508,26 @@ Class ThreadEntry { $inline = is_array($file) && @$file['inline']; - // TODO: Add a unique index to TICKET_ATTACHMENT_TABLE (file_id, - // ref_id), and remove this block - if ($id = db_result(db_query('SELECT attach_id FROM '.TICKET_ATTACHMENT_TABLE - .' WHERE file_id='.db_input($fileId).' AND ref_id=' - .db_input($this->getId())))) + // TODO: Add a unique index to THREAD_ENTRY_ATTACHMENT_TABLE (file_id, + // thread_entry_id), and remove this block + if ($id = db_result(db_query('SELECT id FROM '.THREAD_ENTRY_ATTACHMENT_TABLE + .' WHERE file_id='.db_input($fileId) + .' AND thread_entry_id=' .db_input($this->getId())))) + return $id; - $sql ='INSERT IGNORE INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() ' + $sql ='INSERT IGNORE INTO '.THREAD_ENTRY_ATTACHMENT_TABLE.' SET created=NOW() ' .' ,file_id='.db_input($fileId) - .' ,ticket_id='.db_input($this->getTicketId()) - .' ,inline='.db_input($inline ? 1 : 0) - .' ,ref_id='.db_input($this->getId()); + .' ,thread_entry_id='.db_input($this->getId()) + .' ,inline='.db_input($inline ? 1 : 0); return (db_query($sql) && ($id=db_insert_id()))?$id:0; } function saveAttachments($files) { $ids=array(); - foreach($files as $file) - if(($id=$this->saveAttachment($file))) + foreach ($files as $file) + if (($id=$this->saveAttachment($file))) $ids[] = $id; return $ids; @@ -575,19 +535,19 @@ Class ThreadEntry { function getAttachments() { - if($this->attachments) + 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, lower(f.`key`) as file_hash, f.name, a.inline ' + $sql='SELECT a.id, f.id as file_id, f.size, lower(f.`key`) as file_hash, f.name, a.inline ' .' 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()); + .' INNER JOIN '.THREAD_ENTRY_ATTACHMENT_TABLE.' a + ON(a.file_id=f.id) ' + .' WHERE a.thread_entry_id='.db_input($this->getId()); $this->attachments = array(); - if(($res=db_query($sql)) && db_num_rows($res)) { - while($rec=db_fetch_array($res)) + if (($res=db_query($sql)) && db_num_rows($res)) { + while ($rec=db_fetch_array($res)) $this->attachments[] = $rec; } @@ -598,7 +558,7 @@ Class ThreadEntry { $json = array(); foreach ($this->getAttachments() as $att) { $json[$att['file_hash']] = array( - 'download_url' => sprintf('attachment.php?id=%d&h=%s', $att['attach_id'], + 'download_url' => sprintf('attachment.php?id=%d&h=%s', $att['id'], strtolower(md5($att['file_id'].session_id().$att['file_hash']))), 'filename' => $att['name'], ); @@ -619,7 +579,7 @@ Class ThreadEntry { $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size'])); $str.=sprintf('<a class="Icon file no-pjax" href="%s?id=%d&h=%s" target="%s">%s</a>%s %s', - $file, $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); + $file, $attachment['id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); } return $str; @@ -774,12 +734,17 @@ Class ThreadEntry { /* static */ function logEmailHeaders($id, $mid, $header=false) { - $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE - .' SET thread_id='.db_input($id) - .', email_mid='.db_input($mid); //TODO: change it to message_id. + + if (!$id || !$mid) + return false; + + $sql='INSERT INTO '.THREAD_ENTRY_EMAIL_TABLE + .' SET thread_entry_id='.db_input($id) + .', mid='.db_input($mid); if ($header) $sql .= ', headers='.db_input($header); - return db_query($sql)?db_insert_id():0; + + return db_query($sql) ? db_insert_id() : 0; } /* variables */ @@ -809,12 +774,10 @@ Class ThreadEntry { return false; } - /* static calls */ - - function lookup($id, $tid=0, $type='') { + static function lookup($id, $tid=0, $type='') { return ($id && is_numeric($id) - && ($e = new ThreadEntry($id, $type, $tid)) + && ($e = new ThreadEntry($id, $tid, $type)) && $e->getId()==$id )?$e:null; } @@ -835,8 +798,9 @@ Class ThreadEntry { function lookupByEmailHeaders(&$mailinfo, &$seen=false) { // Search for messages using the References header, then the // in-reply-to header - $search = 'SELECT thread_id, email_mid FROM '.TICKET_EMAIL_INFO_TABLE - . ' WHERE email_mid=%s ORDER BY thread_id DESC'; + $search = 'SELECT thread_entery_id, mid FROM '.THREAD_ENTRY_EMAIL_TABLE + . ' WHERE mid=%s ' + . ' ORDER BY thread_entry_id DESC'; if (list($id, $mid) = db_fetch_row(db_query( sprintf($search, db_input($mailinfo['mid']))))) { @@ -971,7 +935,7 @@ Class ThreadEntry { global $ost; $domain = md5($ost->getConfig()->getURL()); - $ticket = $this->getTicket(); + $ticket = $this->getThread()->getObject(); return sprintf('$%s$%s@%s', base64_encode(pack('V', $this->getId())), substr(md5($to . $ticket->getNumber() . $ticket->getId()), -10), @@ -980,19 +944,19 @@ Class ThreadEntry { } //new entry ... we're trusting the caller to check validity of the data. - function create($vars) { + static function create($vars) { global $cfg; //Must have... - if(!$vars['ticketId'] || !$vars['type'] || !in_array($vars['type'], array('M','R','N'))) + if (!$vars['threadId'] || !$vars['type']) return false; - if (!$vars['body'] instanceof ThreadBody) { + if (!$vars['body'] instanceof ThreadEntryBody) { if ($cfg->isHtmlThreadEnabled()) - $vars['body'] = new HtmlThreadBody($vars['body']); + $vars['body'] = new HtmlThreadEntryBody($vars['body']); else - $vars['body'] = new TextThreadBody($vars['body']); + $vars['body'] = new TextThreadEntryBody($vars['body']); } // Drop stripped images @@ -1008,7 +972,7 @@ Class ThreadEntry { } // Handle extracted embedded images (<img src="data:base64,..." />). - // The extraction has already been performed in the ThreadBody + // The extraction has already been performed in the ThreadEntryBody // class. Here they should simply be added to the attachments list if ($atts = $vars['body']->getEmbeddedHtmlImages()) { if (!is_array($vars['attachments'])) @@ -1025,22 +989,22 @@ Class ThreadEntry { if ($poster && is_object($poster)) $poster = (string) $poster; - $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'], true)) - .' ,format='.db_input($vars['body']->getType()) - .' ,staff_id='.db_input($vars['staffId']) - .' ,user_id='.db_input($vars['userId']) - .' ,poster='.db_input($poster) - .' ,source='.db_input($vars['source']); + $sql=' INSERT INTO '.THREAD_ENTRY_TABLE.' SET `created` = NOW() ' + .' ,`type` = '.db_input($vars['type']) + .' ,`thread_id` = '.db_input($vars['threadId']) + .' ,`title` = '.db_input(Format::sanitize($vars['title'], true)) + .' ,`format` = '.db_input($vars['body']->getType()) + .' ,`staff_id` = '.db_input($vars['staffId']) + .' ,`user_id` = '.db_input($vars['userId']) + .' ,`poster` = '.db_input($poster) + .' ,`source` = '.db_input($vars['source']); if (!isset($vars['attachments']) || !$vars['attachments']) // Otherwise, body will be configured in a block below (after // inline attachments are saved and updated in the database) $sql.=' ,body='.db_input($body); - if(isset($vars['pid'])) + if (isset($vars['pid'])) $sql.=' ,pid='.db_input($vars['pid']); // Check if 'reply_to' is in the $vars as the previous ThreadEntry // instance. If the body of the previous message is found in the new @@ -1049,11 +1013,12 @@ Class ThreadEntry { && $vars['reply_to'] instanceof ThreadEntry) $sql.=' ,pid='.db_input($vars['reply_to']->getId()); - if($vars['ip_address']) + 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']))) + if (!db_query($sql) + || !($entry=self::lookup(db_insert_id(), $vars['threadId']))) return false; /************* ATTACHMENTS *****************/ @@ -1086,16 +1051,20 @@ Class ThreadEntry { 'src="cid:'.$a['key'].'"', $body); } } - $sql = 'UPDATE '.TICKET_THREAD_TABLE.' SET body='.db_input($body) + + $sql = 'UPDATE '.THREAD_ENTRY_TABLE + .' SET body='.db_input($body) .' WHERE `id`='.db_input($entry->getId()); + if (!db_query($sql) || !db_affected_rows()) return false; } // Email message id (required for all thread posts) if (!isset($vars['mid'])) - $vars['mid'] = sprintf('<%s@%s>', Misc::randCode(24), - substr(md5($cfg->getUrl()), -10)); + $vars['mid'] = sprintf('<%s@%s>', + Misc::randCode(24), substr(md5($cfg->getUrl()), -10)); + $entry->saveEmailInfo($vars); // Inline images (attached to the draft) @@ -1106,172 +1075,13 @@ Class ThreadEntry { return $entry; } - function add($vars) { - return ($entry=self::create($vars))?$entry->getId():0; + static function add($vars) { + return ($entry=self::create($vars)) ? $entry->getId() : 0; } } -/* Message - Ticket thread entry of type message */ -class Message extends ThreadEntry { - - function Message($id, $ticketId=0) { - parent::ThreadEntry($id, 'M', $ticketId); - } - - function getSubject() { - return $this->getTitle(); - } - - 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 content is required'); - - if($errors) return false; - - $vars['type'] = 'M'; - $vars['body'] = $vars['message']; - - if (!$vars['poster'] - && $vars['userId'] - && ($user = User::lookup($vars['userId']))) - $vars['poster'] = (string) $user->getName(); - - return ThreadEntry::add($vars); - } - - function lookup($id, $tid=0, $type='M') { - - return ($id - && is_numeric($id) - && ($m = new Message($id, $tid)) - && $m->getId()==$id - )?$m:null; - } - - function lastByTicketId($ticketId) { - return self::byTicketId($ticketId); - } - - function firstByTicketId($ticketId) { - return self::byTicketId($ticketId, false); - } - - function byTicketId($ticketId, $last=true) { - - $sql=' SELECT thread.id FROM '.TICKET_THREAD_TABLE.' thread ' - .' WHERE thread_type=\'M\' AND thread.ticket_id = '.db_input($ticketId) - .sprintf(' ORDER BY thread.id %s LIMIT 1', $last ? 'DESC' : 'ASC'); - - if (($res = db_query($sql)) && ($id = db_result($res))) - return Message::lookup($id); - - return null; - } -} - -/* Response - Ticket thread entry of type response */ -class Response extends ThreadEntry { - - function Response($id, $ticketId=0) { - parent::ThreadEntry($id, 'R', $ticketId); - } - - function getSubject() { - return $this->getTitle(); - } - - function getRespondent() { - return $this->getStaff(); - } - - 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 content is required'); - - if($errors) return false; - - $vars['type'] = 'R'; - $vars['body'] = $vars['response']; - if(!$vars['pid'] && $vars['msgId']) - $vars['pid'] = $vars['msgId']; - - if (!$vars['poster'] - && $vars['staffId'] - && ($staff = Staff::lookup($vars['staffId']))) - $vars['poster'] = (string) $staff->getName(); - - return ThreadEntry::add($vars); - } - - - function lookup($id, $tid=0, $type='R') { - - return ($id - && is_numeric($id) - && ($r = new Response($id, $tid)) - && $r->getId()==$id - )?$r:null; - } -} - -/* Note - Ticket thread entry of type note (Internal Note) */ -class Note extends ThreadEntry { - - function Note($id, $ticketId=0) { - parent::ThreadEntry($id, 'N', $ticketId); - } - - function getMessage() { - return $this->getBody(); - } - - /* 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 content is 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->getId()==$id - )?$n:null; - } -} - -class ThreadBody /* extends SplString */ { +class ThreadEntryBody /* extends SplString */ { static $types = array('text', 'html'); @@ -1286,7 +1096,7 @@ class ThreadBody /* extends SplString */ { function __construct($body, $type='text', $options=array()) { $type = strtolower($type); if (!in_array($type, static::$types)) - throw new Exception("$type: Unsupported ThreadBody type"); + throw new Exception("$type: Unsupported ThreadEntryBody type"); $this->body = (string) $body; if (strlen($this->body) > 250000) { $max_packet = db_get_variable('max_allowed_packet', 'global'); @@ -1309,10 +1119,10 @@ class ThreadBody /* extends SplString */ { $conv = $this->type . ':' . strtolower($type); switch ($conv) { case 'text:html': - return new ThreadBody(sprintf('<pre>%s</pre>', + return new ThreadEntryBody(sprintf('<pre>%s</pre>', Format::htmlchars($this->body)), $type); case 'html:text': - return new ThreadBody(Format::html2text((string) $this), $type); + return new ThreadEntryBody(Format::html2text((string) $this), $type); } } @@ -1376,16 +1186,16 @@ class ThreadBody /* extends SplString */ { static function fromFormattedText($text, $format=false) { switch ($format) { case 'text': - return new TextThreadBody($text); + return new TextThreadEntryBody($text); case 'html': - return new HtmlThreadBody($text, array('strip-embedded'=>false)); + return new HtmlThreadEntryBody($text, array('strip-embedded'=>false)); default: - return new ThreadBody($text); + return new ThreadEntryBody($text); } } } -class TextThreadBody extends ThreadBody { +class TextThreadEntryBody extends ThreadEntryBody { function __construct($body, $options=array()) { parent::__construct($body, 'text', $options); } @@ -1416,7 +1226,7 @@ class TextThreadBody extends ThreadBody { return $this->display('email'); } } -class HtmlThreadBody extends ThreadBody { +class HtmlThreadEntryBody extends ThreadEntryBody { function __construct($body, $options=array()) { if (!isset($options['strip-embedded']) || $options['strip-embedded']) $body = $this->extractEmbeddedHtmlImages($body); @@ -1461,4 +1271,269 @@ class HtmlThreadBody extends ThreadBody { } } } + + +/* Message - Ticket thread entry of type message */ +class MessageThreadEntry extends ThreadEntry { + + const ENTRY_TYPE = 'M'; + + function MessageThreadEntry($id, $threadId=0) { + parent::ThreadEntry($id, $threadId, self::ENTRY_TYPE); + } + + function getSubject() { + return $this->getTitle(); + } + + static function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + static function add($vars, &$errors) { + + if (!$vars || !is_array($vars) || !$vars['threadId']) + $errors['err'] = __('Missing or invalid data'); + elseif (!$vars['message']) + $errors['message'] = __('Message content is required'); + + if ($errors) return false; + + $vars['type'] = self::ENTRY_TYPE; + $vars['body'] = $vars['message']; + + if (!$vars['poster'] + && $vars['userId'] + && ($user = User::lookup($vars['userId']))) + $vars['poster'] = (string) $user->getName(); + + return parent::add($vars); + } + + static function lookup($id, $tid=0) { + + return ($id + && is_numeric($id) + && ($m = new MessageThreadEntry($id, $tid)) + && $m->getId()==$id + )?$m:null; + } + + //TODO: redo shit below. + + function lastByTicketId($ticketId) { + return self::byTicketId($ticketId); + } + + function firstByTicketId($ticketId) { + return self::byTicketId($ticketId, false); + } + + function byTicketId($ticketId, $last=true) { + + $sql=' SELECT thread.id FROM '.TICKET_THREAD_TABLE.' thread ' + .' WHERE thread_type=\'M\' AND thread.ticket_id = '.db_input($ticketId) + .sprintf(' ORDER BY thread.id %s LIMIT 1', $last ? 'DESC' : 'ASC'); + + if (($res = db_query($sql)) && ($id = db_result($res))) + return Message::lookup($id); + + return null; + } +} + + +/* thread entry of type response */ +class ResponseThreadEntry extends ThreadEntry { + + const ENTRY_TYPE = 'R'; + + function ResponseThreadEntry($id, $threadId=0) { + parent::ThreadEntry($id, $threadId, self::ENTRY_TYPE); + } + + function getSubject() { + return $this->getTitle(); + } + + function getRespondent() { + return $this->getStaff(); + } + + static function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + static function add($vars, &$errors) { + + if (!$vars || !is_array($vars) || !$vars['threadId']) + $errors['err'] = __('Missing or invalid data'); + elseif (!$vars['response']) + $errors['response'] = __('Response content is required'); + + if ($errors) return false; + + $vars['type'] = self::ENTRY_TYPE; + $vars['body'] = $vars['response']; + if (!$vars['pid'] && $vars['msgId']) + $vars['pid'] = $vars['msgId']; + + if (!$vars['poster'] + && $vars['staffId'] + && ($staff = Staff::lookup($vars['staffId']))) + $vars['poster'] = (string) $staff->getName(); + + return parent::add($vars); + } + + static function lookup($id, $tid=0) { + + return ($id + && is_numeric($id) + && ($r = new ResponseThreadEntry($id, $tid)) + && $r->getId()==$id + )?$r:null; + } +} + +/* Thread entry of type note (Internal Note) */ +class NoteThreadEntry extends ThreadEntry { + const ENTRY_TYPE = 'N'; + + function NoteThreadEntry($id, $threadId=0) { + parent::ThreadEntry($id, $threadId, self::ENTRY_TYPE); + } + + function getMessage() { + return $this->getBody(); + } + + static function create($vars, &$errors) { + return self::lookup(self::add($vars, $errors)); + } + + static function add($vars, &$errors) { + + //Check required params. + if (!$vars || !is_array($vars) || !$vars['threadId']) + $errors['err'] = __('Missing or invalid data'); + elseif (!$vars['note']) + $errors['note'] = __('Note content is required'); + + if ($errors) return false; + + //TODO: use array_intersect_key when we move to php 5 to extract just what we need. + $vars['type'] = self::ENTRY_TYPE; + $vars['body'] = $vars['note']; + + return parent::add($vars); + } + + static function lookup($id, $tid=0) { + + return ($id + && is_numeric($id) + && ($n = new NoteThreadEntry($id, $tid)) + && $n->getId()==$id + )?$n:null; + } +} + +// Ticket specific thread utils. +class TicketThread extends Thread { + private $_entries = array(); + + function __construct($id) { + + parent::__construct($id); + + if ($this->getId()) { + $sql= ' SELECT `type`, count(DISTINCT e.id) as count ' + .' FROM '.THREAD_TABLE. ' t ' + .' INNER JOIN '.THREAD_ENTRY_TABLE. ' e ON (e.thread_id = t.id) ' + .' WHERE t.id='.db_input($this->getId()) + .' GROUP BY e.`type`'; + + if (($res=db_query($sql)) && db_num_rows($res)) { + while ($row=db_fetch_row($res)) + $this->_entries[$row[0]] = $row[1]; + } + } + } + + function getNumMessages() { + return $this->_entries[MessageThreadEntry::ENTRY_TYPE]; + } + + function getNumResponses() { + return $this->_entries[ResponseThreadEntry::ENTRY_TYPE]; + } + + function getNumNotes() { + return $this->_entries[NoteThreadEntry::ENTRY_TYPE]; + } + + function getMessages() { + return $this->getEntries(MessageThreadEntry::ENTRY_TYPE); + } + + function getResponses() { + return $this->getEntries(ResponseThreadEntry::ENTRY_TYPE); + } + + function getNotes() { + return $this->getEntries(NoteThreadEntry::ENTRY_TYPE); + } + + function addNote($vars, &$errors) { + + //Add ticket Id. + $vars['threadId'] = $this->getId(); + return NoteThreadEntry::create($vars, $errors); + } + + function addMessage($vars, &$errors) { + + $vars['threadId'] = $this->getId(); + $vars['staffId'] = 0; + + return MessageThreadEntry::create($vars, $errors); + } + + function addResponse($vars, &$errors) { + + $vars['threadId'] = $this->getId(); + $vars['userId'] = 0; + + return ResponseThreadEntry::create($vars, $errors); + } + + //TODO: revisit + function getVar($name) { + switch ($name) { + case 'original': + return MessageThreadEntry::first($this->getId())->getBody(); + break; + case 'last_message': + case 'lastmessage': + return $this->ticket->getLastMessage()->getBody(); + break; + } + } + + static function create($ticket) { + $id = is_object($ticket) ? $ticket->getId() : $ticket; + return parent::create(array( + 'object_id' => $id, + 'object_type' => 'T')); + } + + static function lookup($id) { + + return ($id + && ($t= new TicketThread($id)) + && $t->getId() + ) ? $t : null; + } +} ?> diff --git a/include/class.ticket.php b/include/class.ticket.php index 8227d1142b1533cde82b53b28b522b67ba892e54..a092fa661e5300e917b3abc657a1365304db8972 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -183,23 +183,27 @@ class Ticket { function load($id=0) { - if(!$id && !($id=$this->getId())) + if (!$id && !($id=$this->getId())) return false; - $sql='SELECT ticket.*, lock_id, dept.name as dept_name ' + $sql='SELECT ticket.*, thread.id as thread_id, lock_id, dept.name as dept_name ' .' ,count(distinct attach.attach_id) as attachments' .' 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 '.TICKET_LOCK_TABLE.' tlock ON ( ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) ' - .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach - ON ( ticket.ticket_id=attach.ticket_id) ' + .' 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 '.THREAD_ENTRY_ATTACHMENT_TABLE.' attach + ON ( attach.thread_entry_id = entry.id ) ' .' WHERE ticket.ticket_id='.db_input($id) .' GROUP BY ticket.ticket_id'; //echo $sql; - if(!($res=db_query($sql)) || !db_num_rows($res)) + if (!($res=db_query($sql)) || !db_num_rows($res)) return false; @@ -657,19 +661,25 @@ class Ticket { function getLastRespondent() { - $sql ='SELECT resp.staff_id ' - .' FROM '.TICKET_THREAD_TABLE.' resp ' - .' LEFT JOIN '.STAFF_TABLE. ' USING(staff_id) ' - .' WHERE resp.ticket_id='.db_input($this->getId()).' AND resp.staff_id>0 ' - .' AND resp.thread_type="R"' - .' ORDER BY resp.created DESC LIMIT 1'; + if (!isset($this->lastrespondent)) { - if(!($res=db_query($sql)) || !db_num_rows($res)) - return null; + $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); - list($id)=db_fetch_row($res); + $this->lastrespondent = Staff::lookup($id); + } - return Staff::lookup($id); + return $this->lastrespondent; } @@ -706,10 +716,14 @@ class Ticket { return $this->last_message; } + function getThreadId() { + return $this->ht['thread_id']; + } + function getThread() { - if(!$this->thread) - $this->thread = Thread::lookup($this); + if (!$this->thread && $this->getThreadId()) + $this->thread = TicketThread::lookup($this->getThreadId()); return $this->thread; } @@ -1930,10 +1944,10 @@ class Ticket { $files[] = $file['id']; if ($cfg->isHtmlThreadEnabled()) - $response = new HtmlThreadBody( + $response = new HtmlThreadEntryBody( $this->replaceVars($canned->getHtml())); else - $response = new TextThreadBody( + $response = new TextThreadEntryBody( $this->replaceVars($canned->getPlainText())); $info = array('msgId' => $msgId, @@ -2002,6 +2016,7 @@ class Ticket { && $cfg->autoClaimTickets()) $this->setStaffId($thisstaff->getId()); //direct assignment; + $this->lastrespondent = null; $this->onResponse(); //do house cleaning.. /* email the user?? - if disabled - then bail out */ @@ -2084,7 +2099,7 @@ class Ticket { $errors = array(); //Unless specified otherwise, assume HTML if ($note && is_string($note)) - $note = new HtmlThreadBody($note); + $note = new HtmlThreadEntryBody($note); return $this->postNote( array( @@ -2833,7 +2848,10 @@ class Ticket { $sql.=' ,duedate='.db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))); - if(!db_query($sql) || !($id=db_insert_id()) || !($ticket =Ticket::lookup($id))) + if(!db_query($sql) + || !($id=db_insert_id()) + || !($thread=TicketThread::create($id)) + || !($ticket =Ticket::lookup($id))) return null; /* -------------------- POST CREATE ------------------------ */ diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 2a98a5c72acd99ddf44cd9f5a0fef896ef7dc23d..8e3aa3fcf49e1828ff7766ec3374c1d76f3a1da3 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -114,12 +114,12 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) { foreach($thread as $entry) { //Making sure internal notes are not displayed due to backend MISTAKES! - if(!$threadType[$entry['thread_type']]) continue; + if(!$threadType[$entry['type']]) continue; $poster = $entry['poster']; - if($entry['thread_type']=='R' && ($cfg->hideStaffName() || !$entry['staff_id'])) + if($entry['type']=='R' && ($cfg->hideStaffName() || !$entry['staff_id'])) $poster = ' '; ?> - <table class="thread-entry <?php echo $threadType[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="800" border="0"> + <table class="thread-entry <?php echo $threadType[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="800" border="0"> <tr><th><div> <?php echo Format::datetime($entry['created']); ?> <span class="textra"></span> diff --git a/include/staff/templates/tickets.tmpl.php b/include/staff/templates/tickets.tmpl.php index d85b1e4c57d4807c4be14204f5b93f8a41f89fff..338e4c2b33ae1be5cddcab8ebc62e926609073dc 100644 --- a/include/staff/templates/tickets.tmpl.php +++ b/include/staff/templates/tickets.tmpl.php @@ -40,12 +40,15 @@ while ($row = db_fetch_array($res)) if ($results) { $counts_sql = 'SELECT ticket.ticket_id, - count(DISTINCT attach.attach_id) as attachments, - count(DISTINCT thread.id) as thread_count, + count(DISTINCT attach.id) as attachments, + count(DISTINCT entry.id) as thread_count, count(DISTINCT collab.id) as collaborators - FROM '.TICKET_TABLE.' ticket - LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) ' - .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ( ticket.ticket_id=thread.ticket_id) ' + FROM '.TICKET_TABLE.' ticket ' + .' 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") ' .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab ON ( ticket.ticket_id=collab.ticket_id) ' .' WHERE ticket.ticket_id IN ('.implode(',', db_input(array_keys($results))).') diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index abdb74ed1a71040920afeb5f46cea4c3fc8da4d3..25ceb77835578ee84e655a005131cdc845abf270 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -387,7 +387,7 @@ $tcount+= $ticket->getNumNotes(); $types = array('M', 'R', 'N'); if(($thread=$ticket->getThreadEntries($types))) { foreach($thread as $entry) { ?> - <table class="thread-entry <?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> + <table class="thread-entry <?php echo $threadTypes[$entry['type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0"> <tr> <th colspan="4" width="100%"> <div> @@ -429,8 +429,8 @@ $tcount+= $ticket->getNumNotes(); } ?> </table> <?php - if($entry['thread_type']=='M') - $msgId=$entry['id']; + if ($entry['type'] == 'M') + $msgId = $entry['id']; } } else { echo '<p>'.__('Error fetching ticket thread - get technical help.').'</p>'; diff --git a/include/upgrader/streams/core/b26f29a6-186868f5.cleanup.sql b/include/upgrader/streams/core/b26f29a6-186868f5.cleanup.sql new file mode 100644 index 0000000000000000000000000000000000000000..27dca7f84160f3916b8e4bbb968af9622ae89979 --- /dev/null +++ b/include/upgrader/streams/core/b26f29a6-186868f5.cleanup.sql @@ -0,0 +1,9 @@ +ALTER TABLE `%TABLE_PREFIX%thread` + DROP COLUMN `tid`; + +ALTER TABLE `%TABLE_PREFIX%thread_entry` + DROP COLUMN `ticket_id`; + +OPTIMIZE TABLE `%TABLE_PREFIX%ticket`; +OPTIMIZE TABLE `%TABLE_PREFIX%thread`; +OPTIMIZE TABLE `%TABLE_PREFIX%thread_entry`; diff --git a/include/upgrader/streams/core/b26f29a6-186868f5.patch.sql b/include/upgrader/streams/core/b26f29a6-186868f5.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..7f1898fc6b35b62f35c6af30193337950f81d94f --- /dev/null +++ b/include/upgrader/streams/core/b26f29a6-186868f5.patch.sql @@ -0,0 +1,80 @@ +/** + * @version v1.9.5 + * @signature 2257f6f22ca4b31bea6045b8b7d59d56 + * @title Threads revisited + * + * This patch adds ability to thread anything + * + */ + +-- Add thread table +DROP TABLE IF EXISTS `%TABLE_PREFIX%thread`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%thread` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `object_id` int(11) unsigned NOT NULL, + `object_type` char(1) NOT NULL, + `extra` text, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `object_id` (`object_id`), + KEY `object_type` (`object_type`) +) DEFAULT CHARSET=utf8; + +-- create threads +INSERT INTO `%TABLE_PREFIX%thread` + (`object_id`, `object_type`, `created`) + SELECT t1.ticket_id, 'T', t1.created + FROM `%TABLE_PREFIX%ticket_thread` t1 + JOIN ( + SELECT ticket_id, MIN(id) as id + FROM `%TABLE_PREFIX%ticket_thread` + WHERE `thread_type` = 'M' + GROUP BY ticket_id + ) t2 + ON (t1.ticket_id=t2.ticket_id and t1.id=t2.id) + ORDER BY t1.created; + +ALTER TABLE `%TABLE_PREFIX%ticket_thread` + ADD `thread_id` INT( 11 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `pid` , + ADD INDEX ( `thread_id` ); + +UPDATE `%TABLE_PREFIX%ticket_thread` t1 + LEFT JOIN `%TABLE_PREFIX%thread` t2 ON ( t2.object_id = t1.ticket_id ) + SET t1.thread_id = t2.id; + +-- convert ticket_thread to thread_entry +ALTER TABLE `%TABLE_PREFIX%ticket_thread` + CHANGE `thread_type` `type` CHAR( 1 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + ADD INDEX ( `type` ); + +RENAME TABLE `%TABLE_PREFIX%ticket_thread` TO `%TABLE_PREFIX%thread_entry` ; + +-- add thread id to ticket table +ALTER TABLE `%TABLE_PREFIX%ticket` + ADD `thread_id` INT( 11 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `number` , + ADD INDEX ( `thread_id` ); + +UPDATE `%TABLE_PREFIX%ticket` t1 + LEFT JOIN `%TABLE_PREFIX%thread` t2 ON ( t2.object_id = t1.ticket_id ) + SET t1.thread_id = t2.id; + +-- convert ticket_attachment to thread_entry_attachment +ALTER TABLE `%TABLE_PREFIX%ticket_attachment` + CHANGE `attach_id` `id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, + CHANGE `ref_id` `thread_entry_id` INT( 11 ) UNSIGNED NOT NULL DEFAULT '0'; + DROP `ticket_id`; + +RENAME TABLE `%TABLE_PREFIX%ticket_attachment` TO `%TABLE_PREFIX%thread_entry_attachment`; + +-- convert ticket_email_info to thread_entry_mid +ALTER TABLE `%TABLE_PREFIX%ticket_email_info` + CHANGE `thread_id` `thread_entry_id` INT( 11 ) UNSIGNED NOT NULL, + CHANGE `email_mid` `mid` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + ADD INDEX ( `thread_entry_id` ); + +RENAME TABLE `%TABLE_PREFIX%ticket_email_info` TO `%TABLE_PREFIX%thread_entry_email`; + +-- Set new schema signature +UPDATE `%TABLE_PREFIX%config` + SET `value` = '2257f6f22ca4b31bea6045b8b7d59d56' + WHERE `key` = 'schema_signature' AND `namespace` = 'core'; diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql index 3462ad310ad8d379387bd04feee3e89712e364a1..7f968dbd6f542276be62ac09f69150c1d5ff5cdc 100644 --- a/setup/inc/streams/core/install-mysql.sql +++ b/setup/inc/streams/core/install-mysql.sql @@ -609,6 +609,65 @@ CREATE TABLE `%TABLE_PREFIX%team_member` ( PRIMARY KEY (`team_id`,`staff_id`) ) DEFAULT CHARSET=utf8; +DROP TABLE IF EXISTS `%TABLE_PREFIX%thread`; +CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%thread` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `object_id` int(11) unsigned NOT NULL, + `object_type` char(1) NOT NULL, + `extra` text, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `object_id` (`object_id`), + KEY `object_type` (`object_type`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_entry`; +CREATE TABLE `%TABLE_PREFIX%thread_entry` ( + `id` int(11) unsigned NOT NULL auto_increment, + `pid` int(11) unsigned NOT NULL default '0', + `thread_id` int(11) unsigned NOT NULL default '0', + `staff_id` int(11) unsigned NOT NULL default '0', + `user_id` int(11) unsigned not null default 0, + `type` char(1) NOT NULL default '', + `poster` varchar(128) NOT NULL default '', + `source` varchar(32) NOT NULL default '', + `title` varchar(255), + `body` text NOT NULL, + `format` varchar(16) NOT NULL default 'html', + `ip_address` varchar(64) NOT NULL default '', + `created` datetime NOT NULL, + `updated` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `pid` (`pid`), + KEY `thread_id` (`thread_id`), + KEY `staff_id` (`staff_id`), + KEY `type` (`type`) +) DEFAULT CHARSET=utf8; + + +DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_entry_attachment`; +CREATE TABLE `%TABLE_PREFIX%thread_entry_attachment` ( + `id` int(11) unsigned NOT NULL auto_increment, + `file_id` int(10) unsigned NOT NULL default '0', + `thread_entry_id` int(11) unsigned NOT NULL default '0', + `inline` tinyint(1) NOT NULL default '0', + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `file_id` (`file_id`), + KEY `ref_id` (`thread_entry_id`) +) DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_entry_email`; +CREATE TABLE `%TABLE_PREFIX%thread_entry_email` ( + `id` int(11) unsigned NOT NULL auto_increment, + `thread_entry_id` int(11) unsigned NOT NULL, + `mid` varchar(255) NOT NULL, + `headers` text, + PRIMARY KEY (`id`), + KEY `thread_entry_id` (`thread_entry_id`), + KEY `mid` (`mid`) +) DEFAULT CHARSET=utf8; + DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket`; CREATE TABLE `%TABLE_PREFIX%ticket` ( `ticket_id` int(11) unsigned NOT NULL auto_increment, @@ -649,20 +708,6 @@ CREATE TABLE `%TABLE_PREFIX%ticket` ( KEY `sla_id` (`sla_id`) ) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_attachment`; -CREATE TABLE `%TABLE_PREFIX%ticket_attachment` ( - `attach_id` int(11) unsigned NOT NULL auto_increment, - `ticket_id` int(11) unsigned NOT NULL default '0', - `file_id` int(10) unsigned NOT NULL default '0', - `ref_id` int(11) unsigned NOT NULL default '0', - `inline` tinyint(1) NOT NULL default '0', - `created` datetime NOT NULL, - PRIMARY KEY (`attach_id`), - KEY `ticket_id` (`ticket_id`), - KEY `ref_id` (`ref_id`), - KEY `file_id` (`file_id`) -) DEFAULT CHARSET=utf8; - DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_lock`; CREATE TABLE `%TABLE_PREFIX%ticket_lock` ( `lock_id` int(11) unsigned NOT NULL auto_increment, @@ -675,16 +720,6 @@ CREATE TABLE `%TABLE_PREFIX%ticket_lock` ( KEY `staff_id` (`staff_id`) ) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_email_info`; -CREATE TABLE `%TABLE_PREFIX%ticket_email_info` ( - `id` int(11) unsigned NOT NULL auto_increment, - `thread_id` int(11) unsigned NOT NULL, - `email_mid` varchar(255) NOT NULL, - `headers` text, - PRIMARY KEY (`id`), - KEY `email_mid` (`email_mid`) -) DEFAULT CHARSET=utf8; - DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_event`; CREATE TABLE `%TABLE_PREFIX%ticket_event` ( `ticket_id` int(11) unsigned NOT NULL default '0', @@ -731,28 +766,6 @@ CREATE TABLE `%TABLE_PREFIX%ticket_priority` ( KEY `ispublic` (`ispublic`) ) DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_thread`; -CREATE TABLE `%TABLE_PREFIX%ticket_thread` ( - `id` int(11) unsigned NOT NULL auto_increment, - `pid` int(11) unsigned NOT NULL default '0', - `ticket_id` int(11) unsigned NOT NULL default '0', - `staff_id` int(11) unsigned NOT NULL default '0', - `user_id` int(11) unsigned not null default 0, - `thread_type` enum('M','R','N') NOT NULL, - `poster` varchar(128) NOT NULL default '', - `source` varchar(32) NOT NULL default '', - `title` varchar(255), - `body` mediumtext NOT NULL, - `format` varchar(16) NOT NULL default 'html', - `ip_address` varchar(64) NOT NULL default '', - `created` datetime NOT NULL, - `updated` datetime NOT NULL, - PRIMARY KEY (`id`), - KEY `ticket_id` (`ticket_id`), - KEY `staff_id` (`staff_id`), - KEY `pid` (`pid`) -) DEFAULT CHARSET=utf8; - CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `isactive` tinyint(1) NOT NULL DEFAULT '1',