Skip to content
Snippets Groups Projects
class.list.php 37.9 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 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($source=null);


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

/**
 * 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 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['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);
        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 = 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['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;
    }

    function importCsv($stream, $defaults=array()) {
        //Read the header (if any)
        $headers = array('value' => __('Value'), 'abbrev' => __('Abbreviation'));
        $form = $this->getConfigurationForm();
        $named_fields = $fields = array(
            'value' => new TextboxField(array(
                'label' => __('Value'),
                'name' => 'value',
                'configuration' => array(
                    'length' => 0,
                ),
            )),
            'abbrev' => new TextboxField(array(
                'name' => 'abbrev',
                'label' => __('Abbreviation'),
                'configuration' => array(
                    'length' => 0,
                ),
            )),
        );
        $all_fields = $form->getFields();
        $has_header = false;
        foreach ($all_fields as $f)
            if ($f->get('name'))
                $named_fields[] = $f;

        if (!($data = fgetcsv($stream, 1000, ",")))
            return __('Whoops. Perhaps you meant to send some CSV records');

        foreach ($data as $D) {
            if (strcasecmp($D, 'value') === 0)
                $has_header = true;
        }
        if ($has_header) {
            foreach ($data as $h) {
                $found = false;
                foreach ($all_fields as $f) {
                    if (in_array(mb_strtolower($h), array(
                            mb_strtolower($f->get('name')), mb_strtolower($f->get('label'))))) {
                        $found = true;
                        if (!$f->get('name'))
                            return sprintf(__(
                                '%s: Field must have `variable` set to be imported'), $h);
                        $headers[$f->get('name')] = $f->get('label');
                        break;
                    }
                }
                if (!$found) {
                    $has_header = false;
                    if (count($data) == count($named_fields)) {
                        // Number of fields in the user form matches the number
                        // of fields in the data. Assume things line up
                        $headers = array();
                        foreach ($named_fields as $f)
                            $headers[$f->get('name')] = $f->get('label');
                        break;
                    }
                    else {
                        return sprintf(__('%s: Unable to map header to a property'), $h);
                    }
                }
            }
        }

        // 'value' MUST be in the headers
        if (!isset($headers['value']))
            return __('CSV file must include `value` column');

        if (!$has_header)
            fseek($stream, 0);

        $items = array();
        $keys = array('value', 'abbrev');
        foreach ($headers as $h => $label) {
            if (!($f = $form->getField($h)))
                continue;

            $name = $keys[] = $f->get('name');
            $fields[$name] = $f->getImpl();
        }

        // Add default fields (org_id, etc).
        foreach ($defaults as $key => $val) {
            // Don't apply defaults which are also being imported
            if (isset($header[$key]))
                unset($defaults[$key]);
            $keys[] = $key;
        }

        while (($data = fgetcsv($stream, 1000, ",")) !== false) {
            if (count($data) == 1 && $data[0] == null)
                // Skip empty rows
                continue;
            elseif (count($data) != count($headers))
                return sprintf(__('Bad data. Expected: %s'), implode(', ', $headers));
            // Validate according to field configuration
            $i = 0;
            foreach ($headers as $h => $label) {
                $f = $fields[$h];
                $T = $f->parse($data[$i]);
                if ($f->validateEntry($T) && $f->errors())
                    return sprintf(__(
                        /* 1 will be a field label, and 2 will be error messages */
                        '%1$s: Invalid data: %2$s'),
                        $label, implode(', ', $f->errors()));
                // Convert to database format
                $data[$i] = $f->to_database($T);
                $i++;
            }
            // Add default fields
            foreach ($defaults as $key => $val)
                $data[] = $val;

            $items[] = $data;
        }

        foreach ($items as $u) {
            $vars = array_combine($keys, $u);
            $errors = array();
            $item = $this->addItem($vars, $errors);
            if (!$item || !$item->setConfiguration($vars, $errors))
                return sprintf(__('Unable to import item: %s'),
                    print_r($vars, true));
        }

        return count($items);
    }

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

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

    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 = DynamicForm::lookup(
                array('type'=>'L'.$this->get('list_id')))->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();
    }

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

    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;

                    $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 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) {
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 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];
    }
Peter Rotich's avatar
Peter Rotich committed
}
class TicketStatus
extends VerySimpleModel
implements CustomListItem, TemplateVariable {
Peter Rotich's avatar
Peter Rotich committed

    static $meta = array(
        'table' => TICKET_STATUS_TABLE,
        'ordering' => array('name'),
        'pk' => array('id'),
        'joins' => array(
            'tickets' => array(
                'reverse' => 'TicketModel.status',
                )
        )
Peter Rotich's avatar
Peter Rotich committed
    var $_form;
    var $_settings;
    var $_properties;
    const ENABLED   = 0x0001;
    const INTERNAL  = 0x0002; // Forbid deletion or name and status change.
Peter Rotich's avatar
Peter Rotich committed

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

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