Skip to content
Snippets Groups Projects
class.forms.php 163 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jared Hancock's avatar
    Jared Hancock committed
    <?php
    /*********************************************************************
        class.forms.php
    
        osTicket forms framework
    
        Jared Hancock <jared@osticket.com>
        Copyright (c)  2006-2013 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:
    **********************************************************************/
    
    /**
     * Form template, used for designing the custom form and for entering custom
     * data for a ticket
     */
    class Form {
    
        static $renderer = 'GridFluidLayout';
    
        static $id = 0;
    
    
        var $options = array();
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $fields = array();
    
        var $title = '';
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $instructions = '';
    
    
        var $validators = array();
    
    
        var $_errors = null;
    
        var $_source = false;
    
    Peter Rotich's avatar
    Peter Rotich committed
        function __construct($source=null, $options=array()) {
    
    
            $this->options = $options;
    
            if (isset($options['title']))
                $this->title = $options['title'];
            if (isset($options['instructions']))
                $this->instructions = $options['instructions'];
    
            if (isset($options['id']))
                $this->id = $options['id'];
    
    
            // Use POST data if source was not specified
            $this->_source = ($source) ? $source : $_POST;
    
        function getFormId() {
    
            return @$this->id ?: static::$id;
        }
        function setId($id) {
            $this->id = $id;
    
        function data($source) {
            foreach ($this->fields as $name=>$f)
                if (isset($source[$name]))
                    $f->value = $source[$name];
        }
    
        function setFields($fields) {
    
    
            if (!is_array($fields) && !$fields instanceof Traversable)
    
                return;
    
            $this->fields = $fields;
            foreach ($fields as $k=>$f) {
                $f->setForm($this);
    
    Peter Rotich's avatar
    Peter Rotich committed
                if (!$f->get('name') && $k && !is_numeric($k))
    
                    $f->set('name', $k);
            }
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getFields() {
            return $this->fields;
        }
    
    
        function getField($name) {
    
            $fields = $this->getFields();
            foreach($fields as $f)
    
                if(!strcasecmp($f->get('name'), $name))
                    return $f;
    
            if (isset($fields[$name]))
                return $fields[$name];
    
        function hasField($name) {
            return $this->getField($name);
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getTitle() { return $this->title; }
        function getInstructions() { return $this->instructions; }
    
        function getSource() { return $this->_source; }
    
        function setSource($source) { $this->_source = $source; }
    
        /**
         * Validate the form and indicate if there no errors.
         *
         * Parameters:
         * $filter - (callback) function to receive each field and return
         *      boolean true if the field's errors are significant
         */
        function isValid($include=false) {
    
            if (!isset($this->_errors)) {
    
                $this->_errors = array();
    
                $this->validate($this->getClean());
    
                foreach ($this->getFields() as $field)
    
                    if ($field->errors() && (!$include || $include($field)))
    
                        $this->_errors[$field->get('id')] = $field->errors();
            }
            return !$this->_errors;
        }
    
    
        function validate($clean_data) {
            // Validate the whole form so that errors can be added to the
            // individual fields and collected below.
            foreach ($this->validators as $V) {
                $V($this);
            }
        }
    
    
        function getClean() {
            if (!$this->_clean) {
                $this->_clean = array();
    
                foreach ($this->getFields() as $key=>$field) {
    
                    if (!$field->hasData())
    
    Jared Hancock's avatar
    Jared Hancock committed
                        continue;
    
                    // Prefer indexing by field.id if indexing numerically
                    if (is_int($key) && $field->get('id'))
                        $key = $field->get('id');
    
                    $this->_clean[$key] = $this->_clean[$field->get('name')]
                        = $field->getClean();
                }
    
                unset($this->_clean[""]);
    
        function errors($formOnly=false) {
            return ($formOnly) ? $this->_errors['form'] : $this->_errors;
        }
    
    
        function addError($message, $index=false) {
    
            if ($index)
                $this->_errors[$index] = $message;
            else
                $this->_errors['form'][] = $message;
        }
    
        function addErrors($errors=array()) {
            foreach ($errors as $k => $v) {
                if (($f=$this->getField($k)))
                    $f->addError($v);
                else
                    $this->addError($v, $k);
            }
    
        }
    
        function addValidator($function) {
            if (!is_callable($function))
                throw new Exception('Form validator must be callable');
            $this->validators[] = $function;
    
        function render($staff=true, $title=false, $options=array()) {
    
            if ($title)
                $this->title = $title;
    
            if (isset($options['instructions']))
                $this->instructions = $options['instructions'];
    
            $template = $options['template'] ?: 'dynamic-form.tmpl.php';
    
                include(STAFFINC_DIR . 'templates/' . $template);
    
                include(CLIENTINC_DIR . 'templates/' . $template);
    
            echo $this->getMedia();
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getLayout($title=false, $options=array()) {
    
            $rc = @$options['renderer'] ?: static::$renderer;
            return new $rc($title, $options);
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function asTable($title=false, $options=array()) {
            return $this->getLayout($title, $options)->asTable($this);
    
            // XXX: Media can't go in a table
    
            echo $this->getMedia();
    
    
        function getMedia() {
            static $dedup = array();
    
            foreach ($this->getFields() as $f) {
    
                if (($M = $f->getMedia()) && is_array($M)) {
    
                    foreach ($M as $type=>$files) {
                        foreach ($files as $url) {
                            $key = strtolower($type.$url);
                            if (isset($dedup[$key]))
                                continue;
    
    
                            self::emitMedia($url, $type);
    
        function emitJavascript($options=array()) {
    
            // Check if we need to emit javascript
    
            if (!($fid=$this->getFormId()))
    
                return;
            ?>
            <script type="text/javascript">
              $(function() {
                <?php
                //XXX: We ONLY want to watch field on this form. We'll only
                // watch form inputs if form_id is specified. Current FORM API
                // doesn't generate the entire form  (just fields)
                if ($fid) {
                    ?>
                    $(document).off('change.<?php echo $fid; ?>');
                    $(document).on('change.<?php echo $fid; ?>',
                        'form#<?php echo $fid; ?> :input',
                        function() {
                            //Clear any current errors...
                            var errors = $('#field'+$(this).attr('id')+'_error');
                            if (errors.length)
                                errors.slideUp('fast', function (){
                                    $(this).remove();
                                    });
                            //TODO: Validation input inplace or via ajax call
                            // and set any new errors AND visibilty changes
                        }
                       );
                <?php
                }
                ?>
                });
            </script>
            <?php
        }
    
    
        static function emitMedia($url, $type) {
            if ($url[0] == '/')
                $url = ROOT_PATH . substr($url, 1);
    
            switch (strtolower($type)) {
            case 'css': ?>
            <link rel="stylesheet" type="text/css" href="<?php echo $url; ?>"/><?php
                break;
            case 'js': ?>
            <script type="text/javascript" src="<?php echo $url; ?>"></script><?php
                break;
            }
        }
    
    
        /**
         * getState
         *
         * Retrieves an array of information which can be passed to the
         * ::loadState method later to recreate the current state of the form
         * fields and values.
         */
        function getState() {
            $info = array();
            foreach ($this->getFields() as $f) {
                // Skip invisible fields
                if (!$f->isVisible())
                    continue;
    
                // Skip fields set to default values
                $v = $f->getClean();
                $d = $f->get('default');
                if ($v == $d)
                    continue;
    
                // Skip empty values
                if (!$v)
                    continue;
    
    
                $info[$f->get('name') ?: $f->get('id')] = $f->to_database($v);
    
            }
            return $info;
        }
    
        /**
         * loadState
         *
         * Reset this form to the state previously recorded by the ::getState()
         * method
         */
        function loadState($state) {
            foreach ($this->getFields() as $f) {
                $name = $f->get('name');
                $f->reset();
                if (isset($state[$name])) {
                    $f->value = $f->to_php($state[$name]);
                }
            }
        }
    
    
        /*
         * Initialize a generic static form
         */
        static function instantiate() {
            $r = new ReflectionClass(get_called_class());
            return $r->newInstanceArgs(func_get_args());
        }
    }
    
    /**
     * SimpleForm
     * Wrapper for inline/static forms.
     *
     */
    class SimpleForm extends Form {
        function __construct($fields=array(), $source=null, $options=array()) {
            parent::__construct($source, $options);
            $this->setFields($fields);
        }
    
    Peter Rotich's avatar
    Peter Rotich committed
    
        function getId() {
            return $this->getFormId();
        }
    
    class CustomForm extends SimpleForm {
    
        function getFields() {
    
    Peter Rotich's avatar
    Peter Rotich committed
            global $thisstaff, $thisclient;
    
    
            $options = $this->options;
    
    Peter Rotich's avatar
    Peter Rotich committed
            $user = $options['user'] ?: $thisstaff ?: $thisclient;
    
            $isedit = ($options['mode'] == 'edit');
            $fields = array();
            foreach (parent::getFields() as $field) {
                if ($isedit && !$field->isEditable($user))
                    continue;
    
                $fields[] = $field;
            }
    
            return $fields;
        }
    }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
    abstract class AbstractForm extends Form {
        function __construct($source=null, $options=array()) {
            parent::__construct($source, $options);
            $this->setFields($this->buildFields());
        }
        /**
         * Fetch the fields defined for this form. This method is only called
         * once.
         */
        abstract function buildFields();
    
    /**
     * Container class to represent the connection between the form fields and the
     * rendered state of the form.
     */
    interface FormRenderer {
        // Render the form fields into a table
        function asTable($form);
        // Render the form fields into divs
        function asBlock($form);
    }
    
    abstract class FormLayout {
        static $default_cell_layout = 'Cell';
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        var $title;
        var $options;
    
        function __construct($title=false, $options=array()) {
            $this->title = $title;
            $this->options = $options;
        }
    
    
        function getLayout($field) {
            $layout = $field->get('layout') ?: static::$default_cell_layout;
            if (is_string($layout))
                $layout = new $layout();
            return $layout;
        }
    }
    
    class GridFluidLayout
    extends FormLayout
    implements FormRenderer {
        function asTable($form) {
          ob_start();
    ?>
          <table class="<?php echo 'grid form' ?>">
    
    Peter Rotich's avatar
    Peter Rotich committed
              <caption><?php echo Format::htmlchars($this->title ?: $form->getTitle()); ?>
    
                      <div><small><?php echo Format::viewableImages($form->getInstructions()); ?></small></div>
              </caption>
    
              <tbody><tr><?php for ($i=0; $i<12; $i++) echo '<td style="width:8.3333%"/>'; ?></tr></tbody>
    
    <?php
          $row_size = 12;
          $cols = $row = 0;
    
    Peter Rotich's avatar
    Peter Rotich committed
    
          //Layout and rendering options
          $options = $this->options;
    
    
          foreach ($form->getFields() as $f) {
              $layout = $this->getLayout($f);
              $size = $layout->getWidth() ?: 12;
              if ($offs = $layout->getOffset()) {
                  $size += $offs;
              }
              if ($cols < $size || $layout->isBreakForced()) {
                  if ($row) echo '</tr>';
                  echo '<tr>';
                  $cols = $row_size;
                  $row++;
              }
              // Render the cell
              $cols -= $size;
              $attrs = array('colspan' => $size, 'rowspan' => $layout->getHeight(),
                  'style' => '"'.$layout->getOption('style').'"');
              if ($offs) { ?>
    
    Peter Rotich's avatar
    Peter Rotich committed
                  <td colspan="<?php echo $offs; ?>"></td> <?php
    
              }
              ?>
              <td class="cell" <?php echo Format::array_implode('=', ' ', array_filter($attrs)); ?>
                  data-field-id="<?php echo $f->get('id'); ?>">
                  <fieldset class="field <?php if (!$f->isVisible()) echo 'hidden'; ?>"
                    id="field<?php echo $f->getWidget()->id; ?>"
                    data-field-id="<?php echo $f->get('id'); ?>">
    <?php         if ($label = $f->get('label')) { ?>
                  <label class="<?php if ($f->isRequired()) echo 'required'; ?>"
                      for="<?php echo $f->getWidget()->id; ?>">
                      <?php echo Format::htmlchars($label); ?>:
    
    Peter Rotich's avatar
    Peter Rotich committed
                    <?php if ($f->isRequired()) { ?>
                    <span class="error">*</span>
                    <?php
                    }?>
    
                  </label>
    <?php         }
                  if ($f->get('hint')) { ?>
                      <div class="field-hint-text">
                          <?php echo Format::htmlchars($f->get('hint')); ?>
                      </div>
    <?php         }
    
    Peter Rotich's avatar
    Peter Rotich committed
                  $f->render($options);
    
                  if ($f->errors())
                      foreach ($f->errors() as $e)
                          echo sprintf('<div class="error">%s</div>', Format::htmlchars($e));
    ?>
                  </fieldset>
              </td>
          <?php
          }
          if ($row)
            echo  '</tr>';
    
          echo '</tbody></table>';
    
          return ob_get_clean();
        }
    
        function asBlock($form) {}
    }
    
    /**
     * Basic container for field and form layouts. By default every cell takes
     * a whole output row and does not imply any sort of width.
     */
    class Cell {
        function isBreakForced()  { return true; }
        function getWidth()       { return false; }
        function getHeight()      { return 1; }
        function getOffset()      { return 0; }
        function getOption($prop) { return false; }
    }
    
    /**
     * Fluid grid layout, meaning each cell renders to the right of the previous
     * cell (for left-to-right layouts). A width in columns can be specified for
     * each cell along with an offset from the previous cell. A height of columns
     * along with an optional break is supported.
     */
    class GridFluidCell
    extends Cell {
        var $span;
        var $options;
    
        function __construct($span, $options=array()) {
            $this->span = $span;
            $this->options = $options + array(
                'rows' => 1,        # rowspan
                'offset' => 0,      # skip some columns
                'break' => false,   # start on a new row
            );
        }
    
        function isBreakForced()  { return $this->options['break']; }
        function getWidth()       { return $this->span; }
        function getHeight()      { return $this->options['rows']; }
        function getOffset()      { return $this->options['offset']; }
        function getOption($prop) { return $this->options[$prop]; }
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    require_once(INCLUDE_DIR . "class.json.php");
    
    class FormField {
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $ht = array(
    
            'label' => false,
    
    Jared Hancock's avatar
    Jared Hancock committed
            'required' => false,
            'default' => false,
            'configuration' => array(),
        );
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $_cform;
    
        var $_clean;
        var $_errors = array();
    
        var $answer;
    
        var $parent;
    
        var $presentation_only = false;
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        static $types = array(
    
            /* @trans */ 'Basic Fields' => array(
                'text'  => array(   /* @trans */ 'Short Answer', 'TextboxField'),
                'memo' => array(    /* @trans */ 'Long Answer', 'TextareaField'),
                'thread' => array(  /* @trans */ 'Thread Entry', 'ThreadEntryField', false),
                'datetime' => array(/* @trans */ 'Date and Time', 'DatetimeField'),
    
    Peter Rotich's avatar
    Peter Rotich committed
                'timezone' => array(/* @trans */ 'Timezone', 'TimezoneField'),
    
                'phone' => array(   /* @trans */ 'Phone Number', 'PhoneField'),
                'bool' => array(    /* @trans */ 'Checkbox', 'BooleanField'),
                'choices' => array( /* @trans */ 'Choices', 'ChoiceField'),
    
                'files' => array(   /* @trans */ 'File Upload', 'FileUploadField'),
    
                'break' => array(   /* @trans */ 'Section Break', 'SectionBreakField'),
    
                'info' => array(    /* @trans */ 'Information', 'FreeTextField'),
    
    Jared Hancock's avatar
    Jared Hancock committed
        );
        static $more_types = array();
    
        static $uid = null;
    
    Peter Rotich's avatar
    Peter Rotich committed
        function _uid() {
            return ++self::$uid;
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function __construct($options=array()) {
            $this->ht = array_merge($this->ht, $options);
            if (!isset($this->ht['id']))
    
    Peter Rotich's avatar
    Peter Rotich committed
                $this->ht['id'] = self::_uid();
    
        }
    
        function __clone() {
            $this->_widget = null;
    
    Peter Rotich's avatar
    Peter Rotich committed
            $this->ht['id'] = self::_uid();
    
        static function addFieldTypes($group, $callable) {
    
            static::$more_types[$group][] = $callable;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        static function allTypes() {
            if (static::$more_types) {
    
                foreach (static::$more_types as $group => $entries)
                    foreach ($entries as $c)
                        static::$types[$group] = array_merge(
                                static::$types[$group] ?: array(), call_user_func($c));
    
    
    Jared Hancock's avatar
    Jared Hancock committed
                static::$more_types = array();
            }
            return static::$types;
        }
    
    
        static function getFieldType($type) {
            foreach (static::allTypes() as $group=>$types)
                if (isset($types[$type]))
                    return $types[$type];
        }
    
    
        function get($what, $default=null) {
            return array_key_exists($what, $this->ht)
                ? $this->ht[$what]
                : $default;
    
        function set($field, $value) {
            $this->ht[$field] = $value;
        }
    
        function getId() {
            return $this->ht['id'];
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        /**
         * getClean
         *
         * Validates and cleans inputs from POST request. This is performed on a
         * field instance, after a DynamicFormSet / DynamicFormSection is
         * submitted via POST, in order to kick off parsing and validation of
         * user-entered data.
         */
        function getClean() {
    
            if (!isset($this->_clean)) {
    
                $this->_clean = (isset($this->value))
    
                    // XXX: The widget value may be parsed already if this is
                    //      linked to dynamic data via ::getAnswer()
    
                    ? $this->value : $this->parse($this->getWidget()->value);
    
                if ($vs = $this->get('cleaners')) {
                    if (is_array($vs)) {
                        foreach ($vs as $cleaner)
    
                            if (is_callable($cleaner))
                                $this->_clean = call_user_func_array(
                                        $cleaner, array($this, $this->_clean));
    
                    }
                    elseif (is_callable($vs))
    
                        $this->_clean = call_user_func_array(
                                $vs, array($this, $this->_clean));
    
                if (!isset($this->_clean) && ($d = $this->get('default')))
                    $this->_clean = $d;
    
                if ($this->isVisible())
                    $this->validateEntry($this->_clean);
    
            }
            return $this->_clean;
    
        function reset() {
    
            $this->value = $this->_clean = $this->_widget = null;
    
        function getValue() {
            return $this->getWidget()->getValue();
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function errors() {
    
            return $this->_errors;
    
        function addError($message, $index=false) {
    
            if ($index)
    
                $this->_errors[$index] = $message;
    
            else
                $this->_errors[] = $message;
    
    
            // Update parent form errors for the field
            if ($this->_form)
                $this->_form->addError($this->errors(), $this->get('id'));
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function isValidEntry() {
            $this->validateEntry();
            return count($this->_errors) == 0;
        }
    
        /**
         * validateEntry
         *
         * Validates user entry on an instance of the field on a dynamic form.
         * This is called when an instance of this field (like a TextboxField)
         * receives data from the user and that value should be validated.
         *
         * Parameters:
         * $value - (string) input from the user
         */
        function validateEntry($value) {
    
            if (!$value && count($this->_errors))
                return;
    
    
    Jared Hancock's avatar
    Jared Hancock committed
            # Validates a user-input into an instance of this field on a dynamic
            # form
    
            if ($this->get('required') && !$value && $this->hasData())
    
                $this->_errors[] = $this->getLocal('label')
                    ? sprintf(__('%s is a required field'), $this->getLocal('label'))
    
                    : __('This is a required field');
    
    
            # Perform declared validators for the field
            if ($vs = $this->get('validators')) {
                if (is_array($vs)) {
                    foreach ($vs as $validator)
                        if (is_callable($validator))
                            $validator($this, $value);
                }
                elseif (is_callable($vs))
                    $vs($this, $value);
            }
    
        /**
         * isVisible
         *
         * If this field has visibility configuration, then it will parse the
         * constraints with the visibility configuration to determine if the
         * field is visible and should be considered for validation
         */
        function isVisible() {
            if ($this->get('visibility') instanceof VisibilityConstraint) {
                return $this->get('visibility')->isVisible($this);
            }
            return true;
        }
    
    
         * Check if the user has edit rights
    
        function isEditable($user=null) {
    
    
            // Internal editable flag used by internal forms e.g internal lists
            if (!$user && isset($this->ht['editable']))
                return $this->ht['editable'];
    
    
    Peter Rotich's avatar
    Peter Rotich committed
            if ($user instanceof Staff)
    
                $flag = DynamicFormField::FLAG_AGENT_EDIT;
    
    Peter Rotich's avatar
    Peter Rotich committed
            else
                $flag = DynamicFormField::FLAG_CLIENT_EDIT;
    
    
            return (($this->get('flags') & $flag) != 0);
    
        /**
         * isStorable
         *
         * Indicate if this field data is storable locally (default).Some field's data
         * might beed to be stored elsewhere for optimization reasons at the
         * application level.
    
        function isStorable() {
    
            return (($this->get('flags') & DynamicFormField::FLAG_EXT_STORED) == 0);
    
        function isRequired() {
            return $this->get('required');
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
        /**
         * parse
         *
         * Used to transform user-submitted data to a PHP value. This value is
         * not yet considered valid. The ::validateEntry() method will be called
         * on the value to determine if the entry is valid. Therefore, if the
         * data is clearly invalid, return something like NULL that can easily
         * be deemed invalid in ::validateEntry(), however, can still produce a
         * useful error message indicating what is wrong with the input.
         */
        function parse($value) {
    
            return is_string($value) ? trim($value) : $value;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        /**
         * to_php
         *
         * Transforms the data from the value stored in the database to a PHP
         * value. The ::to_database() method is used to produce the database
         * valse, so this method is the compliment to ::to_database().
         *
         * Parameters:
         * $value - (string or null) database representation of the field's
         *      content
         */
        function to_php($value) {
            return $value;
        }
    
    
        /**
         * to_config
         *
         * Transform the data from the value to config form (as determined by
    
         * field). to_php is used for each field returned from
         * ::getConfigurationOptions(), and when the whole configuration is
         * built, to_config() is called and receives the config array. The array
         * should be returned, perhaps with modifications, and will be JSON
         * encoded and stashed in the database.
    
         */
        function to_config($value) {
    
    Jared Hancock's avatar
    Jared Hancock committed
        /**
         * to_database
         *
         * Determines the value to be stored in the database. The database
         * backend for all fields is a text field, so this method should return
         * a text value or NULL to represent the value of the field. The
         * ::to_php() method will convert this value back to PHP.
         *
         * Paremeters:
         * $value - PHP value of the field's content
         */
        function to_database($value) {
            return $value;
        }
    
        /**
         * toString
         *
         * Converts the PHP value created in ::parse() or ::to_php() to a
         * pretty-printed value to show to the user. This is especially useful
         * for something like dates which are stored considerably different in
         * the database from their respective human-friendly versions.
         * Furthermore, this method allows for internationalization and
         * localization.
         *
         * Parametes:
         * $value - PHP value of the field's content
         */
        function toString($value) {
    
            return (string) $value;
    
        function __toString() {
            return $this->toString($this->value);
        }
    
    
        /**
         * When data for this field is deleted permanently from some storage
         * backend (like a database), other associated data may need to be
         * cleaned as well. This hook allows fields to participate when the data
         * for a field is cleaned up.
         */
    
        function db_cleanup($field=false) {
    
        /**
         * Returns an HTML friendly value for the data in the field.
         */
        function display($value) {
            return Format::htmlchars($this->toString($value));
        }
    
    
        /**
         * Returns a value suitable for exporting to a foreign system. Mostly
         * useful for things like dates and phone numbers which should be
         * formatted using a standard when exported
         */
        function export($value) {
            return $this->toString($value);
        }
    
    
        /**
         * Fetch a value suitable for embedding the value of this field in an
         * email template. Reference implementation uses ::to_php();
         */
        function asVar($value, $id=false) {
            return $this->to_php($value, $id);
        }
    
        /**
         * Fetch the var type used with the email templating system's typeahead
         * feature. This helps with variable expansion if supported by this
         * field's ::asVar() method. This method should return a valid classname
         * which implements the `TemplateVariable` interface.
         */
        function asVarType() {
            return false;
        }
    
    
        /**
         * Describe the difference between the to two values. Note that the
         * values should be passed through ::parse() or to_php() before
         * utilizing this method.
         */
        function whatChanged($before, $after) {
            if ($before)
    
                $desc = __('changed from <strong>%1$s</strong> to <strong>%2$s</strong>');
    
                $desc = __('set to <strong>%2$s</strong>');
            return sprintf($desc, $this->display($before), $this->display($after));
    
        /**
         * Convert the field data to something matchable by filtering. The
         * primary use of this is for ticket filtering.
         */
        function getFilterData() {
            return $this->toString($this->getClean());
        }
    
    
        /**
         * Fetches a value that represents this content in a consistent,
         * searchable format. This is used by the search engine system and
         * backend.
         */
    
        function searchable($value) {
            return Format::searchable($this->toString($value));
        }
    
    
        function getKeys($value) {
            return $this->to_database($value);
        }
    
    
        /**
         * Fetches a list of options for searching. The values returned from
         * this method are passed to the widget's `::render()` method so that
         * the widget can be affected by this setting. For instance, date fields
         * might have a 'between' search option which should trigger rendering
         * of two date widgets for search results.
         */
        function getSearchMethods() {
            return array(
                'set' =>        __('has a value'),
    
                'nset' =>       __('does not have a value'),
    
                'equal' =>      __('is'),
    
                'nequal' =>     __('is not'),
    
                'contains' =>   __('contains'),
                'match' =>      __('matches'),
            );
        }
    
        function getSearchMethodWidgets() {
            return array(
                'set' => null,
    
                'equal' => array('TextboxField', array('configuration' => array('size' => 40))),
                'nequal' => array('TextboxField', array('configuration' => array('size' => 40))),
                'contains' => array('TextboxField', array('configuration' => array('size' => 40))),
    
                'match' => array('TextboxField', array(
    
                    'placeholder' => __('Valid regular expression'),
                    'configuration' => array('size'=>30),
    
                    'validators' => function($self, $v) {
    
                        if (false === @preg_match($v, ' ')
                            && false === @preg_match("/$v/", ' '))
    
                            $self->addError(__('Cannot compile this regular expression'));
                    })),
            );
        }
    
    
        /**
         * This is used by the searching system to build a query for the search
         * engine. The function should return a criteria listing to match
         * content saved by the field by the `::to_database()` function.
         */
        function getSearchQ($method, $value, $name=false) {
            $criteria = array();
            $Q = new Q();
            $name = $name ?: $this->get('name');
            switch ($method) {
    
                    $Q->negate();
                case 'set':
                    $criteria[$name . '__isnull'] = false;
                    break;
    
    
                    $Q->negate();
                case 'equal':
    
                    $criteria[$name] = $value;
    
                    break;
    
                case 'contains':
                    $criteria[$name . '__contains'] = $value;
                    break;
    
                case 'match':
                    $criteria[$name . '__regex'] = $value;
                    break;
            }
            return $Q->add($criteria);
        }
    
    
        function getSearchWidget($method) {
            $methods = $this->getSearchMethodWidgets();
            $info = $methods[$method];
            if (is_array($info)) {
                $class = $info[0];
                return new $class($info[1]);
            }
            return $info;
        }
    
    
        function describeSearchMethod($method) {
            switch ($method) {
            case 'set':
                return __('%s has a value');
            case 'nset':
                return __('%s does not have a value');
            case 'equal':
                return __('%s is %s' /* describes an equality */);
            case 'nequal':
                return __('%s is not %s' /* describes an inequality */);
            case 'contains':
                return __('%s contains "%s"');
            case 'match':
                return __('%s matches pattern %s');
    
            case 'includes':
                return __('%s in (%s)');
            case '!includes':
                return __('%s not in (%s)');