Skip to content
Snippets Groups Projects
class.list.php 25.3 KiB
Newer Older
<?php
/*********************************************************************
    class.list.php

    Custom List utils

    Jared Hancock <jared@osticket.com>
    Peter Rotich <peter@osticket.com>
    Copyright (c)  2014 osTicket
    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.dynamic_forms.php');

/**
 * Interface for Custom Lists
 *
 * Custom lists are used to represent list of arbitrary data that can be
 * used as dropdown or typeahead selections in dynamic forms. This model
 * defines a list. The individual items are stored in the "Item" model.
 *
 */

interface CustomList {

    function getId();
    function getName();
    function getPluralName();

    function getNumItems();
    function getAllItems();
    function getItems($criteria);

    function getItem($id);
    function addItem($vars, &$errors);

    function getForm(); // Config form
    function hasProperties();

    function getSortModes();
    function getSortMode();
    function getListOrderBy();

    function allowAdd();
    function hasAbbrev();

    function update($vars, &$errors);
    function delete();

    static function create($vars, &$errors);
    static function lookup($id);
}

/*
 * Custom list item interface
 */
interface CustomListItem {
    function getId();
    function getValue();
    function getAbbrev();
    function getSortOrder();

    function getConfiguration();
    function getConfigurationForm();


    function isEnabled();
    function isDeletable();
    function isEnableable();
    function isInternal();

    function enable();
    function disable();

    function update($vars, &$errors);
    function delete();
 * Base class for Custom List handlers
 * Custom list handler extends custom list and might store data outside the
 * typical dynamic list store.
    function __construct($list) {
        $this->_list = $list;
        $rv = null;
        if ($this->_list && is_callable(array($this->_list, $name)))
            $rv = $args
                ? call_user_func_array(array($this->_list, $name), $args)
                : call_user_func(array($this->_list, $name));
    function update($vars, &$errors) {
        return $this->_list->update($vars, $errors);
    abstract function getNumItems();
    abstract function getAllItems();
    abstract function getItems($criteria);
    abstract function getItem($id);
    abstract function addItem($vars, &$errors);
}

/**
 * Dynamic lists are Custom Lists solely defined by the user.
 *
 */
class DynamicList extends VerySimpleModel implements CustomList {

    static $meta = array(
        'table' => LIST_TABLE,
        'ordering' => array('name'),
        'pk' => array('id'),
    );

    static $sort_modes = array(
            'Alpha'     => 'Alphabetical',
            '-Alpha'    => 'Alphabetical (Reversed)',
            'SortCol'   => 'Manually Sorted'
            );

    // Required fields
    static $fields = array('name', 'name_plural', 'sort_mode', 'notes');

    // Supported masks
    const MASK_EDIT     = 0x0001;
    const MASK_ADD      = 0x0002;
    const MASK_DELETE   = 0x0004;
    const MASK_ABBREV   = 0x0008;

    var $_items;
    var $_form;
    function __construct() {
        call_user_func_array(array('parent', '__construct'), func_get_args());
        $this->_config = new Config('list.'.$this->getId());
    function getId() {
        return $this->get('id');
    }

    function getInfo() {
        return $this->ht;
    }

    function hasProperties() {
        return ($this->getForm() && $this->getForm()->getFields());
    }

    function getSortModes() {
       return static::$sort_modes;
    }

    function getSortMode() {
        return $this->sort_mode;
    }

    function getListOrderBy() {
        switch ($this->getSortMode()) {
            case 'Alpha':   return 'value';
            case '-Alpha':  return '-value';
            case 'SortCol': return 'sort';
        }
    }

    function getName() {
        return $this->get('name');
    }

    function getPluralName() {
        if ($name = $this->get('name_plural'))
            return $name;
        else
            return $this->get('name') . 's';
    }

    function getItemCount() {
        return DynamicListItem::objects()->filter(array('list_id'=>$this->id))
            ->count();
    }

    function getNumItems() {
        return $this->getItemCount();
    }

    function getAllItems() {
         return DynamicListItem::objects()->filter(
                array('list_id'=>$this->get('id')))
                ->order_by($this->getListOrderBy());
    }

    function getItems($limit=false, $offset=false) {
        if (!$this->_items) {
            $this->_items = DynamicListItem::objects()->filter(
                array('list_id'=>$this->get('id'),
                      'status__hasbit'=>DynamicListItem::ENABLED))
                ->order_by($this->getListOrderBy());
            if ($limit)
                $this->_items->limit($limit);
            if ($offset)
                $this->_items->offset($offset);
        }
        return $this->_items;
    }

    function getItem($val) {

        $criteria = array('list_id' => $this->getId());
        if (is_int($val))
            $criteria['id'] = $val;
        else
            $criteria['value'] = $val;

         return DynamicListItem::lookup($criteria);
    }

    function addItem($vars, &$errors) {

        $item = DynamicListItem::create(array(
Peter Rotich's avatar
Peter Rotich committed
            'status' => 1,
            'list_id' => $this->getId(),
            'sort'  => $vars['sort'],
            'value' => $vars['value'],
            'extra' => $vars['abbrev']
        ));

        $item->save();

        $this->_items = false;

        return $item;
    }

    function getConfigurationForm($autocreate=false) {
        if (!$this->_form) {
            $this->_form = DynamicForm::lookup(array('type'=>'L'.$this->getId()));
            if (!$this->_form
                    && $autocreate
                    && $this->createConfigurationForm())
                return $this->getConfigurationForm(false);
        return $this->_form;
    }

    function isDeleteable() {
        return !$this->hasMask(static::MASK_DELETE);
    }

    function isEditable() {
        return !$this->hasMask(static::MASK_EDIT);
    }

    function allowAdd() {
        return !$this->hasMask(static::MASK_ADD);
    }

    function hasAbbrev() {
        return !$this->hasMask(static::MASK_ABBREV);
    }

    protected function hasMask($mask) {
        return 0 !== ($this->get('masks') & $mask);
    }

    protected function clearMask($mask) {
        return $this->set('masks', $this->get('masks') & ~$mask);
    }

    protected function setFlag($mask) {
        return $this->set('mask', $this->get('mask') | $mask);
    }

    private function createConfigurationForm() {

        $form = DynamicForm::create(array(
                    'type' => 'L'.$this->getId(),
                    'title' => $this->getName() . ' Properties'
        ));

        return $form->save(true);
    }

    function getForm($autocreate=true) {
        return $this->getConfigurationForm($autocreate);
    function getConfiguration() {
        return JsonDataParser::parse($this->_config->get('configuration'));
    }

    function update($vars, &$errors) {

        $required = array();
        if ($this->isEditable())
            $required = array('name');

        foreach (static::$fields as $f) {
            if (in_array($f, $required) && !$vars[$f])
                $errors[$f] = sprintf('%s is required', mb_convert_case($f, MB_CASE_TITLE));
            elseif (isset($vars[$f]))
                $this->set($f, $vars[$f]);
        }

        if ($errors)
            return false;

        return $this->save(true);
    }

    function save($refetch=false) {
        if (count($this->dirty))
            $this->set('updated', new SqlFunction('NOW'));
        if (isset($this->dirty['notes']))
            $this->notes = Format::sanitize($this->notes);
        return parent::save($refetch);
    }

    function delete() {
        $fields = DynamicFormField::objects()->filter(array(
            'type'=>'list-'.$this->id))->count();
        if ($fields == 0)
            return parent::delete();
        else
            // Refuse to delete lists that are in use by fields
            return false;
    }

    private function createForm() {

        $form = DynamicForm::create(array(
                    'type' => 'L'.$this->getId(),
                    'title' => $this->getName() . ' Properties'
        ));

        return $form->save(true);
    }

    static function add($vars, &$errors) {

        $required = array('name');
        $ht = array();
        foreach (static::$fields as $f) {
            if (in_array($f, $required) && !$vars[$f])
                $errors[$f] = sprintf('%s is required', mb_convert_case($f, MB_CASE_TITLE));
            elseif(isset($vars[$f]))
                $ht[$f] = $vars[$f];
        }

        if (!$ht || $errors)
            return false;

        // Create the list && form
        if (!($list = self::create($ht))
                || !$list->save(true)
                || !$list->createConfigurationForm())
            return false;

        return $list;
    }

    static function create($ht=false, &$errors=array()) {
        $inst = parent::create($ht);
        $inst->set('created', new SqlFunction('NOW'));

        if (isset($ht['properties'])) {
            $inst->save();
            $ht['properties']['type'] = 'L'.$inst->getId();
            $form = DynamicForm::create($ht['properties']);
            $form->save();
        }

        if (isset($ht['configuration'])) {
            $inst->save();
            $c = new Config('list.'.$inst->getId());
            $c->set('configuration', JsonDataEncoder::encode($ht['configuration']));
        }

        if (isset($ht['items'])) {
            $inst->save();
            foreach ($ht['items'] as $i) {
                $i['list_id'] = $inst->getId();
                $item = DynamicListItem::create($i);
                $item->save();
            }
        }

    static function lookup($id) {

        if (!($list = parent::lookup($id)))
            return null;

        if (($config = $list->getConfiguration())) {
            if (($lh=$config['handler']) && class_exists($lh))
                $list = new $lh($list);
        }

        return $list;
    }

    static function getSelections() {
        $selections = array();
        foreach (DynamicList::objects() as $list) {
            $selections['list-'.$list->id] =
                array($list->getPluralName(),
                    SelectionField, $list->get('id'));
        }
        return $selections;
    }

}
FormField::addFieldTypes('Custom Lists', array('DynamicList', 'getSelections'));

/**
 * Represents a single item in a dynamic list
 *
 * Fields:
 * value - (char * 255) Actual list item content
 * extra - (char * 255) Other values that represent the same item in the
 *      list, such as an abbreviation. In practice, should be a
 *      space-separated list of tokens which should hit this list item in a
 *      search
 * sort - (int) If sorting by this field, represents the numeric sort order
 *      that this item should come in the dropdown list
 */
class DynamicListItem extends VerySimpleModel implements CustomListItem {

    static $meta = array(
        'table' => LIST_ITEM_TABLE,
        'pk' => array('id'),
        'joins' => array(
            'list' => array(
                'null' => true,
                'constraint' => array('list_id' => 'DynamicList.id'),
            ),
        ),
    );

    var $_config;
    var $_form;

    const ENABLED   = 0x0001;
    const INTERNAL  = 0x0002;

    protected function hasStatus($flag) {
        return 0 !== ($this->get('status') & $flag);
    }

    protected function clearStatus($flag) {
        return $this->set('status', $this->get('status') & ~$flag);
    }

    protected function setStatus($flag) {
        return $this->set('status', $this->get('status') | $flag);
    }

    function isInternal() {
        return  $this->hasStatus(self::INTERNAL);
    }

    function isEnableable() {
        return true;
    }

    function isDeletable() {
        return !$this->isInternal();
    }

    function isEnabled() {
        return $this->hasStatus(self::ENABLED);
    }

    function enable() {
        $this->setStatus(self::ENABLED);
    }
    function disable() {
        $this->clearStatus(self::ENABLED);
    }

    function getId() {
        return $this->get('id');
    }

    function getListId() {
        return $this->get('list_id');
    }

    function getValue() {
        return $this->get('value');
    }

    function getAbbrev() {
        return $this->get('extra');
    }

    function getSortOrder() {
        return $this->get('sort');
    }

    function getConfiguration() {
        if (!$this->_config) {
            $this->_config = $this->get('properties');
            if (is_string($this->_config))
                $this->_config = JsonDataParser::parse($this->_config);
            elseif (!$this->_config)
                $this->_config = array();
        }
        return $this->_config;
    }

    function setConfiguration(&$errors=array()) {
        $config = array();
        foreach ($this->getConfigurationForm()->getFields() as $field) {
            $val = $field->to_database($field->getClean());
            $config[$field->get('id')] = is_array($val) ? $val[1] : $val;
            $errors = array_merge($errors, $field->errors());
        }
        if (count($errors) === 0)
            $this->set('properties', JsonDataEncoder::encode($config));

        return count($errors) === 0;
    }

    function getConfigurationForm() {
        if (!$this->_form) {
            $this->_form = DynamicForm::lookup(
                array('type'=>'L'.$this->get('list_id')));
        }
        return $this->_form;
    }

    function getForm() {
        return $this->getConfigurationForm();
    }

    function getVar($name) {
        $config = $this->getConfiguration();
        $name = mb_strtolower($name);
        foreach ($this->getConfigurationForm()->getFields() as $field) {
            if (mb_strtolower($field->get('name')) == $name)
                return $config[$field->get('id')];
        }
    }

    function toString() {
        return $this->get('value');
    }

    function __toString() {
        return $this->toString();
    }

    function update($vars, &$errors=array()) {

        if (!$vars['value']) {
            $errors['value-'.$this->getId()] = 'Value required';
            return false;
        }

        foreach (array(
                    'sort' => 'sort',
                    'value' => 'value',
                    'abbrev' => 'extra') as $k => $v) {
            if (isset($vars[$k]))
                $this->set($v, $vars[$k]);
        }

        return $this->save();
    }

    function delete() {
        # Don't really delete, just unset the list_id to un-associate it with
        # the list
        $this->set('list_id', null);
        return $this->save();
    }

    static function create($ht=false, &$errors=array()) {

        if (isset($ht['properties']) && is_array($ht['properties']))
            $ht['properties'] = JsonDataEncoder::encode($ht['properties']);

        $inst = parent::create($ht);
        $inst->save(true);

        // Auto-config properties if any
        if ($ht['configuration'] && is_array($ht['configuration'])) {
            $config = $inst->getConfiguration();
            if (($form = $inst->getConfigurationForm())) {
                foreach ($form->getFields() as $f) {
                    if (!isset($ht['configuration'][$f->get('name')]))
                        continue;

                    if (is_array($ht['configuration'][$f->get('name')]))
                        $val = JsonDataEncoder::encode(
                                $ht['configuration'][$f->get('name')]);
                    else
                        $val = $ht['configuration'][$f->get('name')];

                    $config[$f->get('id')] = $val;
                }
            }

            $inst->set('properties', JsonDataEncoder::encode($config));
        }

        return $inst;
    }
Peter Rotich's avatar
Peter Rotich committed

/*
 * Ticket status List
 *
 */

class TicketStatusList extends CustomListHandler {
Peter Rotich's avatar
Peter Rotich committed
    // Fields of interest we need to store
    static $config_fields = array('sort_mode', 'notes');

    var $_items;
    var $_form;

Peter Rotich's avatar
Peter Rotich committed
    function getNumItems() {
Peter Rotich's avatar
Peter Rotich committed
        return TicketStatus::objects()->count();
Peter Rotich's avatar
Peter Rotich committed
    }

    function getAllItems() {
Peter Rotich's avatar
Peter Rotich committed
         return TicketStatus::objects()->order_by($this->getListOrderBy());
Peter Rotich's avatar
Peter Rotich committed
    }

    function getItems($criteria) {

Peter Rotich's avatar
Peter Rotich committed
        if (!$this->_items) {
            $this->_items = TicketStatus::objects()->filter(
                array('flags__hasbit' => TicketStatus::ENABLED))
                ->order_by($this->getListOrderBy());
            if ($criteria['limit'])
                $this->_items->limit($criteria['limit']);
            if ($criteria['offset'])
                $this->_items->offset($criteria['offset']);
        }

        return $this->_items;
    function getItem($val) {

        if (!is_int($val))
            $val = array('name' => $val);

         return TicketStatus::lookup($val, $this);
Peter Rotich's avatar
Peter Rotich committed
    }

    function addItem($vars, &$errors) {
Peter Rotich's avatar
Peter Rotich committed
        $item = TicketStatus::create(array(
Peter Rotich's avatar
Peter Rotich committed
            'sort'  => $vars['sort'],
            'name' => $vars['value'],
        ));
        $item->save();

        $this->_items = false;

        return $item;
    }

    static function __load() {
        require_once(INCLUDE_DIR.'class.i18n.php');

        $i18n = new Internationalization();
Peter Rotich's avatar
Peter Rotich committed
        $tpl = $i18n->getTemplate('list.yaml');
Peter Rotich's avatar
Peter Rotich committed
        foreach ($tpl->getData() as $f) {
Peter Rotich's avatar
Peter Rotich committed
            if ($f['type'] == 'ticket-status') {
                $list = DynamicList::create($f);
                $list->save();
Peter Rotich's avatar
Peter Rotich committed
        if (!$list || !($o=DynamicForm::objects()->filter(
                        array('type'=>'L'.$list->getId()))))
Peter Rotich's avatar
Peter Rotich committed
            return false;

        // Create default statuses
        if (($statuses = $i18n->getTemplate('ticket_status.yaml')->getData()))
            foreach ($statuses as $status)
                TicketStatus::__create($status);

        return $o[0];
    }
Peter Rotich's avatar
Peter Rotich committed
}
Peter Rotich's avatar
Peter Rotich committed

class TicketStatus  extends VerySimpleModel implements CustomListItem {

    static $meta = array(
        'table' => TICKET_STATUS_TABLE,
        'ordering' => array('name'),
        'pk' => array('id'),
    );

Peter Rotich's avatar
Peter Rotich committed
    var $_form;
    var $_config;
    var $_settings;

    const ENABLED   = 0x0001;
    const INTERNAL  = 0x0002; // Forbid deletion or name and status change.
Peter Rotich's avatar
Peter Rotich committed



    function __construct() {
        call_user_func_array(array('parent', '__construct'), func_get_args());
        $this->_config = new Config('TS.'.$this->getId());
    }

    protected function hasFlag($field, $flag) {
        return 0 !== ($this->get($field) & $flag);
    }

    protected function clearFlag($field, $flag) {
        return $this->set($field, $this->get($field) & ~$flag);
    }

    protected function setFlag($field, $flag) {
        return $this->set($field, $this->get($field) | $flag);
    }

    protected function hasProperties() {
        return ($this->_config->get('properties'));
    }

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

    function getConfigurationForm() {

        if (!$this->_form && $this->_list) {
            $this->_form = DynamicForm::lookup(
                    array('type'=>'L'.$this->_list->getId()));
Peter Rotich's avatar
Peter Rotich committed
        }

        return $this->_form;
    }

    function isEnabled() {
        return $this->hasFlag('mode', self::ENABLED);
    }

    function enable() {

        // Ticket status without properties cannot be enabled!
        if (!$this->isEnableable())
            return false;

        return $this->setFlag('mode', self::ENABLED);
    }

    function disable() {
        return (!$this->isInternal()
                && $this->clearFlag('mode', self::ENABLED));
    }

    function isEnableable() {
Peter Rotich's avatar
Peter Rotich committed
    }

    function isDeletable() {
        return !$this->isInternal();
    }

    function isInternal() {
        return ($this->hasFlag('mode', self::INTERNAL));
    }

    function getId() {
        return $this->get('id');
    }

    function getName() {
        return $this->get('name');
    }

    function getValue() {
        return $this->getName();
    }

    function getAbbrev() {
        return '';
    }

    function getSortOrder() {
        return $this->get('sort');
    }

    function getConfiguration() {

        if (!$this->_settings) {
             $this->_settings = $this->_config->get('properties');
             if (is_string($this->_settings))
                 $this->_settings = JsonDataParser::parse($this->_settings);
             elseif (!$this->_settings)
                 $this->_settings = array();

            if ($this->getConfigurationForm()) {
                foreach ($this->getConfigurationForm()->getFields() as $f)  {
                    $name = mb_strtolower($f->get('name'));
                    $id = $f->get('id');
                    switch($name) {
                        case 'flags':
                            foreach (TicketFlagField::$_flags as $k => $v)
Peter Rotich's avatar
Peter Rotich committed
                                if ($this->hasFlag('flags', $v['flag']))
                                    $this->_settings[$id][$k] = $v['name'];
Peter Rotich's avatar
Peter Rotich committed
                            break;
                        case 'state':
                            $this->_settings[$id][$this->get('state')] = $this->get('state');
Peter Rotich's avatar
Peter Rotich committed
                            break;
                        default:
                            if (!$this->_settings[$id] && $this->_settings[$name])
                                $this->_settings[$id] = $this->_settings[$name];
                    }
                }
            }
        }

        return $this->_settings;
    }

    function setConfiguration(&$errors=array()) {
        $properties = array();
        foreach ($this->getConfigurationForm()->getFields() as $f) {
            if ($this->isInternal() //Item is internal.
                    && !$f->isEditable())
                continue;
Peter Rotich's avatar
Peter Rotich committed
            $val = $f->getClean();
            $errors = array_merge($errors, $f->errors());
            if ($f->errors()) continue;
Peter Rotich's avatar
Peter Rotich committed
            $name = mb_strtolower($f->get('name'));
            switch ($name) {
                case 'flags':
                    if ($val && is_array($val)) {
                        $flags = 0;
                        foreach ($val as $k => $v) {
                            if (isset(TicketFlagField::$_flags[$k]))
                                $flags += TicketFlagField::$_flags[$k]['flag'];
Peter Rotich's avatar
Peter Rotich committed
                            elseif (!$f->errors())
                                $f->addError('Unknown or invalid flag', $name);
                        }
                        $this->set('flags', $flags);
                    } elseif ($val && !$f->errors()) {
                        $f->addError('Unknown or invalid flag format', $name);
                    }
                    break;
                case 'state':
                    if ($val && is_array($val))
                        $this->set('state', key($val));
Peter Rotich's avatar
Peter Rotich committed
                    else
                        $f->addError('Unknown or invalid state', $name);
                    break;
                default: //Custom properties the user might add.
Peter Rotich's avatar
Peter Rotich committed
                    $properties[$f->get('id')] = $f->to_php($val);
            // Add field specific validation errors (warnings)
Peter Rotich's avatar
Peter Rotich committed
            $errors = array_merge($errors, $f->errors());
        }

        if (count($errors) === 0) {
            $this->save(true);
            $this->setProperties($properties);
        }

        return count($errors) === 0;
    }

    function setProperties($properties) {
        if ($properties && is_array($properties))
            $properties = JsonDataEncoder::encode($properties);

        $this->_config->set('properties', $properties);
    }

    function update($vars, &$errors) {

        $fields = array('value' => 'name', 'sort' => 'sort');
        foreach($fields as $k => $v) {
            if (isset($vars[$k]))
                $this->set($v, $vars[$k]);
        }

        return $this->save(true);
    }

    function delete() {

        if (!$this->isDeletable())
            return false;

        // TODO: Delete and do house cleaning (move tickets..etc)

    }

    function toString() {
        return $this->getValue();
    }

    function __toString() {
        return $this->toString();
    }


    static function lookup($var, $list= false) {

        if (!($item = parent::lookup($var)))
            return null;

        $item->_list = $list;

        return $item;
    }


Peter Rotich's avatar
Peter Rotich committed
    static function __create($ht, &$error=false) {
        global $ost;

        $properties = JsonDataEncoder::encode($ht['properties']);
        unset($ht['properties']);
        $ht['created'] = new SqlFunction('NOW');
        if ($status = TicketStatus::create($ht)) {
            $status->save(true);
            $status->_config = new Config('TS.'.$status->getId());
            $status->_config->set('properties', $properties);
        }

        return $status;
    }
}