Skip to content
Snippets Groups Projects
class.forms.php 110 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 $id = 0;
    
    
    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()) {
    
    
            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 getId() {
            return static::$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->getClean();
    
                // Validate the whole form so that errors can be added to the
                // individual fields and collected below.
                foreach ($this->validators as $V) {
                    $V($this);
                }
    
                foreach ($this->getFields() as $field)
    
                    if ($field->errors() && (!$include || $include($field)))
    
                        $this->_errors[$field->get('id')] = $field->errors();
            }
            return !$this->_errors;
        }
    
        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;
    
                    $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'];
    
            $form = $this;
            if ($staff)
                include(STAFFINC_DIR . 'templates/dynamic-form.tmpl.php');
            else
                include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php');
    
            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->getId()))
                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);
        }
    
    
    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'),
                '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;
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function __construct($options=array()) {
            $this->ht = array_merge($this->ht, $options);
            if (!isset($this->ht['id']))
    
                $this->ht['id'] = self::$uid++;
        }
    
        function __clone() {
            $this->_widget = null;
            $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;
        }
    
    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))
                    ? $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 ($this->isVisible())
                    $this->validateEntry($this->_clean);
    
    
                if (!isset($this->_clean) && ($d = $this->get('default')))
                    $this->_clean = $d;
    
            }
            return $this->_clean;
    
        function reset() {
            $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[] = sprintf(__('%s is a required field'),
                    $this->getLabel());
    
    
            # 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() {
            $config = $this->getConfiguration();
            if ($this->get('visibility') instanceof VisibilityConstraint) {
                return $this->get('visibility')->isVisible($this);
            }
            return true;
        }
    
    
        /**
         * FIXME: Temp
         *
         */
    
        function isEditable() {
            return (($this->get('edit_mask') & 32) == 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_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() {
        }
    
    
        /**
         * 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;
        }
    
    
        /**
         * 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));
        }
    
    
        /**
         * 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'),
    
                'notset' =>     __('does not have a value'),
    
                'equal' =>      __('is'),
                'equal.not' =>  __('is not'),
                'contains' =>   __('contains'),
                'match' =>      __('matches'),
            );
        }
    
        function getSearchMethodWidgets() {
            return array(
                'set' => null,
    
                'equal' => array('TextboxField', array()),
                'equal.not' => array('TextboxField', array()),
                'contains' => array('TextboxField', array()),
                'match' => array('TextboxField', array(
    
                    'placeholder' => __('Valid regular expression'),
                    'configuration' => array('size'=>30),
    
                    'validators' => function($self, $v) {
                        if (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) {
                case 'notset':
                    $Q->negate();
                case 'set':
                    $criteria[$name . '__isnull'] = false;
                    break;
    
                case 'equal.not':
                    $Q->negate();
                case 'equal':
                    $criteria[$name . '__eq'] = $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;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getLabel() { return $this->get('label'); }
    
        /**
         * getImpl
         *
         * Magic method that will return an implementation instance of this
         * field based on the simple text value of the 'type' value of this
         * field instance. The list of registered fields is determined by the
         * global get_dynamic_field_types() function. The data from this model
         * will be used to initialize the returned instance.
         *
         * For instance, if the value of this field is 'text', a TextField
         * instance will be returned.
         */
    
        function getImpl($parent=null) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            // Allow registration with ::addFieldTypes and delayed calling
    
            $type = static::getFieldType($this->get('type'));
            $clazz = $type[1];
    
    Jared Hancock's avatar
    Jared Hancock committed
            $inst = new $clazz($this->ht);
            $inst->parent = $parent;
    
            $inst->setForm($this->_form);
    
    Jared Hancock's avatar
    Jared Hancock committed
            return $inst;
    
        function __call($what, $args) {
            // XXX: Throw exception if $this->parent is not set
    
            if (!$this->parent)
    
                throw new Exception(sprintf(__('%s: Call to undefined function'),
                    $what));
    
    Jared Hancock's avatar
    Jared Hancock committed
            // BEWARE: DynamicFormField has a __call() which will create a new
            //      FormField instance and invoke __call() on it or bounce
            //      immediately back
    
            return call_user_func_array(
                array($this->parent, $what), $args);
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getAnswer() { return $this->answer; }
    
        function setAnswer($ans) { $this->answer = $ans; }
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function getFormName() {
    
            if (is_numeric($this->get('id')))
    
                return substr(md5(
                    session_id() . '-field-id-'.$this->get('id')), -16);
    
                return $this->get('name') ?: $this->get('id');
    
        function setForm($form) {
            $this->_form = $form;
        }
        function getForm() {
            return $this->_form;
        }
        /**
         * Returns the data source for this field. If created from a form, the
         * data source from the form is returned. Otherwise, if the request is a
         * POST, then _POST is returned.
         */
        function getSource() {
            if ($this->_form)
                return $this->_form->getSource();
            elseif ($_SERVER['REQUEST_METHOD'] == 'POST')
                return $_POST;
            else
                return array();
        }
    
    
        function render($options=array()) {
            $rv = $this->getWidget()->render($options);
    
            if ($v = $this->get('visibility')) {
                $v->emitJavascript($this);
            }
            return $rv;
    
        function renderExtras($options=array()) {
    
        function getMedia() {
            $widget = $this->getWidget();
            return $widget::$media;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getConfigurationOptions() {
            return array();
        }
    
        /**
         * getConfiguration
         *
         * Loads configuration information from database into hashtable format.
         * Also, the defaults from ::getConfigurationOptions() are integrated
         * into the database-backed options, so that if options have not yet
         * been set or a new option has been added and not saved for this field,
         * the default value will be reflected in the returned configuration.
         */
        function getConfiguration() {
            if (!$this->_config) {
                $this->_config = $this->get('configuration');
                if (is_string($this->_config))
                    $this->_config = JsonDataParser::parse($this->_config);
                elseif (!$this->_config)
                    $this->_config = array();
                foreach ($this->getConfigurationOptions() as $name=>$field)
                    if (!isset($this->_config[$name]))
                        $this->_config[$name] = $field->get('default');
            }
            return $this->_config;
        }
    
    
        /**
         * If the [Config] button should be shown to allow for the configuration
         * of this field
         */
    
    Jared Hancock's avatar
    Jared Hancock committed
        function isConfigurable() {
            return true;
        }
    
    
        /**
         * Field type is changeable in the admin interface
         */
        function isChangeable() {
            return true;
        }
    
        /**
         * Field does not contain data that should be saved to the database. Ie.
         * non data fields like section headers
         */
        function hasData() {
            return true;
        }
    
        /**
         * Returns true if the field/widget should be rendered as an entire
         * block in the target form.
         */
        function isBlockLevel() {
            return false;
        }
    
        /**
         * Fields should not be saved with the dynamic data. It is assumed that
         * some static processing will store the data elsewhere.
         */
        function isPresentationOnly() {
    
            return $this->presentation_only;
    
        /**
         * Indicates if the field places data in the `value_id` column. This
         * is currently used by the materialized view system
         */
        function hasIdValue() {
            return false;
        }
    
    
        /**
         * Indicates if the field has subfields accessible via getSubFields()
         * method. Useful for filter integration. Should connect with
         * getFilterData()
         */
        function hasSubFields() {
            return false;
        }
        function getSubFields() {
            return null;
        }
    
    
        /**
         * Indicates if the field provides for searching for something other
         * than keywords. For instance, textbox fields can have hits by keyword
         * searches alone, but selection fields should provide the option to
         * match a specific value or set of values and therefore need to
         * participate on any search builder.
         */
        function hasSpecialSearch() {
            return true;
        }
    
    
        function getConfigurationForm($source=null) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            if (!$this->_cform) {
    
                $type = static::getFieldType($this->get('type'));
                $clazz = $type[1];
    
                $T = new $clazz($this->ht);
    
                $config = $this->getConfiguration();
    
                $this->_cform = new SimpleForm($T->getConfigurationOptions(), $source);
    
                    foreach ($this->_cform->getFields() as $name=>$f) {
    
                        if ($config && isset($config[$name]))
    
                            $f->value = $config[$name];
                        elseif ($f->get('default'))
                            $f->value = $f->get('default');
                    }
                }
    
    Jared Hancock's avatar
    Jared Hancock committed
            }
            return $this->_cform;
        }
    
    
        function configure($prop, $value) {
            $this->getConfiguration();
            $this->_config[$prop] = $value;
        }
    
    
        function getWidget($widgetClass=false) {
    
            if (!static::$widget)
    
                throw new Exception(__('Widget not defined for this field'));
    
            if (!isset($this->_widget)) {
    
                $wc = $widgetClass ?: $this->get('widget') ?: static::$widget;
    
    Jared Hancock's avatar
    Jared Hancock committed
                $this->_widget = new $wc($this);
    
                $this->_widget->parseValue();
            }
            return $this->_widget;
    
    
        function getSelectName() {
            $name = $this->get('name') ?: 'field_'.$this->get('id');
            if ($this->hasIdValue())
                $name .= '_id';
    
            return $name;
        }
    
    
        function getTranslateTag($subtag) {
    
            return _H(sprintf('field.%s.%s%s', $subtag, $this->get('id'),
                $this->get('form_id') ? '' : '*internal*'));
    
        }
        function getLocal($subtag, $default=false) {
            $tag = $this->getTranslateTag($subtag);
            $T = CustomDataTranslation::translate($tag);
            return $T != $tag ? $T : ($default ?: $this->get($subtag));
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class TextboxField extends FormField {
    
        static $widget = 'TextboxWidget';
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function getConfigurationOptions() {
            return array(
                'size'  =>  new TextboxField(array(
    
                    'id'=>1, 'label'=>__('Size'), 'required'=>false, 'default'=>16,
    
    Jared Hancock's avatar
    Jared Hancock committed
                        'validator' => 'number')),
                'length' => new TextboxField(array(
    
                    'id'=>2, 'label'=>__('Max Length'), 'required'=>false, 'default'=>30,
    
    Jared Hancock's avatar
    Jared Hancock committed
                        'validator' => 'number')),
                'validator' => new ChoiceField(array(
    
                    'id'=>3, 'label'=>__('Validator'), 'required'=>false, 'default'=>'',
                    'choices' => array('phone'=>__('Phone Number'),'email'=>__('Email Address'),
    
                        'ip'=>__('IP Address'), 'number'=>__('Number'),
                        'regex'=>__('Custom (Regular Expression)'), ''=>__('None')))),
                'regex' => new TextboxField(array(
                    'id'=>6, 'label'=>__('Regular Expression'), 'required'=>true,
                    'configuration'=>array('size'=>40, 'length'=>100),
                    'visibility' => new VisibilityConstraint(
                        new Q(array('validator__eq'=>'regex')),
                        VisibilityConstraint::HIDDEN
                    ),
                    'cleaners' => function ($self, $value) {
                        $wrapped = "/".$value."/iu";
                        if (false === @preg_match($value, ' ')
                                && false !== @preg_match($wrapped, ' ')) {
    
                            $value = $wrapped;
    
                        if ($value == '//iu')
                            return '';
    
    
                        return $value;
                    },
                    'validators' => function($self, $v) {
                        if (false === @preg_match($v, ' '))
                            $self->addError(__('Cannot compile this regular expression'));
                    })),
    
                'validator-error' => new TextboxField(array(
    
                    'id'=>4, 'label'=>__('Validation Error'), 'default'=>'',
    
                    'configuration'=>array('size'=>40, 'length'=>60,
                        'translatable'=>$this->getTranslateTag('validator-error')