Newer
Older
<?php
/*********************************************************************
class.topic.php
Help topic helper
Peter Rotich <peter@osticket.com>
http://www.osticket.com
Released under the GNU General Public License WITHOUT ANY WARRANTY.
See LICENSE.TXT for details.
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
require_once INCLUDE_DIR . 'class.sequence.php';
class Topic extends VerySimpleModel {
static $meta = array(
'table' => TOPIC_TABLE,
'pk' => array('topic_id'),
'ordering' => array('topic'),
'joins' => array(
'parent' => array(
'list' => false,
'constraint' => array(
'topic_pid' => 'Topic.topic_id',
),
),
'faqs' => array(
'list' => true,
'reverse' => 'FaqTopic.topic'
),
'page' => array(
'null' => true,
'constraint' => array(
'page_id' => 'Page.id',
),
),
'dept' => array(
'null' => true,
'constraint' => array(
'dept_id' => 'Dept.id',
),
),
'priority' => array(
'null' => true,
'constraint' => array(
'priority_id' => 'Priority.priority_id',
),
),
const DISPLAY_DISABLED = 2;
const FORM_USE_PARENT = 4294967295;
const FLAG_CUSTOM_NUMBERS = 0x0001;
function asVar() {
return $this->getName();
}
}
function getParent() {
return $this->parent;
}
function getName() {
return $this->topic;
}
function getLocalName() {
return $this->getLocal('name');
function getFullName() {
return self::getTopicName($this->getId());
}
static function getTopicName($id) {
$names = static::getHelpTopics(false, true);
return $this->priority_id;
return $this->status_id;
function getPageId() {
}
function getPage() {
return $this->page;
}
$id = $this->getFormId();
if ($id == self::FORM_USE_PARENT && ($p = $this->getParent()))
$this->form = $p->getForm();
elseif ($id && !$this->form)
$this->form = DynamicForm::lookup($id);
return !$this->noautoresp;
return $this->isActive();
}
/**
* Determine if the help topic is currently enabled. The ancestry of
* this topic will be considered to see if any of the parents are
* disabled. If any are disabled, then this topic will be considered
* disabled.
*
* Parameters:
* $chain - array<id:bool> recusion chain used to detect loops. The
* chain should be maintained and passed to a parent's ::isActive()
* method. When consulting a parent, if the local topic ID is a key
* in the chain, then this topic has already been considered, and
* there is a loop in the ancestry
*/
function isActive(array $chain=array()) {
if (!isset($chain[$this->getId()]) && ($p = $this->getParent())) {
$chain[$this->getId()] = true;
return $p->isActive($chain);
}
else {
return ($this->ispublic);
}
function getHashtable() {
return $this->ht;
}
function getInfo() {
$base = $this->getHashtable();
$base['custom-numbers'] = $this->hasFlag(self::FLAG_CUSTOM_NUMBERS);
return $base;
}
function hasFlag($flag) {
return $this->flags & $flag != 0;
}
function getNewTicketNumber() {
global $cfg;
if (!$this->hasFlag(self::FLAG_CUSTOM_NUMBERS))
return $cfg->getNewTicketNumber();
if ($this->sequence_id)
$sequence = Sequence::lookup($this->sequence_id);
if (!$sequence)
$sequence = new RandomSequence();
return $sequence->next($this->number_format ?: '######',
array('Ticket', 'isTicketNumberUnique'));
function getTranslateTag($subtag) {
return _H(sprintf('topic.%s.%s', $subtag, $this->getId()));
}
function getLocal($subtag) {
$tag = $this->getTranslateTag($subtag);
$T = CustomDataTranslation::translate($tag);
return $T != $tag ? $T : $this->ht[$subtag];
}
if ($i != $this->sort) {
$this->sort = $i;
return $this->save();
}
// Noop
return true;
}
global $cfg;
if ($this->getId() == $cfg->getDefaultTopicId())
return false;
if (parent::delete()) {
self::objects()->filter(array(
'topic_pid' => $this->getId()
))->update(array(
'topic_pid' => 0
));
FaqTopic::objects()->filter(array(
'topic_id' => $this->getId()
))->delete();
db_query('UPDATE '.TICKET_TABLE.' SET topic_id=0 WHERE topic_id='.db_input($this->getId()));
}
function __toString() {
return $this->getFullName();
}
static function create($vars=array()) {
$topic = parent::create($vars);
$topic->created = SqlFunction::NOW();
return $topic;
}
static function __create($vars, &$errors) {
$topic = self::create();
static function getHelpTopics($publicOnly=false, $disabled=false, $localize=true) {
static $topics, $names = array();
// If localization is specifically requested, then rebuild the list.
if (!$names || $localize) {
$objects = self::objects()->values_flat(
'topic_id', 'topic_pid', 'ispublic', 'isactive', 'topic'
)
->order_by('sort');
// Fetch information for all topics, in declared sort order
$topics = array();
foreach ($objects as $T) {
list($id, $pid, $pub, $act, $topic) = $T;
$topics[$id] = array('pid'=>$pid, 'public'=>$pub,
'disabled'=>!$act, 'topic'=>$topic);
$localize_this = function($id, $default) use ($localize) {
if (!$localize)
return $default;
$tag = _H("topic.name.{$id}");
$T = CustomDataTranslation::translate($tag);
return $T != $tag ? $T : $default;
};
// Resolve parent names
foreach ($topics as $id=>$info) {
$name = $localize_this($id, $info['topic']);
$loop = array($id=>true);
while (($pid = $info['pid']) && ($info = $topics[$info['pid']])) {
$name = sprintf('%s / %s', $localize_this($pid, $info['topic']),
$name);
if ($parent && $parent['disabled'])
// Cascade disabled flag
$topics[$id]['disabled'] = true;
if (isset($loop[$info['pid']]))
break;
$loop[$info['pid']] = true;
}
$names[$id] = $name;
}
}
// Apply requested filters
$requested_names = array();
foreach ($names as $id=>$n) {
$info = $topics[$id];
if ($publicOnly && !$info['public'])
continue;
if (!$disabled && $info['disabled'])
continue;
if ($disabled === self::DISPLAY_DISABLED && $info['disabled'])
$requested_names[$id] = $n;
// XXX: If localization requested and the current locale is not the
// primary, the list may need to be sorted. Caching is ok here,
// because the locale is not going to be changed within a single
// request.
return $requested_names;
static function getPublicHelpTopics() {
return self::getHelpTopics(true);
}
static function getAllHelpTopics($localize=false) {
return self::getHelpTopics(false, true, $localize);
static function getIdByName($name, $pid=0) {
$list = self::objects()->filter(array(
'topic'=>$name,
'topic_pid'=>$pid,
if ($list)
return $list[0];
function update($vars, &$errors) {
$vars['topic'] = Format::striptags(trim($vars['topic']));
if (isset($this->topic_id) && $this->getId() != $vars['id'])
$errors['err']=__('Internal error occurred');
$errors['topic']=__('Help topic name is required');
elseif (strlen($vars['topic'])<5)
$errors['topic']=__('Topic is too short. Five characters minimum');
elseif (($tid=self::getIdByName($vars['topic'], $vars['topic_pid']))
&& (!isset($this->topic_id) || $tid!=$this->getId()))
$errors['topic']=__('Topic already exists');
if (!is_numeric($vars['dept_id']))
$errors['dept_id']=__('Department selection is required');
if ($vars['custom-numbers'] && !preg_match('`(?!<\\\)#`', $vars['number_format']))
$errors['number_format'] =
'Ticket number format requires at least one hash character (#)';
if ($errors)
return false;
$this->topic = $vars['topic'];
$this->topic_pid = $vars['topic_pid'] ?: 0;
$this->dept_id = $vars['dept_id'];
$this->priority_id = $vars['priority_id'] ?: 0;
$this->status_id = $vars['status_id'] ?: 0;
$this->sla_id = $vars['sla_id'] ?: 0;
$this->form_id = $vars['form_id'] ?: 0;
$this->page_id = $vars['page_id'] ?: 0;
$this->isactive = !!$vars['isactive'];
$this->ispublic = !!$vars['ispublic'];
$this->sequence_id = $vars['custom-numbers'] ? $vars['sequence_id'] : 0;
$this->number_format = $vars['custom-numbers'] ? $vars['number_format'] : '';
$this->flags = $vars['custom-numbers'] ? self::FLAG_CUSTOM_NUMBERS : 0;
$this->noautoresp = !!$vars['noautoresp'];
$this->notes = Format::sanitize($vars['notes']);
if ($vars['assign'] && $vars['assign'][0] == 's') {
$this->team_id = 0;
$this->staff_id = preg_replace("/[^0-9]/", "", $vars['assign']);
}
elseif ($vars['assign'] && $vars['assign'][0] == 't') {
$this->staff_id = 0;
$this->team_id = preg_replace("/[^0-9]/", "", $vars['assign']);
}
else {
$this->staff_id = 0;
$this->team_id = 0;
}
$rv = false;
if ($this->__new__) {
if (isset($this->topic_pid)
&& ($parent = Topic::lookup($this->topic_pid))) {
$this->sort = ($parent->sort ?: 0) + 1;
if (!($rv = $this->save())) {
$errors['err']=sprintf(__('Unable to create %s.'), __('this help topic'))
.' '.__('Internal error occurred');
}
}
elseif (!($rv = $this->save())) {
$errors['err']=sprintf(__('Unable to update %s.'), __('this help topic'))
.' '.__('Internal error occurred');
if (!$cfg || $cfg->getTopicSortMode() == 'a') {
static::updateSortOrder();
return $rv;
}
function save($refetch=false) {
if ($this->dirty)
$this->updated = SqlFunction::NOW();
return parent::save($refetch || $this->dirty);
}
static function updateSortOrder() {
// Fetch (un)sorted names
if (!($names = static::getHelpTopics(false, true, false)))
if ($cfg && function_exists('collator_create')) {
$coll = Collator::create($cfg->getPrimaryLanguage());
// UASORT is necessary to preserve the keys
uasort($names, function($a, $b) use ($coll) {
return $coll->compare($a, $b); });
}
else {
// Really only works on English names
asort($names);
}
$update = array_keys($names);
foreach ($update as $idx=>&$id) {
$id = sprintf("(%s,%s)", db_input($id), db_input($idx+1));
}
if (!count($update))
return;
// Thanks, http://stackoverflow.com/a/3466
$sql = sprintf('INSERT INTO `%s` (topic_id,`sort`) VALUES %s
ON DUPLICATE KEY UPDATE `sort`=VALUES(`sort`)',
TOPIC_TABLE, implode(',', $update));
db_query($sql);
// Add fields from the standard ticket form to the ticket filterable fields
Filter::addSupportedMatches(/* @trans */ 'Help Topic', array('topicId' => 'Topic ID'), 100);