Newer
Older
<?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 {
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];
}
if (!is_array($fields) && !$fields instanceof Traversable)
return;
$this->fields = $fields;
foreach ($fields as $k=>$f) {
$f->setForm($this);
function getFields() {
return $this->fields;
}
$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);
}
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) {
$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) {
// 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();
}
}
return $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');
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);
$dedup[$key] = true;
}
}
}
}
}
function emitJavascript($options=array()) {
// Check if we need to emit javascript
if (!($fid=$this->getId()))
return;
?>
<script type="text/javascript">
$(function() {
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<?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);
}
}
require_once(INCLUDE_DIR . "class.json.php");
class FormField {
static $widget = false;
'required' => false,
'default' => false,
'configuration' => array(),
);
var $_clean;
var $_errors = array();
var $presentation_only = false;
/* @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'),
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;
}
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));
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'];
}
/**
* 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() {
$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;
$this->value = $this->_clean = $this->_widget = null;
function getValue() {
return $this->getWidget()->getValue();
}
function addError($message, $index=false) {
$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'));
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;
# Validates a user-input into an instance of this field on a dynamic
# form
if ($this->get('required') && !$value && $this->hasData())
$this->_errors[] = $this->getLabel()
? sprintf(__('%s is a required field'), $this->getLabel())
: __('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() {
$config = $this->getConfiguration();
if ($this->get('visibility') instanceof VisibilityConstraint) {
return $this->get('visibility')->isVisible($this);
}
return true;
}
/**
* FIXME: Temp
*
*/
function isEditable() {
return (($this->get('flags') & DynamicFormField::FLAG_MASK_EDIT) == 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');
}
/**
* 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;
}
/**
* 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). By default to_php is used at the base level
*
*/
function to_config($value) {
return $this->to_php($value);
}
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
/**
* 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) {
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>%2$s</strong> to <strong>%1$s</strong>');
else
$desc = __('set to <strong>%1$s</strong>');
return sprintf($desc, $this->display($after), $this->display($before));
}
/**
* 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'),
'nset' => __('does not have a value'),
'contains' => __('contains'),
'match' => __('matches'),
);
}
function getSearchMethodWidgets() {
return array(
'set' => null,
'equal' => array('TextboxField', array()),
'nequal' => 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) {
$Q->negate();
case 'set':
$criteria[$name . '__isnull'] = false;
break;
$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;
}
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');
}
}
function describeSearch($method, $value, $name=false) {
$desc = $this->describeSearchMethod($method);
$value = $this->toString($value);
return sprintf($desc, $name, $value);
}
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.
*/
// Allow registration with ::addFieldTypes and delayed calling
$type = static::getFieldType($this->get('type'));
$clazz = $type[1];
$inst = new $clazz($this->ht);
$inst->parent = $parent;
$inst->setForm($this->_form);
function __call($what, $args) {
// XXX: Throw exception if $this->parent is not set
throw new Exception(sprintf(__('%s: Call to undefined function'),
$what));
// 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);
}
function getAnswer() { return $this->answer; }
function setAnswer($ans) { $this->answer = $ans; }
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;
}
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
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
*/
function isConfigurable() {
return true;
}
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
/**
* 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) {
$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');
}
}
function configure($prop, $value) {
$this->getConfiguration();
$this->_config[$prop] = $value;
}
function getWidget($widgetClass=false) {
throw new Exception(__('Widget not defined for this field'));
$wc = $widgetClass ?: $this->get('widget') ?: static::$widget;
$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));
}
}
class TextboxField extends FormField {
static $widget = 'TextboxWidget';
function getConfigurationOptions() {
return array(
'size' => new TextboxField(array(
'id'=>1, 'label'=>__('Size'), 'required'=>false, 'default'=>16,
'validator' => 'number')),
'length' => new TextboxField(array(
'id'=>2, 'label'=>__('Max Length'), 'required'=>false, 'default'=>30,
'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, ' ')) {
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')
),
'hint'=>__('Message shown to user if the input does not match the validator'))),
'placeholder' => new TextboxField(array(
'id'=>5, 'label'=>__('Placeholder'), 'required'=>false, 'default'=>'',
'hint'=>__('Text shown in before any input from the user'),
'configuration'=>array('size'=>40, 'length'=>40,
'translatable'=>$this->getTranslateTag('placeholder')
),
function hasSpecialSearch() {
return false;
}
function validateEntry($value) {
parent::validateEntry($value);
'email' => array(array('Validator', 'is_valid_email'),
__('Enter a valid email address')),
'phone' => array(array('Validator', 'is_phone'),
__('Enter a valid phone number')),
__('Enter a valid IP address')),
'number' => array('is_numeric', __('Enter a number')),
'regex' => array(
function($v) use ($config) {
$regex = $config['regex'];
return @preg_match($regex, $v);
}, __('Value does not match required pattern')
),
);
// Support configuration forms, as well as GUI-based form fields
$valid = $this->get('validator');
if (!$valid) {
$valid = $config['validator'];
}
if (!$value || !isset($validators[$valid]))
return;
$error = $func[1];
if ($config['validator-error'])
$error = $this->getLocal('validator-error', $config['validator-error']);
if (is_array($func) && is_callable($func[0]))
if (!call_user_func($func[0], $value))
class PasswordField extends TextboxField {
static $widget = 'PasswordWidget';
function parse($value) {
// Don't trim the value
return $value;
}
// If not set in UI, don't save the empty value
if (!$value)
throw new FieldUnchanged();
return Crypto::encrypt($value, SECRET_SALT, 'pwfield');
return Crypto::decrypt($value, SECRET_SALT, 'pwfield');
static $widget = 'TextareaWidget';
function getConfigurationOptions() {
return array(
'cols' => new TextboxField(array(
'id'=>1, 'label'=>__('Width').' '.__('(chars)'), 'required'=>true, 'default'=>40)),
'id'=>2, 'label'=>__('Height').' '.__('(rows)'), 'required'=>false, 'default'=>4)),
'id'=>3, 'label'=>__('Max Length'), 'required'=>false, 'default'=>0)),
'id'=>4, 'label'=>__('HTML'), 'required'=>false, 'default'=>true,
'configuration'=>array('desc'=>__('Allow HTML input in this box')))),
'placeholder' => new TextboxField(array(
'id'=>5, 'label'=>__('Placeholder'), 'required'=>false, 'default'=>'',
'hint'=>__('Text shown in before any input from the user'),
'configuration'=>array('size'=>40, 'length'=>40,
'translatable'=>$this->getTranslateTag('placeholder')),
function hasSpecialSearch() {
return false;
}
function display($value) {
$config = $this->getConfiguration();
if ($config['html'])
return Format::safe_html($value);
else
return nl2br(Format::htmlchars($value));
$value = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $value); //<?php
$value = Format::htmldecode(Format::striptags($value));
return Format::searchable($value);
}
function export($value) {
return (!$value) ? $value : Format::html2text($value);
}
function parse($value) {
$config = $this->getConfiguration();
if ($config['html'])
return Format::sanitize($value);
else
return $value;
}
}
class PhoneField extends FormField {
static $widget = 'PhoneNumberWidget';
function getConfigurationOptions() {
return array(
'ext' => new BooleanField(array(
'label'=>__('Extension'), 'default'=>true,
'desc'=>__('Add a separate field for the extension'),
),
)),
'digits' => new TextboxField(array(
'label'=>__('Minimum length'), 'default'=>7,
'hint'=>__('Fewest digits allowed in a valid phone number'),
'configuration'=>array('validator'=>'number', 'size'=>5),
)),
'format' => new ChoiceField(array(
'label'=>__('Display format'), 'default'=>'us',
'choices'=>array(''=>'-- '.__('Unformatted').' --',
'us'=>__('United States')),
function hasSpecialSearch() {
return false;
}
function validateEntry($value) {
parent::validateEntry($value);
$config = $this->getConfiguration();
# Run validator against $this->value for email type
list($phone, $ext) = explode("X", $value, 2);
if ($phone && (
!is_numeric($phone) ||
strlen($phone) < $config['digits']))
$this->_errors[] = __("Enter a valid phone number");
$this->_errors[] = __("Enter a valid phone extension");
$this->_errors[] = __("Enter a phone number for the extension");
function parse($value) {
// NOTE: Value may have a legitimate 'X' to separate the number and
// extension parts. Don't remove the 'X'
$val = preg_replace('/[^\dX]/', '', $value);
// Pass completely-incorrect string for validation error
return $val ?: $value;
$config = $this->getConfiguration();
list($phone, $ext) = explode("X", $value, 2);
switch ($config['format']) {
case 'us':
$phone = Format::phone($phone);
break;
}
if ($ext)
$phone.=" x$ext";
return $phone;
}
}
class BooleanField extends FormField {
static $widget = 'CheckboxWidget';
function getConfigurationOptions() {
return array(
'desc' => new TextareaField(array(
'id'=>1, 'label'=>__('Description'), 'required'=>false, 'default'=>'',
'hint'=>__('Text shown inline with the widget'),
'configuration'=>array('rows'=>2)))
);
}
function to_database($value) {
return ($value) ? '1' : '0';
}
function parse($value) {
return $this->to_php($value);
}
return ($value) ? __('Yes') : __('No');
function getSearchMethods() {
return array(
'set' => __('checked'),
'nset' => __('unchecked'),
);
}
function getSearchMethodWidgets() {
return array(
'set' => null,
function getSearchQ($method, $value, $name=false) {
$name = $name ?: $this->get('name');
switch ($method) {
case 'set':
return new Q(array($name => '1'));
return new Q(array($name => '0'));
default:
return parent::getSearchQ($method, $value, $name);
}
}
}
class ChoiceField extends FormField {
static $widget = 'ChoicesWidget';
function getConfigurationOptions() {
return array(
'choices' => new TextareaField(array(
'id'=>1, 'label'=>__('Choices'), 'required'=>false, 'default'=>'',
'hint'=>__('List choices, one per line. To protect against spelling changes, specify key:value names to preserve entries if the list item names change'),
'configuration'=>array('html'=>false)
)),
'default' => new TextboxField(array(
'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
'hint'=>__('(Enter a key). Value selected from the list initially'),
'configuration'=>array('size'=>20, 'length'=>40),
)),
'prompt' => new TextboxField(array(
'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
'hint'=>__('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40,
'translatable'=>$this->getTranslateTag('prompt'),
),
'multiselect' => new BooleanField(array(
'id'=>1, 'label'=>'Multiselect', 'required'=>false, 'default'=>false,
'configuration'=>array(
'desc'=>'Allow multiple selections')
)),
return $this->to_php($value ?: null);
}
function to_database($value) {
if (!is_array($value)) {
$choices = $this->getChoices();
if (isset($choices[$value]))
$value = array($value => $choices[$value]);
}
if (is_array($value))
return $value;
}
function to_php($value) {
if (is_string($value))
$value = JsonDataParser::parse($value) ?: $value;
// CDATA table may be built with comma-separated key,value,key,value
if (is_string($value)) {
$values = array();
$choices = $this->getChoices();
foreach (explode(',', $value) as $V) {
if (isset($choices[$V]))
$values[$V] = $choices[$V];
if (array_filter($values))
$value = $values;
$config = $this->getConfiguration();
if (!$config['multiselect'] && is_array($value) && count($value) < 2) {
reset($value);
if (!is_array($value))
$value = $this->getChoice($value);
if (is_array($value))
return implode(', ', $value);
return (string) $value;
function whatChanged($before, $after) {
$B = (array) $before;
$A = (array) $after;
$added = array_diff($A, $B);
$deleted = array_diff($B, $A);
$added = array_map(array($this, 'display'), $added);
$deleted = array_map(array($this, 'display'), $deleted);
if ($added && $deleted) {
$desc = sprintf(
__('added <strong>%1$s</strong> and removed <strong>%2$s</strong>'),
implode(', ', $added), implode(', ', $deleted));
}
elseif ($added) {
$desc = sprintf(
__('added <strong>%1$s</strong>'),
implode(', ', $added));
}
elseif ($deleted) {
$desc = sprintf(
__('removed <strong>%1$s</strong>'),
implode(', ', $deleted));
}
else {
$desc = sprintf(
__('changed from <strong>%1$s</strong> to <strong>%2$s</strong>'),
$this->display($before), $this->display($after));
}
return $desc;
}
/*
Return criteria to which the choice should be filtered by
*/
function getCriteria() {
$config = $this->getConfiguration();
$criteria = array();
if (isset($config['criteria']))
$criteria = $config['criteria'];
return $criteria;
}
$selection = array();
if ($value && is_array($value)) {
} elseif (isset($choices[$value]))
$selection[] = $choices[$value];
elseif ($this->get('default'))
$selection[] = $choices[$this->get('default')];
return $selection;
function getChoices($verbose=false) {
if ($this->_choices === null || $verbose) {
// Allow choices to be set in this->ht (for configurationOptions)
$this->_choices = $this->get('choices');
if (!$this->_choices) {
$this->_choices = array();
$config = $this->getConfiguration();
$choices = explode("\n", $config['choices']);
foreach ($choices as $choice) {
// Allow choices to be key: value
list($key, $val) = explode(':', $choice);
if ($val == null)
$val = $key;
$this->_choices[trim($key)] = trim($val);
}
// Add old selections if nolonger available
// This is necessary so choices made previously can be
// retained
$values = ($a=$this->getAnswer()) ? $a->getValue() : array();
if ($values && is_array($values)) {
foreach ($values as $k => $v) {
if (!isset($this->_choices[$k])) {
if ($verbose) $v .= ' (retired)';
$this->_choices[$k] = $v;
}
}
}
}
}
return $this->_choices;
function lookupChoice($value) {
return null;
}
function getSearchMethods() {
return array(
'set' => __('has a value'),
'nset' => __('does not have a value'),
'includes' => __('includes'),
'!includes' => __('does not include'),
);
}
function getSearchMethodWidgets() {
return array(
'set' => null,
'includes' => array('ChoiceField', array(
'choices' => $this->getChoices(),
'configuration' => array('multiselect' => true),
)),
'!includes' => array('ChoiceField', array(
'choices' => $this->getChoices(),
'configuration' => array('multiselect' => true),
)),
function getSearchQ($method, $value, $name=false) {
$name = $name ?: $this->get('name');
switch ($method) {
case '!includes':
return Q::not(array("{$name}__in" => array_keys($value)));
case 'includes':
return new Q(array("{$name}__in" => array_keys($value)));
default:
return parent::getSearchQ($method, $value, $name);
}
}
function describeSearchMethod($method) {
switch ($method) {
case 'includes':
return __('%s includes %s' /* includes -> if a list includes a selection */);
case 'includes':
return __('%s does not include %s' /* includes -> if a list includes a selection */);
default:
return parent::describeSearchMethod($method);
}
}
}
class DatetimeField extends FormField {
static $widget = 'DatetimePickerWidget';
function to_database($value) {
// Store time in gmt time, unix epoch format
return (string) $value;
}
function to_php($value) {
if (!$value)
return $value;
else
return (int) $value;
}
function asVar($value, $id=false) {
if (!$value) return null;
return new FormattedDate((int) $value, 'UTC', false, false);
}
function asVarType() {
return 'FormattedDate';
}
function toString($value) {
global $cfg;
$config = $this->getConfiguration();
// If GMT is set, convert to local time zone. Otherwise, leave
// unchanged (default TZ is UTC)
if ($config['time'])
return Format::datetime($value, false, !$config['gmt'] ? 'UTC' : false);
return Format::date($value, false, false, !$config['gmt'] ? 'UTC' : false);
function export($value) {
$config = $this->getConfiguration();
if (!$value)
return '';
else
return Format::date($value, false, 'y-MM-dd HH:mm:ss', !$config['gmt'] ? 'UTC' : false);
function getConfigurationOptions() {
return array(
'time' => new BooleanField(array(
'id'=>1, 'label'=>__('Time'), 'required'=>false, 'default'=>false,
'desc'=>__('Show time selection with date picker')))),
'id'=>2, 'label'=>__('Timezone Aware'), 'required'=>false,
'desc'=>__("Show date/time relative to user's timezone")))),
'id'=>3, 'label'=>__('Earliest'), 'required'=>false,
'hint'=>__('Earliest date selectable'))),
'id'=>4, 'label'=>__('Latest'), 'required'=>false,
'default'=>null, 'hint'=>__('Latest date selectable'))),
'id'=>5, 'label'=>__('Allow Future Dates'), 'required'=>false,
'desc'=>__('Allow entries into the future' /* Used in the date field */)),
)),
);
}
function validateEntry($value) {
$config = $this->getConfiguration();
parent::validateEntry($value);
if (!$value) return;
if ($config['min'] and $value < $config['min'])
$this->_errors[] = __('Selected date is earlier than permitted');
elseif ($config['max'] and $value > $config['max'])
$this->_errors[] = __('Selected date is later than permitted');
// strtotime returns -1 on error for PHP < 5.1.0 and false thereafter
elseif ($value === -1 or $value === false)
$this->_errors[] = __('Enter a valid date');
// SearchableField interface ------------------------------
function getSearchMethods() {
return array(
'set' => __('has a value'),
'nset' => __('does not have a value'),
'equal' => __('on'),
'before' => __('before'),
'after' => __('after'),
'between' => __('between'),
'ndaysago' => __('in the last n days'),
'ndays' => __('in the next n days'),
);
}
function getSearchMethodWidgets() {
$config_notime = $config = $this->getConfiguration();
$config_notime['time'] = false;
return array(
'set' => null,
'equal' => array('DatetimeField', array(
'configuration' => $config_notime,
'nequal' => array('DatetimeField', array(
'configuration' => $config_notime,
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
)),
'before' => array('DatetimeField', array(
'configuration' => $config,
)),
'after' => array('DatetimeField', array(
'configuration' => $config,
)),
'between' => array('InlineformField', array(
'form' => array(
'left' => new DatetimeField(),
'text' => new FreeTextField(array(
'configuration' => array('content' => 'and'))
),
'right' => new DatetimeField(),
),
)),
'ndaysago' => array('InlineformField', array(
'form' => array(
'until' => new TextboxField(array(
'configuration' => array('validator'=>'number', 'size'=>4))
),
'text' => new FreeTextField(array(
'configuration' => array('content' => 'days'))
),
),
)),
'ndays' => array('InlineformField', array(
'form' => array(
'until' => new TextboxField(array(
'configuration' => array('validator'=>'number', 'size'=>4))
),
'text' => new FreeTextField(array(
'configuration' => array('content' => 'days'))
),
),
)),
);
}
function getSearchQ($method, $value, $name=false) {
$name = $name ?: $this->get('name');
$value = is_int($value)
? DateTime::createFromFormat('U', Misc::dbtime($value)) ?: $value
: $value;
case 'equal':
$l = clone $value;
$r = $value->add(new DateInterval('P1D'));
return new Q(array(
"{$name}__gte" => $l,
"{$name}__lt" => $r
));
case 'nequal':
$l = clone $value;
$r = $value->add(new DateInterval('P1D'));
return Q::any(array(
"{$name}__lt" => $l,
"{$name}__gte" => $r,
));
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
case 'after':
return new Q(array("{$name}__gte" => $value));
case 'before':
return new Q(array("{$name}__lt" => $value));
case 'between':
return new Q(array(
"{$name}__gte" => $value['left'],
"{$name}__lte" => $value['right'],
));
case 'ndaysago':
return new Q(array(
"{$name}__lt" => SqlFunction::NOW(),
"{$name}__gte" => SqlExpression::minus(SqlFunction::NOW(), SqlInterval::DAY($value['until'])),
));
case 'ndays':
return new Q(array(
"{$name}__gt" => SqlFunction::NOW(),
"{$name}__lte" => SqlExpression::plus(SqlFunction::NOW(), SqlInterval::DAY($value['until'])),
));
default:
return parent::getSearchQ($method, $value, $name);
}
}
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
function describeSearchMethod($method) {
switch ($method) {
case 'before':
return __('%1$s before %2$s' /* occurs before a date and time */);
case 'after':
return __('%1$s after %2$s' /* occurs after a date and time */);
case 'ndays':
return __('%1$s in the next %2$s' /* occurs within a window (like 3 days) */);
case 'ndaysago':
return __('%1$s in the last %2$s' /* occurs within a recent window (like 3 days) */);
case 'between':
return __('%1$s between %2$s and %3$s');
default:
return parent::describeSearchMethod($method);
}
}
function describeSearch($method, $value, $name=false) {
if ($method === 'between') {
$l = $this->toString($value['left']);
$r = $this->toString($value['right']);
$desc = $this->describeSearchMethod($method);
return sprintf($desc, $name, $l, $r);
}
return parent::describeSearch($method, $value, $name);
}
/**
* This is kind-of a special field that doesn't have any data. It's used as
* a field to provide a horizontal section break in the display of a form
*/
class SectionBreakField extends FormField {
static $widget = 'SectionBreakWidget';
function hasData() {
return false;
}
function isBlockLevel() {
return true;
}
}
class ThreadEntryField extends FormField {
static $widget = 'ThreadEntryWidget';
function isChangeable() {
return false;
}
function isBlockLevel() {
return true;
}
function isPresentationOnly() {
return true;
}
function hasSpecialSearch() {
return false;
}
function getMedia() {
$config = $this->getConfiguration();
$media = parent::getMedia() ?: array();
if ($config['attachments'])
$media = array_merge_recursive($media, FileUploadWidget::$media);
return $media;
}
function getConfigurationOptions() {
global $cfg;
$attachments = new FileUploadField();
$fileupload_config = $attachments->getConfigurationOptions();
if ($cfg->getAllowedFileTypes())
$fileupload_config['extensions']->set('default', $cfg->getAllowedFileTypes());
foreach ($fileupload_config as $C) {
$C->set('visibility', new VisibilityConstraint(new Q(array(
'attachments__eq'=>true,
)), VisibilityConstraint::HIDDEN));
}
return array(
'attachments' => new BooleanField(array(
'label'=>__('Enable Attachments'),
'default'=>$cfg->allowAttachments(),
'configuration'=>array(
'desc'=>__('Enables attachments, regardless of channel'),
'validators' => function($self, $value) {
if (!ini_get('file_uploads'))
$self->addError(__('The "file_uploads" directive is disabled in php.ini'));
}
+ $fileupload_config;
function isAttachmentsEnabled() {
$config = $this->getConfiguration();
return $config['attachments'];
}
}
class PriorityField extends ChoiceField {
function getWidget($widgetClass=false) {
$widget = parent::getWidget($widgetClass);
if ($widget->value instanceof Priority)
$widget->value = $widget->value->getId();
return $widget;
}
function hasIdValue() {
return true;
}
function getChoices($verbose=false) {
$sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE
.' ORDER BY priority_urgency DESC';
$choices = array('' => '— '.__('Default').' —');
if (!($res = db_query($sql)))
return $choices;
while ($row = db_fetch_row($res))
$choices[$row[0]] = $row[1];
return $choices;
}
function parse($id) {
return $this->to_php(null, $id);
}
function to_php($value, $id=false) {
if (is_array($id)) {
reset($id);
$id = key($id);
}
elseif (is_array($value))
list($value, $id) = $value;
elseif ($id === false)
$id = $value;
if ($id)
return Priority::lookup($id);
}
function to_database($prio) {
return ($prio instanceof Priority)
? array($prio->getDesc(), $prio->getId())
: $prio;
}
function toString($value) {
return ($value instanceof Priority) ? $value->getDesc() : $value;
}
function searchable($value) {
// Priority isn't searchable this way
return null;
}
function getConfigurationOptions() {
$choices = $this->getChoices();
$choices[''] = __('System Default');
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
'hint'=>__('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
'default' => new ChoiceField(array(
'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
'choices' => $choices,
'hint'=>__('Default selection for this field'),
'configuration'=>array('size'=>20, 'length'=>40),
)),
function getConfiguration() {
global $cfg;
$config = parent::getConfiguration();
if (!isset($config['default']))
$config['default'] = $cfg->getDefaultPriorityId();
return $config;
}
FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
'priority' => array(__('Priority Level'), PriorityField),
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
class DepartmentField extends ChoiceField {
function getWidget() {
$widget = parent::getWidget();
if ($widget->value instanceof Dept)
$widget->value = $widget->value->getId();
return $widget;
}
function hasIdValue() {
return true;
}
function getChoices() {
global $cfg;
$choices = array();
if (($depts = Dept::getDepartments()))
foreach ($depts as $id => $name)
$choices[$id] = $name;
return $choices;
}
function parse($id) {
return $this->to_php(null, $id);
}
function to_php($value, $id=false) {
if (is_array($id)) {
reset($id);
$id = key($id);
}
return $id;
}
function to_database($dept) {
return ($dept instanceof Dept)
? array($dept->getName(), $dept->getId())
: $dept;
}
function toString($value) {
return (string) $value;
}
function searchable($value) {
return null;
}
function getConfigurationOptions() {
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
'hint'=>__('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
);
}
}
FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
return array(
'department' => array(__('Department'), DepartmentField),
);
});
class AssigneeField extends ChoiceField {
var $_choices = array();
var $_criteria = null;
function getWidget() {
$widget = parent::getWidget();
if (is_object($widget->value))
$widget->value = $widget->value->getId();
return $widget;
}
function getCriteria() {
if (!isset($this->_criteria)) {
$this->_criteria = array('available' => true);
if (($c=parent::getCriteria()))
$this->_criteria = array_merge($this->_criteria, $c);
}
return $this->_criteria;
}
function hasIdValue() {
return true;
}
function getChoices() {
global $cfg;
if (!$this->_choices) {
$config = $this->getConfiguration();
$choices = array(
__('Agents') => new ArrayObject(),
__('Teams') => new ArrayObject());
$A = current($choices);
$criteria = $this->getCriteria();
$agents = array();
if (($dept=$config['dept']) && $dept->assignMembersOnly()) {
if (($members = $dept->getMembers($criteria)))
foreach ($members as $member)
$agents[$member->getId()] = $member;
} else {
$agents = Staff::getStaffMembers($criteria);
}
$A['s'.$id] = $name;
next($choices);
$T = current($choices);
if (($teams = Team::getTeams()))
foreach ($teams as $id => $name)
$T['t'.$id] = $name;
$this->_choices = $choices;
}
return $this->_choices;
function getValue() {
if (($value = parent::getValue()) && ($id=$this->getClean()))
return $value[$id];
}
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
function parse($id) {
return $this->to_php(null, $id);
}
function to_php($value, $id=false) {
if (is_array($id)) {
reset($id);
$id = key($id);
}
return $id;
}
function to_database($value) {
return (is_object($value))
? array($value->getName(), $value->getId())
: $value;
}
function toString($value) {
return (string) $value;
}
function searchable($value) {
return null;
}
function getConfigurationOptions() {
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
'hint'=>__('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
);
}
}
FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
return array(
'assignee' => array(__('Assignee'), AssigneeField),
);
});
class TicketStateField extends ChoiceField {
'name' => /* @trans, @context "ticket state name" */ 'Open',
'verb' => /* @trans, @context "ticket state action" */ 'Open'
'name' => /* @trans, @context "ticket state name" */ 'Closed',
'verb' => /* @trans, @context "ticket state action" */ 'Close'
// Private states
static $_privatestates = array(
'name' => /* @trans, @context "ticket state name" */ 'Archived',
'verb' => /* @trans, @context "ticket state action" */ 'Archive'
'name' => /* @trans, @context "ticket state name" */ 'Deleted',
'verb' => /* @trans, @context "ticket state action" */ 'Delete'
);
function hasIdValue() {
return true;
}
function isChangeable() {
return false;
}
function getChoices($verbose=false) {
static $_choices;
if (!isset($_choices)) {
// Translate and cache the choices
foreach (static::$_states as $k => $v)
$_choices[$k] = _P('ticket state name', $v['name']);
$this->ht['default'] = '';
}
return $_choices;
}
function getChoice($state) {
if ($state && is_array($state))
$state = key($state);
if (isset(static::$_states[$state]))
return _P('ticket state name', static::$_states[$state]['name']);
if (isset(static::$_privatestates[$state]))
return _P('ticket state name', static::$_privatestates[$state]['name']);
}
function getConfigurationOptions() {
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=> __('Prompt'), 'required'=>false, 'default'=>'',
'hint'=> __('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
);
}
static function getVerb($state) {
if (isset(static::$_states[$state]))
return _P('ticket state action', static::$_states[$state]['verb']);
if (isset(static::$_privatestates[$state]))
return _P('ticket state action', static::$_privatestates[$state]['verb']);
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
}
FormField::addFieldTypes('Dynamic Fields', function() {
return array(
'state' => array('Ticket State', TicketStateField, false),
);
});
class TicketFlagField extends ChoiceField {
// Supported flags (TODO: move to configurable custom list)
static $_flags = array(
'onhold' => array(
'flag' => 1,
'name' => 'Onhold',
'states' => array('open'),
),
'overdue' => array(
'flag' => 2,
'name' => 'Overdue',
'states' => array('open'),
),
'answered' => array(
'flag' => 4,
'name' => 'Answered',
'states' => array('open'),
)
);
var $_choices;
function hasIdValue() {
return true;
}
function isChangeable() {
return true;
}
function getChoices($verbose=false) {
Loading
Loading full blame...