Skip to content
Snippets Groups Projects
class.topic.php 17.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jared Hancock's avatar
    Jared Hancock committed
    <?php
    /*********************************************************************
        class.topic.php
    
        Help topic helper
    
        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:
    **********************************************************************/
    
    
    require_once INCLUDE_DIR . 'class.sequence.php';
    
    Jared Hancock's avatar
    Jared Hancock committed
    require_once INCLUDE_DIR . 'class.filter.php';
    
    class Topic extends VerySimpleModel
    implements TemplateVariable {
    
    
        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',
                    ),
                ),
    
    Peter Rotich's avatar
    Peter Rotich committed
                'priority' => array(
                    'null' => true,
                    'constraint' => array(
                        'priority_id' => 'Priority.priority_id',
                    ),
                ),
    
                'forms' => array(
                    'reverse' => 'TopicFormModel.topic',
                    'null' => true,
                ),
    
        const DISPLAY_DISABLED = 2;
    
    
        const FORM_USE_PARENT = 4294967295;
    
    
        const FLAG_CUSTOM_NUMBERS = 0x0001;
    
    
        const SORT_ALPHA = 'a';
        const SORT_MANUAL = 'm';
    
    
        function asVar() {
            return $this->getName();
        }
    
        static function getVarScope() {
            return array(
                'dept' => array(
    
                    'class' => 'Dept', 'desc' => __('Department'),
    
                'fullname' => __('Help topic full path'),
                'name' => __('Help topic'),
    
                'parent' => array(
    
                    'class' => 'Topic', 'desc' => __('Parent'),
    
                    'class' => 'SLA', 'desc' => __('Service Level Agreement'),
    
        function getId() {
    
            return $this->topic_id;
    
    
        function getPid() {
    
            return $this->topic_pid;
    
        }
    
        function getParent() {
            return $this->parent;
        }
    
        function getName() {
    
            return $this->topic;
        }
    
        function getLocalName() {
            return $this->getLocal('name');
    
        function getFullName() {
    
            return self::getTopicName($this->getId()) ?: $this->topic;
    
        }
    
        static function getTopicName($id) {
    
            $names = static::getHelpTopics(false, true);
    
        function getDeptId() {
    
            return $this->dept_id;
    
        function getSLAId() {
    
            return $this->sla_id;
    
        function getPriorityId() {
    
            return $this->priority_id;
    
        function getStatusId() {
    
            return $this->status_id;
    
        function getStaffId() {
    
            return $this->staff_id;
    
        function getTeamId() {
    
            return $this->team_id;
    
            return $this->page_id;
    
        function getForms() {
            if (!isset($this->_forms)) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                $this->_forms = array();
    
                foreach ($this->forms->select_related('form') as $F) {
                    $extra = JsonDataParser::decode($F->extra) ?: array();
                    $F->form->disableFields($extra['disable'] ?: array());
                    $this->_forms[] = $F->form;
                }
            }
            return $this->_forms;
    
    Jared Hancock's avatar
    Jared Hancock committed
        function autoRespond() {
    
            return !$this->noautoresp;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function isEnabled() {
    
            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 (!$this->isactive)
    
            if (!isset($chain[$this->getId()]) && ($p = $this->getParent())) {
                $chain[$this->getId()] = true;
                return $p->isActive($chain);
            }
            else {
    
                return $this->isactive;
    
        function isPublic() {
    
            return ($this->ispublic);
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        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];
        }
    
    
        function setSortOrder($i) {
    
            if ($i != $this->sort) {
                $this->sort = $i;
                return $this->save();
    
        function delete() {
    
            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();
    
    Jared Hancock's avatar
    Jared Hancock committed
                db_query('UPDATE '.TICKET_TABLE.' SET topic_id=0 WHERE topic_id='.db_input($this->getId()));
            }
    
    
        function __toString() {
            return $this->getFullName();
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        /*** Static functions ***/
    
    
        static function create($vars=array()) {
            $topic = parent::create($vars);
            $topic->created = SqlFunction::NOW();
            return $topic;
        }
    
        static function __create($vars, &$errors) {
    
    Peter Rotich's avatar
    Peter Rotich committed
            $topic = self::create($vars);
            if (!isset($vars['dept_id']))
                $vars['dept_id'] = 0;
    
            $vars['id'] = $vars['topic_id'];
    
            $topic->update($vars, $errors);
    
        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);
    
                    $parent = false;
    
                    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;
    
                        $parent = $info;
    
            // 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'])
    
                    $n .= " - ".__("(disabled)");
    
                $requested_names[$id] = $n;
    
            // 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.
    
            if ($localize && $cfg->getTopicSortMode() == self::SORT_ALPHA)
    
                return Internationalization::sortKeyedList($requested_names);
    
            return $requested_names;
    
        static function getPublicHelpTopics() {
    
            return self::getHelpTopics(true);
        }
    
    
        static function getAllHelpTopics($localize=false) {
    
            return self::getHelpTopics(false, true, $localize);
    
    Jared Hancock's avatar
    Jared Hancock committed
        static function getLocalNameById($id) {
            $topics = static::getHelpTopics(false, true);
            return $topics[$id];
        }
    
    
        static function getIdByName($name, $pid=0) {
            $list = self::objects()->filter(array(
                'topic'=>$name,
                'topic_pid'=>$pid,
    
            ))->values_flat('topic_id')->first();
    
            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');
    
            if (!$vars['topic'])
    
                $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->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']);
    
    Jared Hancock's avatar
    Jared Hancock committed
    
            //Auto assign ID is overloaded...
    
            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;
            }
    
            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 ($rv) {
                if (!$cfg || $cfg->getTopicSortMode() == 'a') {
                    static::updateSortOrder();
                }
                $this->updateForms($vars, $errors);
    
        function updateForms($vars, &$errors) {
            $find_disabled = function($form) use ($vars) {
                $fields = $vars['fields'];
                $disabled = array();
                foreach ($form->fields->values_flat('id') as $row) {
                    list($id) = $row;
                    if (false === ($idx = array_search($id, $fields))) {
                        $disabled[] = $id;
                    }
                }
                return $disabled;
            };
    
            // Consider all the forms in the request
    
            $current = array();
    
            if (is_array($form_ids = $vars['forms'])) {
                $forms = TopicFormModel::objects()
                    ->select_related('form')
                    ->filter(array('topic_id' => $this->getId()));
                foreach ($forms as $F) {
                    if (false !== ($idx = array_search($F->form_id, $form_ids))) {
    
                        $current[] = $F->form_id;
    
                        $F->sort = $idx + 1;
                        $F->extra = JsonDataEncoder::encode(
                            array('disable' => $find_disabled($F->form))
                        );
                        $F->save();
                        unset($form_ids[$idx]);
                    }
                    elseif ($F->form->get('type') != 'T') {
                        $F->delete();
                    }
                }
                foreach ($form_ids as $sort=>$id) {
                    if (!($form = DynamicForm::lookup($id))) {
                        continue;
                    }
    
                    elseif (in_array($id, $current)) {
                        // Don't add a form more than once
                        continue;
                    }
    
                    TopicFormModel::create(array(
                        'topic_id' => $this->getId(),
                        'form_id' => $id,
                        'sort' => $sort + 1,
                        'extra' => JsonDataEncoder::encode(
                            array('disable' => $find_disabled($form))
                        )
                    ))->save();
                }
            }
            return true;
        }
    
    
        function save($refetch=false) {
            if ($this->dirty)
                $this->updated = SqlFunction::NOW();
            return parent::save($refetch || $this->dirty);
        }
    
    
        static function updateSortOrder() {
    
            if (!($names = static::getHelpTopics(false, true, false)))
    
            $names = Internationalization::sortKeyedList($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);
    
    
    class TopicFormModel extends VerySimpleModel {
        static $meta = array(
            'table' => TOPIC_FORM_TABLE,
            'pk' => array('id'),
            'ordering' => array('sort'),
            'joins' => array(
                'topic' => array(
                    'constraint' => array('topic_id' => 'Topic.topic_id'),
                ),
                'form' => array(
                    'constraint' => array('form_id' => 'DynamicForm.id'),
                ),
            ),
        );
    }