Skip to content
Snippets Groups Projects
class.list.php 40.8 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');
require_once(INCLUDE_DIR .'class.variable.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 isItemUnique($vars);
    function getForm(); // Config form
    function hasProperties();
    function getConfigurationForm();
    function getSummaryFields();

    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();

Peter Rotich's avatar
Peter Rotich committed
    function getList();
    function getListId();

    function getConfiguration();

Peter Rotich's avatar
Peter Rotich committed
    function hasProperties();
    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 __get($field) {
        return $this->_list->{$field};
    }

    function update($vars, &$errors) {
        return $this->_list->update($vars, $errors);
    abstract function getListOrderBy();
    abstract function getNumItems();
    abstract function getAllItems();
    abstract function getItems($criteria);
    abstract function getItem($id);
    abstract function addItem($vars, &$errors);

    static protected $registry = array();
    static function forList(/* CustomList */ $list) {
        if ($list->type && ($handler = static::$registry[$list->type]))
            return new $handler($list);
        return $list;
    }
    static function register($type, $handler) {
        static::$registry[$type] = $handler;
    }
}

/**
 * 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'),
        'joins' => array(
            'items' => array(
                'reverse' => 'DynamicListItem.list',
            ),
        ),
    );

    // 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 getId() {
        return $this->get('id');
    }

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

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

    function getSortModes() {
        return array(
            'Alpha'     => __('Alphabetical'),
            '-Alpha'    => __('Alphabetical (Reversed)'),
            'SortCol'   => __('Manually Sorted')
        );
    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->getLocal('name');
    }

    function getPluralName() {
        if ($name = $this->getLocal('name_plural'))
            return $name;
        else
            return $this->getName() . '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 search($q) {
        $items = clone $this->getAllItems();
        return $items->filter(Q::any(array(
            'value__startswith' => $q,
            'extra__contains' => $q,
            'properties__contains' => '"'.$q,
        )));
    }

    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, $extra=false) {
        $items = DynamicListItem::objects()->filter(
                array('list_id' => $this->getId()));

        if (is_int($val))
            $items->filter(array('id' => $val));
        elseif ($extra)
            $items->filter(array('extra' => $val));
            $items->filter(array('value' => $val));

        return $items->first();
    }

    function addItem($vars, &$errors) {
        if (($item=$this->getItem($vars['value'])))
            return $item;

        $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['extra']
        ));
        $this->_items = false;

        return $item;
    }

    function isItemUnique($data) {
        try {
            $this->getItems()->filter(array('value'=>$data['value']))->one();
            return false;
        }
        catch (DoesNotExist $e) {
            return true;
        }
    }

    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 getListItemBasicForm($source=null, $item=false) {
        return new SimpleForm(array(
            'value' => new TextboxField(array(
                'required' => true,
                'label' => __('Value'),
                'configuration' => array(
                    'translatable' => $item ? $item->getTranslateTag('value') : false,
                    'size' => 60,
                    'length' => 0,
                    'autofocus' => true,
                ),
            )),
            'extra' => new TextboxField(array(
                'label' => __('Abbreviation'),
                'configuration' => array(
                    'size' => 60,
                    'length' => 0,
                ),
            )),
        ), $source);
    }

    // Fields shown on the list items page
    function getSummaryFields() {
        $prop_fields = array();
        foreach ($this->getConfigurationForm()->getFields() as $f) {
            if (in_array($f->get('type'), array('text', 'datetime', 'phone')))
                $prop_fields[] = $f;
            if (strpos($f->get('type'), 'list-') === 0)
                $prop_fields[] = $f;

            // 4 property columns max
            if (count($prop_fields) == 4)
                break;
        }
        return $prop_fields;
    }

    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);
        return JsonDataParser::parse($this->configuration);
    function getTranslateTag($subtag) {
        return _H(sprintf('list.%s.%s', $subtag, $this->id));
    }
    function getLocal($subtag) {
        $tag = $this->getTranslateTag($subtag);
        $T = CustomDataTranslation::translate($tag);
        return $T != $tag ? $T : $this->get($subtag);
    }

    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();

        // Refuse to delete lists that are in use by fields
        if ($fields != 0)
            return false;

        if (!parent::delete())
            return false;

        if (($form = $this->getForm(false))) {
            $form->delete(false);
            $form->fields->delete();
        }

        return true;
    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()) {
        if (isset($ht['configuration'])) {
            $ht['configuration'] = JsonDataEncoder::encode($ht['configuration']);
        }

        $inst = new static($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['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(),
Marco Borla's avatar
Marco Borla committed
                    'SelectionField', $list->get('id'));
        }
        return $selections;
    }

Peter Rotich's avatar
Peter Rotich committed
   function importCsv($stream, $defaults=array()) {
        require_once INCLUDE_DIR . 'class.import.php';

        $form = $this->getConfigurationForm();
Peter Rotich's avatar
Peter Rotich committed
        $fields = array(
            'value' => new TextboxField(array(
                'label' => __('Value'),
                'name' => 'value',
                'configuration' => array(
                    'length' => 0,
                ),
            )),
            'abbrev' => new TextboxField(array(
Peter Rotich's avatar
Peter Rotich committed
                'name' => 'extra',
                'label' => __('Abbreviation'),
                'configuration' => array(
                    'length' => 0,
                ),
            )),
        );

Peter Rotich's avatar
Peter Rotich committed
        $form = $this->getConfigurationForm();
        if ($form && ($custom_fields = $form->getFields())
                && count($custom_fields)) {
            foreach ($custom_fields as $f)
                if ($f->get('name'))
                    $fields[$f->get('name')] = $f;
Peter Rotich's avatar
Peter Rotich committed
        $importer = new CsvImporter($stream);
        $imported = 0;
        try {
            db_autocommit(false);
            $records = $importer->importCsv($fields, $defaults);
            foreach ($records as $data) {
                $errors = array();
                $item = $this->addItem($data, $errors);
                if ($item && $item->setConfiguration($data, $errors))
                    $imported++;
                else
                    echo sprintf(__('Unable to import item: %s'), print_r($data, true));
Peter Rotich's avatar
Peter Rotich committed
            db_autocommit(true);
Peter Rotich's avatar
Peter Rotich committed
        catch (Exception $ex) {
            db_rollback();
            return $ex->getMessage();
Peter Rotich's avatar
Peter Rotich committed
        return $imported;
    }

    function importFromPost($stuff, $extra=array()) {
        if (is_array($stuff) && !$stuff['error']) {
            // Properly detect Macintosh style line endings
            ini_set('auto_detect_line_endings', true);
            $stream = fopen($stuff['tmp_name'], 'r');
        }
        elseif ($stuff) {
            $stream = fopen('php://temp', 'w+');
            fwrite($stream, $stuff);
            rewind($stream);
        }
        else {
            return __('Unable to parse submitted items');
        }

        return self::importCsv($stream, $extra);
    }
FormField::addFieldTypes(/* @trans */ '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;
    }

Peter Rotich's avatar
Peter Rotich committed
    function isDisableable() {
        return !$this->isInternal();
    }

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

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

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

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

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

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

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

    function getValue() {
        return $this->getLocal('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($vars, &$errors=array()) {
        $config = array();
        foreach ($this->getConfigurationForm($vars)->getFields() as $field) {
            $config[$field->get('id')] = $field->to_php($field->getClean());
            $errors = array_merge($errors, $field->errors());
        }

        if ($errors)
            return false;

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

        return $this->save();
    function getConfigurationForm($source=null) {
        if (!$this->_form) {
            $config = $this->getConfiguration();
            $this->_form = $this->list->getForm()->getForm($source);
            if (!$source && $config) {
                $fields = $this->_form->getFields();
                foreach ($fields as $f) {
                    $name = $f->get('id');
                    if (isset($config[$name]))
                        $f->value = $f->to_php($config[$name]);
                    else if ($f->get('default'))
                        $f->value = $f->get('default');
                }
            }
        return $this->_form;
    }

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

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

    function getVar($name) {
        $config = $this->getConfiguration();
        $name = mb_strtolower($name);
        foreach ($this->getConfigurationForm()->getFields() as $field) {
            if (mb_strtolower($field->get('name')) == $name)
                return $field->asVar($config[$field->get('id')]);
    function getFilterData() {
        $data = array();
        foreach ($this->getConfigurationForm()->getFields() as $F) {
            $data['.'.$F->get('id')] = $F->toString($F->value);
        }
        $data['.abb'] = (string) $this->get('extra');
    function getTranslateTag($subtag) {
        return _H(sprintf('listitem.%s.%s', $subtag, $this->id));
    }
    function getLocal($subtag) {
        $tag = $this->getTranslateTag($subtag);
        $T = CustomDataTranslation::translate($tag);
        return $T != $tag ? $T : $this->get($subtag);
    }

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

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

Peter Rotich's avatar
Peter Rotich committed
    function display() {
        return sprintf('<a class="preview" href="#"
                data-preview="#list/%d/items/%d/preview">%s</a>',
                $this->getListId(),
                $this->getId(),
                $this->getValue()
                );
    }

    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 = new static($ht);

        // 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;

                    $config[$f->get('id')] =
                        $ht['configuration'][$f->get('name')];
                }
            }

            $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;

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

Peter Rotich's avatar
Peter Rotich committed
    function getNumItems() {
Peter Rotich's avatar
Peter Rotich committed
        return TicketStatus::objects()->count();
    function getAllItems() {
        if (!$this->_items)
            $this->_items = TicketStatus::objects()->order_by($this->getListOrderBy());
Peter Rotich's avatar
Peter Rotich committed

        return $this->_items;
    function search($q) {
        $items = clone $this->getAllItems();
        return $items->filter(Q::any(array(
            'name__startswith' => $q,
            'properties__contains' => '"'.$q,
        )));
    }

    function getItems($criteria = array()) {
        // Default to only enabled items
        if (!isset($criteria['enabled']))
            $criteria['enabled'] = true;
        $filters =  array();
        if ($criteria['enabled'])
            $filters['mode__hasbit'] = TicketStatus::ENABLED;
        if ($criteria['states'] && is_array($criteria['states']))
            $filters['state__in'] = $criteria['states'];
        else
            $filters['state__isnull'] = false;

        $items = TicketStatus::objects();
        if ($filters)
            $items->filter($filters);
        if ($criteria['limit'])
            $items->limit($criteria['limit']);
        if ($criteria['offset'])
            $items->offset($criteria['offset']);

        $items->order_by($this->getListOrderBy());

        return $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) {
        $item = TicketStatus::create(array(
Peter Rotich's avatar
Peter Rotich committed
            'sort'  => $vars['sort'],
            'name' => $vars['name'],
Peter Rotich's avatar
Peter Rotich committed
        ));
        $this->_items = false;

        return $item;
    }

    function isItemUnique($data) {
        try {
            $this->getItems()->filter(array('name'=>$data['name']))->one();
            return false;
        }
        catch (DoesNotExist $e) {
            return true;
        }
    }


    static function getStatuses($criteria=array()) {
Peter Rotich's avatar
Peter Rotich committed

        $statuses = array();
        if (($list = DynamicList::lookup(
                        array('type' => 'ticket-status'))))
            $statuses = $list->getItems($criteria);
Peter Rotich's avatar
Peter Rotich committed

        return $statuses;
    }

Peter Rotich's avatar
Peter Rotich committed
    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];
    }

    function getExtraConfigOptions($source=null) {
        $status_choices = array( 0 => __('System Default'));
        if (($statuses=TicketStatusList::getStatuses(
                        array( 'enabled' => true, 'states' =>
                            array('open')))))
            foreach ($statuses as $s)
                $status_choices[$s->getId()] = $s->getName();

        return array(
            'allowreopen' => new BooleanField(array(
                'label' =>__('Allow Reopen'),
Peter Rotich's avatar
Peter Rotich committed
                'editable' => true,
                'default' => isset($source['allowreopen'])
                    ?  $source['allowreopen']: true,
                'id' => 'allowreopen',
                'name' => 'allowreopen',
                'configuration' => array(
                    'desc'=>__('Allow tickets on this status to be reopened by end users'),
                ),