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 {
static $renderer = 'GridFluidLayout';
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->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) {
// 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'];
$template = $options['template'] ?: 'dynamic-form.tmpl.php';
include(STAFFINC_DIR . 'templates/' . $template);
include(CLIENTINC_DIR . 'templates/' . $template);
function getLayout($title=false, $options=array()) {
$rc = @$options['renderer'] ?: static::$renderer;
return new $rc($title, $options);
}
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);
$dedup[$key] = true;
}
}
}
}
}
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
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);
}
class CustomForm extends SimpleForm {
function getFields() {
$options = $this->options;
$user = @$options['user'];
$isedit = ($options['mode'] == 'edit');
$fields = array();
foreach (parent::getFields() as $field) {
if ($isedit && !$field->isEditable($user))
continue;
$fields[] = $field;
}
return $fields;
}
}
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';
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' ?>">
<colgroup width="8.333333%"><col span="12"/></colgroup>
<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;
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) { ?>
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
}
?>
<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); ?>:
</label>
<?php }
if ($f->get('hint')) { ?>
<div class="field-hint-text">
<?php echo Format::htmlchars($f->get('hint')); ?>
</div>
<?php }
$f->render();
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]; }
}
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 (!isset($this->_clean) && ($d = $this->get('default')))
$this->_clean = $d;
if ($this->isVisible())
$this->validateEntry($this->_clean);
$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() {
if ($this->get('visibility') instanceof VisibilityConstraint) {
return $this->get('visibility')->isVisible($this);
}
return true;
}
function isEditable($user=null) {
if ($user instanceof EndUser)
$flag = DynamicFormField::FLAG_CLIENT_EDIT;
else
$flag = DynamicFormField::FLAG_AGENT_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');
}
/**
* 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);
}
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
/**
* 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; }
function setValue($value) {
$this->reset();
$this->getWidget()->value = $value;
}
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;
}
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
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;
}
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
/**
* 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) {
if (!static::$widget)
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,
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
)),
'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,
));
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
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);
}
}
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
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'];
}
function getWidget($widgetClass=false) {
if ($hint = $this->get('hint'))
$this->set('placeholder', $hint);
$this->set('hint', null);
$widget = parent::getWidget($widgetClass);
return $widget;
}
}
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),
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
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];
}
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
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']);
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
}
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) {
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
$this->ht['default'] = '';
if (!$this->_choices) {
foreach (static::$_flags as $k => $v)
$this->_choices[$k] = $v['name'];
}
return $this->_choices;
}
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('Dynamic Fields', function() {
return array(
'flags' => array('Ticket Flags', TicketFlagField, false),
);
});
class FileUploadField extends FormField {
static $widget = 'FileUploadWidget';
protected $attachments;
static function getFileTypes() {
static $filetypes;
if (!isset($filetypes)) {
if (function_exists('apc_fetch')) {
$key = md5(SECRET_SALT . GIT_VERSION . 'filetypes');
$filetypes = apc_fetch($key);
}
if (!$filetypes)
$filetypes = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml');
if ($key)
apc_store($key, $filetypes, 7200);
}
return $filetypes;
}
function getConfigurationOptions() {
// Compute size selections
$sizes = array('262144' => '— '.__('Small').' —');
$next = 512 << 10;
$max = strtoupper(ini_get('upload_max_filesize'));
$limit = (int) $max;
if (!$limit) $limit = 2 << 20; # 2M default value
elseif (strpos($max, 'K')) $limit <<= 10;
elseif (strpos($max, 'M')) $limit <<= 20;
elseif (strpos($max, 'G')) $limit <<= 30;
while ($next <= $limit) {
// Select the closest, larger value (in case the
// current value is between two)
$sizes[$next] = Format::file_size($next);
$next *= 2;
}
// Add extra option if top-limit in php.ini doesn't fall
// at a power of two
if ($next < $limit * 2)
$sizes[$limit] = Format::file_size($limit);
foreach (self::getFileTypes() as $type=>$info) {
$types[$type] = $info['description'];
}
return array(
'size' => new ChoiceField(array(
'label'=>__('Maximum File Size'),
'hint'=>__('Choose maximum size of a single file uploaded to this field'),
'default'=>$cfg->getMaxFileSize(),
'mimetypes' => new ChoiceField(array(
'label'=>__('Restrict by File Type'),
'hint'=>__('Optionally, choose acceptable file types.'),
'required'=>false,
'choices'=>$types,
'configuration'=>array('multiselect'=>true,'prompt'=>__('No restrictions'))
'extensions' => new TextareaField(array(
'label'=>__('Additional File Type Filters'),
'hint'=>__('Optionally, enter comma-separated list of additional file types, by extension. (e.g .doc, .pdf).'),
'configuration'=>array('html'=>false, 'rows'=>2),
)),
'max' => new TextboxField(array(
'label'=>__('Maximum Files'),
'hint'=>__('Users cannot upload more than this many files.'),
'default'=>false,
'required'=>false,
'validator'=>'number',
'configuration'=>array('size'=>8, 'length'=>4, 'placeholder'=>__('No limit')),
function hasSpecialSearch() {
return false;
}
/**
* Called from the ajax handler for async uploads via web clients.
*/
function ajaxUpload($bypass=false) {
$config = $this->getConfiguration();
$files = AttachmentFile::format($_FILES['upload'],
// For numeric fields assume configuration exists
!is_numeric($this->get('id')));
if (count($files) != 1)
Http::response(400, 'Send one file at a time');
$file = array_shift($files);
$file['name'] = urldecode($file['name']);
if (!$bypass && !$this->isValidFileType($file['name'], $file['type']))
Http::response(415, 'File type is not allowed');
$config = $this->getConfiguration();
if (!$bypass && $file['size'] > $config['size'])
Http::response(413, 'File is too large');
if (!($F = AttachmentFile::upload($file)))
Http::response(500, 'Unable to store file: '. $file['error']);
return $F->getId();
/**
* Called from FileUploadWidget::getValue() when manual upload is used
* for browsers which do not support the HTML5 way of uploading async.
*/
function uploadFile($file) {
if (!$this->isValidFileType($file['name'], $file['type']))
throw new FileUploadError(__('File type is not allowed'));
$config = $this->getConfiguration();
if ($file['size'] > $config['size'])
throw new FileUploadError(__('File size is too large'));
return AttachmentFile::upload($file);
}
/**
* Called from API and email routines and such to handle attachments
* sent other than via web upload
*/
function uploadAttachment(&$file) {
if (!$this->isValidFileType($file['name'], $file['type']))
throw new FileUploadError(__('File type is not allowed'));
if (is_callable($file['data']))
$file['data'] = $file['data']();
if (!isset($file['size'])) {
// bootstrap.php include a compat version of mb_strlen
if (extension_loaded('mbstring'))
$file['size'] = mb_strlen($file['data'], '8bit');
else
$file['size'] = strlen($file['data']);
}
$config = $this->getConfiguration();
if ($file['size'] > $config['size'])
throw new FileUploadError(__('File size is too large'));
if (!$F = AttachmentFile::create($file))
throw new FileUploadError(__('Unable to save file'));
return $F;
}
function isValidFileType($name, $type=false) {
$config = $this->getConfiguration();
// Check MIME type - file ext. shouldn't be solely trusted.
if ($type && $config['__mimetypes']
&& in_array($type, $config['__mimetypes']))
// Return true if all file types are allowed (.*)
if (!$config['__extensions'] || in_array('.*', $config['__extensions']))
$allowed = $config['__extensions'];
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
return ($ext && is_array($allowed) && in_array(".$ext", $allowed));
}
function getFiles() {
if (!isset($this->attachments) && ($a = $this->getAnswer())
&& ($e = $a->getEntry()) && ($e->get('id'))
) {
$this->attachments = GenericAttachments::forIdAndType(
// Combine the field and entry ids to make the key
sprintf('%u', crc32('E'.$this->get('id').$e->get('id'))),
'E');
}
return $this->attachments ?: array();
function setAttachments(GenericAttachments $att) {
$this->attachments = $att;
}
function getConfiguration() {
$config = parent::getConfiguration();
$_types = self::getFileTypes();
$mimetypes = array();
$extensions = array();
if (isset($config['mimetypes']) && is_array($config['mimetypes'])) {
foreach ($config['mimetypes'] as $type=>$desc) {
foreach ($_types[$type]['types'] as $mime=>$exts) {
$mimetypes[$mime] = true;
if (is_array($exts))
foreach ($exts as $ext)
$extensions['.'.$ext] = true;
}
}
}
if (strpos($config['extensions'], '.*') !== false)
$config['extensions'] = '';
if (is_string($config['extensions'])) {
foreach (preg_split('/\s+/', str_replace(',',' ', $config['extensions'])) as $ext) {
if (!$ext) {
continue;
}
elseif (strpos($ext, '/')) {
}
else {
if ($ext[0] != '.')
$ext = '.' . $ext;
// Ensure that the extension is lower-cased for comparison latr
$ext = strtolower($ext);
// Add this to the MIME types list so it can be exported to
// the @accept attribute
if (!isset($extensions[$ext]))
$mimetypes[$ext] = true;
$extensions[$ext] = true;
}
$config['__extensions'] = array_keys($extensions);
}
elseif (is_array($config['extensions'])) {
$config['__extensions'] = $config['extensions'];
}
// 'mimetypes' is the array represented from the user interface,
// '__mimetypes' is a complete list of supported MIME types.
$config['__mimetypes'] = array_keys($mimetypes);
return $config;
}
// When the field is saved to database, encode the ID listing as a json
// array. Then, inspect the difference between the files actually
// attached to this field
function to_database($value) {
$this->getFiles();
if (isset($this->attachments)) {
$this->attachments->keepOnlyFileIds($value);
}
return JsonDataEncoder::encode($value);
}
function parse($value) {
// Values in the database should be integer file-ids
return array_map(function($e) { return (int) $e; },
$value ?: array());
}
function to_php($value) {
return JsonDataParser::decode($value);
}
function to_config($value) {
if ($value && is_array($value))
$value = array_values($value);
return $value;
}
function display($value) {
$links = array();
foreach ($this->getFiles() as $f) {
$links[] = sprintf('<a class="no-pjax" href="%s">%s</a>',
Format::htmlchars($f->file->getDownloadUrl()),
Format::htmlchars($f->file->name));
}
return implode('<br/>', $links);
}
function toString($value) {
$files = array();
foreach ($this->getFiles() as $f) {
}
return implode(', ', $files);
}
function db_cleanup($field=false) {
// Delete associated attachments from the database, if any
$this->getFiles();
if (isset($this->attachments)) {
$this->attachments->deleteAll();
}
}
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
function asVar($value, $id=false) {
return new FileFieldAttachments($this->getFiles());
}
function asVarType() {
return 'FileFieldAttachments';
}
}
class FileFieldAttachments {
var $files;
function __construct($files) {
$this->files = $files;
}
function __toString() {
$files = array();
foreach ($this->files as $f) {
$files[] = $f->file->name;
}
return implode(', ', $files);
}
function getVar($tag) {
switch ($tag) {
case 'names':
return $this->__toString();
case 'files':
throw new OOBContent(OOBContent::FILES, $this->files->all());
}
}
static function getVarScope() {
return array(
'names' => __('List of file names'),
'files' => __('Attached files'),
);
}
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
class InlineFormData extends ArrayObject {
var $_form;
function __construct($form, array $data=array()) {
parent::__construct($data);
$this->_form = $form;
}
function getVar($tag) {
foreach ($this->_form->getFields() as $f) {
if ($f->get('name') == $tag)
return $this[$f->get('id')];
}
}
}
class InlineFormField extends FormField {
static $widget = 'InlineFormWidget';
var $_iform = null;
function validateEntry($value) {
if (!$this->getInlineForm()->isValid()) {
$this->_errors[] = __('Correct errors in the inline form');
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
}
}
function parse($value) {
// The InlineFieldWidget returns an array of cleaned data
return $value;
}
function to_database($value) {
return JsonDataEncoder::encode($value);
}
function to_php($value) {
$data = JsonDataParser::decode($value);
// The InlineFormData helps with the variable replacer API
return new InlineFormData($this->getInlineForm(), $data);
}
function display($data) {
$form = $this->getInlineForm();
ob_start(); ?>
<div><?php
foreach ($form->getFields() as $field) { ?>
<span style="display:inline-block;padding:0 5px;vertical-align:top">
<strong><?php echo Format::htmlchars($field->get('label')); ?></strong>
<div><?php
$value = $data[$field->get('id')];
echo $field->display($value); ?></div>
</span><?php
} ?>
</div><?php
return ob_get_clean();
}
function getInlineForm($data=false) {
$form = $this->get('form');
if (is_array($form)) {
$form = new SimpleForm($form, $data ?: $this->value ?: $this->getSource());
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
}
return $form;
}
}
class InlineDynamicFormField extends FormField {
function getInlineForm($data=false) {
if (!isset($this->_iform) || $data) {
$config = $this->getConfiguration();
$this->_iform = DynamicForm::lookup($config['form']);
if ($data)
$this->_iform = $this->_iform->getForm($data);
}
return $this->_iform;
}
function getConfigurationOptions() {
$forms = DynamicForm::objects()->filter(array('type'=>'G'))
->values_flat('id', 'title');
$choices = array();
foreach ($forms as $row) {
list($id, $title) = $row;
$choices[$id] = $title;
}
return array(
'form' => new ChoiceField(array(
'id'=>2, 'label'=>'Inline Form', 'required'=>true,
'default'=>'', 'choices'=>$choices
)),
);
}
}
class InlineFormWidget extends Widget {
function render($mode=false) {
$form = $this->field->getInlineForm();
if (!$form)
return;
// Handle first-step edits -- load data from $this->value
if ($form instanceof DynamicForm && !$form->getSource())
$form = $form->getForm($this->value);
$inc = ($mode == 'client') ? CLIENTINC_DIR : STAFFINC_DIR;
include $inc . 'templates/inline-form.tmpl.php';
}
function getValue() {
$data = $this->field->getSource();
if (!$data)
return null;
$form = $this->field->getInlineForm($data);
if (!$form)
return null;
return $form->getClean();
}
}
function __construct($field) {
$this->field = $field;
$this->name = $field->getFormName();
$this->id = '_' . $this->name;
$this->value = $this->getValue();
if (!isset($this->value) && is_object($this->field->getAnswer()))
$this->value = $this->field->getAnswer()->getValue();
if (!isset($this->value) && isset($this->field->value))
$data = $this->field->getSource();
// Search for HTML form name first
if (isset($data[$this->name]))
return $data[$this->name];
elseif (isset($data[$this->field->get('name')]))
return $data[$this->field->get('name')];
elseif (isset($data[$this->field->get('id')]))
return $data[$this->field->get('id')];
/**
* getJsValueGetter
*
* Used with the dependent fields feature, this function should return a
* single javascript expression which can be used in a larger expression
* (<> == true, where <> is the result of this function). The %s token
* will be replaced with a jQuery variable representing this widget.
*/
function getJsValueGetter() {
return '%s.val()';
}
}
class TextboxWidget extends Widget {
function render($options=array(), $extraConfig=false) {
if (is_array($extraConfig)) {
foreach ($extraConfig as $k=>$v)
if (!isset($config[$k]) || !$config[$k])
$config[$k] = $v;
}
if (isset($config['size']))
$size = "size=\"{$config['size']}\"";
if (isset($config['length']) && $config['length'])
$maxlength = "maxlength=\"{$config['length']}\"";
if (isset($config['classes']))
$classes = 'class="'.$config['classes'].'"';
if (isset($config['autocomplete']))
$autocomplete = 'autocomplete="'.($config['autocomplete']?'on':'off').'"';
if (isset($config['autofocus']))
$autofocus = 'autofocus';
if (isset($config['disabled']))
$disabled = 'disabled="disabled"';
if (isset($config['translatable']) && $config['translatable'])
$translatable = 'data-translate-tag="'.$config['translatable'].'"';
$type = static::$input_type;
$types = array(
'email' => 'email',
'phone' => 'tel',
);
if ($type == 'text' && isset($types[$config['validator']]))
$type = $types[$config['validator']];
$placeholder = sprintf('placeholder="%s"', $this->field->getLocal('placeholder',
$config['placeholder']));
<input type="<?php echo $type; ?>"
id="<?php echo $this->id; ?>"
<?php echo implode(' ', array_filter(array(
$size, $maxlength, $classes, $autocomplete, $disabled,
$translatable, $placeholder, $autofocus))); ?>
name="<?php echo $this->name; ?>"
value="<?php echo Format::htmlchars($this->value); ?>"/>
<?php
}
}
class TextboxSelectionWidget extends TextboxWidget {
//TODO: Support multi-input e.g comma separated inputs
function render($options=array()) {
if ($this->value && is_array($this->value))
$this->value = current($this->value);
parent::render($options);
}
function getValue() {
$value = parent::getValue();
if ($value && ($item=$this->field->lookupChoice((string) $value)))
$value = $item;
return $value;
}
}
class PasswordWidget extends TextboxWidget {
static $input_type = 'password';
function render($mode=false, $extra=false) {
$extra = array();
if ($this->field->value) {
$extra['placeholder'] = '••••••••••••';
}
return parent::render($mode, $extra);
}
if ($_SERVER['REQUEST_METHOD'] != 'POST'
|| !$this->field->getForm()->isValid())
$class = $cols = $rows = $maxlength = "";
if (isset($config['rows']))
$rows = "rows=\"{$config['rows']}\"";
if (isset($config['cols']))
$cols = "cols=\"{$config['cols']}\"";
if (isset($config['length']) && $config['length'])
$maxlength = "maxlength=\"{$config['length']}\"";
if (isset($config['html']) && $config['html']) {
$class = array('richtext', 'no-bar');
$class[] = @$config['size'] ?: 'small';
$class = sprintf('class="%s"', implode(' ', $class));
$this->value = Format::viewableImages($this->value);
}
if (isset($config['context']))
$attrs['data-root-context'] = '"'.$config['context'].'"';
<span style="display:inline-block;width:100%">
<textarea <?php echo $rows." ".$cols." ".$maxlength." ".$class
.' '.Format::array_implode('=', ' ', $attrs)
.' placeholder="'.$config['placeholder'].'"'; ?>
id="<?php echo $this->id; ?>"
name="<?php echo $this->name; ?>"><?php
echo Format::htmlchars($this->value);
?></textarea>
</span>
<?php
}
}
class PhoneNumberWidget extends Widget {
$config = $this->field->getConfiguration();
list($phone, $ext) = explode("X", $this->value);
?>
<input id="<?php echo $this->id; ?>" type="tel" name="<?php echo $this->name; ?>" value="<?php
echo Format::htmlchars($phone); ?>"/><?php
// Allow display of extension field even if disabled if the phone
// number being edited has an extension
if ($ext || $config['ext']) { ?> <?php echo __('Ext'); ?>:
echo $this->name; ?>-ext" value="<?php echo Format::htmlchars($ext);
?>" size="5"/>
$data = $this->field->getSource();
$base = parent::getValue();
if ($base === null)
return $base;
$ext = $data["{$this->name}-ext"];
// NOTE: 'X' is significant. Don't change it
}
}
class ChoicesWidget extends Widget {
function render($options=array()) {
$mode = isset($options['mode']) ? $options['mode'] : null;
if ($mode == 'view') {
if (!($val = (string) $this->field))
$val = sprintf('<span class="faded">%s</span>', __('None'));
echo $val;
return;
}
if ($mode == 'search') {
$config['multiselect'] = true;
}
// Determine the value for the default (the one listed if nothing is
// selected)
$prompt = ($config['prompt'])
? $this->field->getLocal('prompt', $config['prompt'])
: __('Select'
/* Used as a default prompt for a custom drop-down list */);
// We don't consider the 'default' when rendering in 'search' mode
if (!strcasecmp($mode, 'search')) {
$def_val = $prompt;
} else {
$def_key = $this->field->get('default');
if (!$def_key && $config['default'])
$def_key = $config['default'];
if (is_array($def_key))
$def_key = key($def_key);
$have_def = isset($choices[$def_key]);
$def_val = $have_def ? $choices[$def_key] : $prompt;
$values = $this->value;
if (!is_array($values) && isset($values)) {
$values = array($values => $this->field->getChoice($values));
}
$values = $have_def ? array($def_key => $choices[$def_key]) : array();
if (isset($config['classes']))
$classes = 'class="'.$config['classes'].'"';
?>
<select name="<?php echo $this->name; ?>[]"
<?php echo implode(' ', array_filter(array($classes))); ?>
id="<?php echo $this->id; ?>"
<?php if (isset($config['data']))
foreach ($config['data'] as $D=>$V)
echo ' data-'.$D.'="'.Format::htmlchars($V).'"';
?>
data-placeholder="<?php echo $prompt; ?>"
<?php if ($config['multiselect'])
<?php if (!$have_def && !$config['multiselect']) { ?>
<option value="<?php echo $def_key; ?>">— <?php
echo $def_val; ?> —</option>
$this->emitChoices($choices, $values, $have_def, $def_key); ?>
if ($config['multiselect']) {
?>
<script type="text/javascript">
$(function() {
.select2({'minimumResultsForSearch':10, 'width': '350px'});
});
</script>
<?php
}
function emitChoices($choices, $values=array(), $have_def=false, $def_key=null) {
reset($choices);
if (is_array(current($choices)) || current($choices) instanceof Traversable)
return $this->emitComplexChoices($choices, $values, $have_def, $def_key);
foreach ($choices as $key => $name) {
if (!$have_def && $key == $def_key)
continue; ?>
<option value="<?php echo $key; ?>" <?php
if (isset($values[$key])) echo 'selected="selected"';
?>><?php echo $name; ?></option>
<?php
}
}
function emitComplexChoices($choices, $values=array(), $have_def=false, $def_key=null) {
foreach ($choices as $label => $group) { ?>
<optgroup label="<?php echo $label; ?>"><?php
foreach ($group as $key => $name) {
if (!$have_def && $key == $def_key)
continue; ?>
<option value="<?php echo $key; ?>" <?php
if (isset($values[$key])) echo 'selected="selected"';
?>><?php echo $name; ?></option>
<?php } ?>
</optgroup><?php
}
}
function getValue() {
if (!($value = parent::getValue()))
return null;
if ($value && !is_array($value))
$value = array($value);
// Assume multiselect
$values = array();
$choices = $this->field->getChoices();
if ($choices && is_array($value)) {
// Complex choices
if (is_array(current($choices))
|| current($choices) instanceof Traversable) {
foreach ($choices as $label => $group) {
foreach ($group as $k => $v)
if (in_array($k, $value))
$values[$k] = $v;
}
} else {
foreach($value as $k => $v) {
if (isset($choices[$v]))
$values[$v] = $choices[$v];
elseif (($i=$this->field->lookupChoice($v)))
$values += $i;
}
return $values;
}
function getJsValueGetter() {
return '%s.find(":selected").val()';
}
/**
* A widget for the ChoiceField which will render a list of radio boxes or
* checkboxes depending on the value of $config['multiple']. Complex choices
* are also supported and will be rendered as divs.
*/
class BoxChoicesWidget extends Widget {
function render($options=array()) {
$this->emitChoices($this->field->getChoices());
}
function emitChoices($choices) {
if (!isset($this->value))
$this->value = $this->field->get('default');
$config = $this->field->getConfiguration();
$type = $config['multiple'] ? 'checkbox' : 'radio';
if (isset($config['classes']))
$classes = 'class="'.$config['classes'].'"';
foreach ($choices as $k => $v) {
if (is_array($v)) {
$this->renderSectionBreak($k);
$this->emitChoices($v);
continue;
}
?>
<label <?php echo $classes; ?>
for="<?php echo $id; ?>" style="display:block;">
<input id="<?php echo $id; ?>" style="vertical-align:top;"
type="<?php echo $type; ?>" name="<?php echo $this->name; ?>[]" <?php
if ($this->value[$k]) echo 'checked="checked"'; ?> value="<?php
echo Format::htmlchars($k); ?>"/>
<?php
if ($v) { ?>
<em style="display:inline-block;vertical-align:baseline;width:90%;width:calc(100% - 25px);"><?php
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
echo Format::viewableImages($v); ?></em>
<?php } ?>
</label>
<?php }
}
function renderSectionBreak($label) { ?>
<div><?php echo Format::htmlchars($label); ?></div>
<?php
}
function getValue() {
$data = $this->field->getSource();
if (count($data)) {
if (!isset($data[$this->name]))
return array();
return $this->collectValues($data[$this->name], $this->field->getChoices());
}
return parent::getValue();
}
function collectValues($data, $choices) {
$value = array();
foreach ($choices as $k => $v) {
if (is_array($v))
$value = array_merge($value, $this->collectValues($data, $v));
elseif (@in_array($k, $data))
$value[$k] = $v;
}
return $value;
}
}
/**
* An extension to the BoxChoicesWidget which will render complex choices in
* tabs.
*/
class TabbedBoxChoicesWidget extends BoxChoicesWidget {
function render($options=array()) {
$tabs = array();
foreach ($this->field->getChoices() as $label=>$group) {
if (is_array($group)) {
$tabs[$label] = $group;
}
else {
$this->emitChoices(array($label=>$group));
}
}
if ($tabs) {
?>
<div>
<ul class="alt tabs">
<?php $i = 0;
foreach ($tabs as $label => $group) {
$active = $i++ == 0; ?>
<li <?php if ($active) echo 'class="active"';
?>><a href="#<?php echo sprintf('%s-%s', $this->name, Format::slugify($label));
?>"><?php echo Format::htmlchars($label); ?></a></li>
<?php } ?>
</ul>
<?php $i = 0;
foreach ($tabs as $label => $group) {
$first = $i++ == 0; ?>
<div class="tab_content <?php if (!$first) echo 'hidden'; ?>" id="<?php
echo sprintf('%s-%s', $this->name, Format::slugify($label));?>">
<?php $this->emitChoices($group); ?>
</div>
<?php } ?>
</div>
<?php }
}
}
class CheckboxWidget extends Widget {
function __construct($field) {
parent::__construct($field);
$this->name = '_field-checkboxes';
}
if (!isset($this->value))
$this->value = $this->field->get('default');
if (isset($config['classes']))
$classes = 'class="'.$config['classes'].'"';
<div <?php echo implode(' ', array_filter(array($classes))); ?>>
<label class="checkbox">
<input id="<?php echo $this->id; ?>"
type="checkbox" name="<?php echo $this->name; ?>[]" <?php
if ($this->value) echo 'checked="checked"'; ?> value="<?php
echo $this->field->get('id'); ?>"/>
<?php
if ($config['desc']) {
echo Format::viewableImages($config['desc']);
$data = $this->field->getSource();
if (count($data)) {
if (!isset($data[$this->name]))
return @in_array($this->field->get('id'), $data[$this->name]);
function getJsValueGetter() {
return '%s.is(":checked")';
}
class DatetimePickerWidget extends Widget {
$config = $this->field->getConfiguration();
if ($this->value) {
$this->value = is_int($this->value) ? $this->value :
strtotime($this->value);
if ($config['gmt']) {
// Convert to GMT time
$tz = new DateTimeZone($cfg->getTimezone());
$D = DateTime::createFromFormat('U', $this->value);
$this->value += $tz->getOffset($D);
}
list($hr, $min) = explode(':', date('H:i', $this->value));
$this->value = Format::date($this->value, false, false, 'UTC');
}
?>
<input type="text" name="<?php echo $this->name; ?>"
id="<?php echo $this->id; ?>" style="display:inline-block;width:auto"
value="<?php echo Format::htmlchars($this->value); ?>" size="12"
autocomplete="off" class="dp" />
<script type="text/javascript">
$(function() {
$('input[name="<?php echo $this->name; ?>"]').datepicker({
<?php
if ($config['min'])
echo "minDate: new Date({$config['min']}000),";
if ($config['max'])
echo "maxDate: new Date({$config['max']}000),";
elseif (!$config['future'])
echo "maxDate: new Date().getTime(),";
?>
numberOfMonths: 2,
showButtonPanel: true,
buttonImage: './images/cal.png',
dateFormat: $.translate_format('<?php echo $cfg->getDateFormat(true); ?>')
});
});
</script>
<?php
if ($config['time'])
// TODO: Add time picker -- requires time picker or selection with
// Misc::timeDropdown
echo ' ' . Misc::timeDropdown($hr, $min, $this->name . ':time');
}
/**
* Function: getValue
* Combines the datepicker date value and the time dropdown selected
* time value into a single date and time string value.
*/
function getValue() {
$data = $this->field->getSource();
$config = $this->field->getConfiguration();
if ($datetime = parent::getValue()) {
$datetime = is_int($datetime) ? $datetime :
strtotime($datetime);
if ($datetime && isset($data[$this->name . ':time'])) {
list($hr, $min) = explode(':', $data[$this->name . ':time']);
$datetime += $hr * 3600 + $min * 60;
}
if ($datetime && $config['gmt']) {
// Convert to GMT time
$tz = new DateTimeZone($cfg->getTimezone());
$D = DateTime::createFromFormat('U', $datetime);
$datetime -= $tz->getOffset($D);
}
class SectionBreakWidget extends Widget {
?><div class="form-header section-break"><h3><?php
echo Format::htmlchars($this->field->getLocal('label'));
?></h3><em><?php echo Format::htmlchars($this->field->getLocal('hint'));
?></em></div>
<?php
}
}
class ThreadEntryWidget extends Widget {
$object_id = false;
if ($options['client']) {
$namespace = $options['draft-namespace']
?: 'ticket.client';
$object_id = substr(session_id(), -12);
} else {
$namespace = $options['draft-namespace'] ?: 'ticket.staff';
list($draft, $attrs) = Draft::getDraftAndDataAttrs($namespace, $object_id, $this->value);
<textarea style="width:100%;" name="<?php echo $this->field->get('name'); ?>"
placeholder="<?php echo Format::htmlchars($this->field->get('placeholder')); ?>"
class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
?> draft draft-delete" <?php echo $attrs; ?>
cols="21" rows="8" style="width:80%;"><?php echo
$draft ?: Format::htmlchars($this->value); ?></textarea>
$config = $this->field->getConfiguration();
if (!$config['attachments'])
return;
$attachments = $this->getAttachments($config);
print $attachments->render($options);
foreach ($attachments->getMedia() as $type=>$urls) {
foreach ($urls as $url)
Form::emitMedia($url, $type);
function getAttachments($config=false) {
if (!$config)
$config = $this->field->getConfiguration();
$field = new FileUploadField(array(
'id'=>'attach',
'name'=>'attach:' . $this->field->get('id'),
'configuration'=>$config)
);
$field->setForm($this->field->getForm());
return $field;
class FileUploadWidget extends Widget {
static $media = array(
'css' => array(
'/css/filedrop.css',
),
);
$config = $this->field->getConfiguration();
$name = $this->field->getFormName();
$id = substr(md5(spl_object_hash($this)), 10);
$attachments = $this->field->getFiles();
$mimetypes = array_filter($config['__mimetypes'],
function($t) { return strpos($t, '/') !== false; }
);
$maxfilesize = ($config['size'] ?: 1048576) / 1048576;
$files = $F = array();
$new = array_fill_keys($this->field->getClean(), 1);
foreach ($attachments as $f) {
$F[] = $f->file;
}
// Add in newly added files not yet saved (if redisplaying after an
// error)
if ($new) {
$F = array_merge($F, AttachmentFile::objects()->filter(array(
'id__in' => array_keys($new)))->all());
}
foreach ($F as $file) {
$files[] = array(
'id' => $file->getId(),
'name' => $file->getName(),
'type' => $file->getType(),
'size' => $file->getSize(),
'download_url' => $file->getDownloadUrl(),
);
?><div id="<?php echo $id;
?>" class="filedrop"><div class="files"></div>
<div class="dropzone"><i class="icon-upload"></i>
<?php echo sprintf(
__('Drop files here or %s choose them %s'),
'<a href="#" class="manual">', '</a>'); ?>
<input type="file" multiple="multiple"
id="file-<?php echo $id; ?>" style="display:none;"
accept="<?php echo implode(',', $config['__mimetypes']); ?>"/>
$(function(){$('#<?php echo $id; ?> .dropzone').filedropbox({
url: 'ajax.php/form/upload/<?php echo $this->field->get('id') ?>',
link: $('#<?php echo $id; ?>').find('a.manual'),
fallback_id: 'file-<?php echo $id; ?>',
allowedfileextensions: <?php echo JsonDataEncoder::encode(
$config['__extensions'] ?: array()); ?>,
allowedfiletypes: <?php echo JsonDataEncoder::encode(
maxfiles: <?php echo $config['max'] ?: 20; ?>,
maxfilesize: <?php echo $maxfilesize; ?>,
name: '<?php echo $name; ?>[]',
files: <?php echo JsonDataEncoder::encode($files); ?>
});});
</script>
<?php
}
function getValue() {
$ids = array();
// Handle manual uploads (IE<10)
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES[$this->name])) {
foreach (AttachmentFile::format($_FILES[$this->name]) as $file) {
$F = $this->field->uploadFile($file);
$ids[] = $F->getId();
// If no value was sent, assume an empty list
$base = parent::getValue();
if (!$base)
if (is_array($base)) {
foreach ($base as $info) {
@list($id, $name) = explode(',', $info, 2);
// Keep the values as the IDs
if ($name)
$ids[$name] = $id;
else
$ids[] = $id;
}
}
class FileUploadError extends Exception {}
class FreeTextField extends FormField {
static $widget = 'FreeTextWidget';
function getConfigurationOptions() {
return array(
'content' => new TextareaField(array(
'configuration' => array('html' => true, 'size'=>'large'),
'label'=>__('Content'), 'required'=>true, 'default'=>'',
'hint'=>__('Free text shown in the form, such as a disclaimer'),
)),
'attachments' => new FileUploadField(array(
'id'=>'attach',
'name'=>'files',
'configuration' => array('extensions'=>'')
)),
);
}
function hasData() {
return false;
}
function isBlockLevel() {
return true;
}
/* utils */
function to_config($config) {
$keepers = array();
if ($config && isset($config['attachments']))
foreach ($config['attachments'] as $fid)
$keepers[] = $fid;
$this->getAttachments()->keepOnlyFileIds($keepers);
return $config;
}
function db_cleanup($field=false) {
$this->getAttachments()->deleteAll();
}
function getAttachments() {
if (!isset($this->attachments))
$this->attachments = GenericAttachments::forIdAndType($this->get('id'), 'I');
return $this->attachments;
}
function getFiles() {
if (!($attachments = $this->getAttachments()))
return array();
return $attachments->all();
}
}
class FreeTextWidget extends Widget {
$class = $config['classes'] ?: 'thread-body'
?><div class="<?php echo $class; ?>" style="padding:0"><?php
if ($label = $this->field->getLocal('label')) { ?>
<h3><?php
echo Format::htmlchars($label);
?></h3><?php
}
if ($hint = $this->field->getLocal('hint')) { ?>
<em><?php
echo Format::htmlchars($hint);
?></em><?php
} ?>
<div><?php
echo Format::viewableImages($config['content']); ?></div>
</div>
<?php
if (($attachments=$this->field->getFiles())) { ?>
<section class="freetext-files">
<div class="title"><?php echo __('Related Resources'); ?></div>
<?php foreach ($attachments as $attach) { ?>
<a href="<?php echo $attach->file->getDownloadUrl(); ?>"
target="_blank" download="<?php echo $attach->file->getDownloadUrl();
?>" class="truncate no-pjax">
<?php echo Format::htmlchars($attach->getFilename()); ?>
</a>
</div>
<?php } ?>
</section>
<?php }
class VisibilityConstraint {
const HIDDEN = 0x0001;
const VISIBLE = 0x0002;
var $initial;
var $constraint;
function __construct($constraint, $initial=self::VISIBLE) {
$this->constraint = $constraint;
$this->initial = $initial;
}
function emitJavascript($field) {
$func = 'recheck';
$form = $field->getForm();
?>
<script type="text/javascript">
!(function() {
var <?php echo $func; ?> = function() {
var target = $('#field<?php echo $field->getWidget()->id; ?>');
<?php $fields = $this->getAllFields($this->constraint);
foreach ($fields as $f) {
$field = $form->getField($f);
echo sprintf('var %1$s = $("#%1$s");',
}
$expression = $this->compileQ($this->constraint, $form);
?>
if (<?php echo $expression; ?>)
$(this).trigger('show');
});
else
target.slideUp('fast', function (){
$(this).trigger('hide');
});
};
<?php foreach ($fields as $f) {
$w = $form->getField($f)->getWidget();
?>
$('#<?php echo $w->id; ?>').on('change', <?php echo $func; ?>);
$('#field<?php echo $w->id; ?>').on('show hide', <?php
echo $func; ?>);
<?php } ?>
})();
</script><?php
}
/**
* Determines if the field was visible when the form was submitted
*/
function isVisible($field) {
return $this->compileQPhp($this->constraint, $field);
}
function compileQPhp(Q $Q, $field) {
if (!($form = $field->getForm())) {
return $this->initial == self::VISIBLE;
}
$expr = array();
foreach ($Q->constraints as $c=>$value) {
if ($value instanceof Q) {
$expr[] = $this->compileQPhp($value, $field);
}
else {
@list($f, $op) = explode('__', $c, 2);
$field = $form->getField($f);
$wval = $field->getClean();
switch ($op) {
case 'eq':
case null:
$expr[] = ($wval == $value && $field->isVisible());
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
}
}
}
$glue = $Q->isOred()
? function($a, $b) { return $a || $b; }
: function($a, $b) { return $a && $b; };
$initial = !$Q->isOred();
$expression = array_reduce($expr, $glue, $initial);
if ($Q->isNegated)
$expression = !$expression;
return $expression;
}
function getAllFields(Q $Q, &$fields=array()) {
foreach ($Q->constraints as $c=>$value) {
if ($c instanceof Q) {
$this->getAllFields($c, $fields);
}
else {
list($f, $op) = explode('__', $c, 2);
$fields[$f] = true;
}
}
return array_keys($fields);
}
function compileQ($Q, $form) {
$expr = array();
foreach ($Q->constraints as $c=>$value) {
if ($value instanceof Q) {
$expr[] = $this->compileQ($value, $form);
}
else {
list($f, $op) = explode('__', $c, 2);
$widget = $form->getField($f)->getWidget();
$id = $widget->id;
$expr[] = sprintf('(%s.is(":visible") && %s)',
$id,
sprintf('%s == %s',
sprintf($widget->getJsValueGetter(), $id),
JsonDataEncoder::encode($value))
);
}
}
}
$glue = $Q->isOred() ? ' || ' : ' && ';
$expression = implode($glue, $expr);
if (count($expr) > 1)
$expression = '('.$expression.')';
if ($Q->isNegated)
$expression = '!'.$expression;
return $expression;
}
}
class AssignmentForm extends Form {
static $id = 'assign';
var $_assignee = null;
function __construct($source=null, $options=array()) {
parent::__construct($source, $options);
// Department of the object -- if necessary to limit assinees
if (isset($options['dept']))
$this->_dept = $options['dept'];
}
function getFields() {
if ($this->fields)
return $this->fields;
$fields = array(
'assignee' => new AssigneeField(array(
'id'=>1,
'label' => __('Assignee'),
'flags' => hexdec(0X450F3),
'required' => true,
'validator-error' => __('Assignee selection required'),
'configuration' => array(
'criteria' => array(
'available' => true,
),
'dept' => $this->_dept ?: null,
),
)
),
'comments' => new TextareaField(array(
'id' => 2,
'label'=> '',
'required'=>false,
'default'=>'',
'configuration' => array(
'html' => true,
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
'placeholder' => __('Optional reason for the assignment'),
),
)
),
);
$this->setFields($fields);
return $this->fields;
}
function isValid() {
if (!parent::isValid())
return false;
// Do additional assignment validation
if (!($assignee = $this->getAssignee())) {
$this->getField('assignee')->addError(
__('Unknown assignee'));
} elseif ($assignee instanceof Staff) {
// Make sure the agent is available
if (!$assignee->isAvailable())
$this->getField('assignee')->addError(
__('Agent is unavailable for assignment')
);
}
return !$this->errors();
}
function render($options) {
switch(strtolower($options['template'])) {
case 'simple':
$inc = STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
break;
default:
throw new Exception(sprintf(__('%s: Unknown template style %s'),
'FormUtils', $options['template']));
}
$form = $this;
include $inc;
}
function getAssignee() {
if (!isset($this->_assignee)) {
$value = $this->getField('assignee')->getClean();
if ($value[0] == 's')
$this->_assignee = Staff::lookup(substr($value, 1));
elseif ($value[0] == 't')
$this->_assignee = Team::lookup(substr($value, 1));
}
return $this->_assignee;
}
function assigneeCriteria() {
$dept = $this->id;
return function () use($dept) {
return array('dept_id' =>$dept);
};
}
}
class TransferForm extends Form {
static $id = 'transfer';
var $_dept = null;
function __construct($source=null, $options=array()) {
parent::__construct($source, $options);
}
function getFields() {
if ($this->fields)
return $this->fields;
$fields = array(
'dept' => new DepartmentField(array(
'id'=>1,
'label' => __('Department'),
'flags' => hexdec(0X450F3),
'required' => true,
'validator-error' => __('Department selection required'),
)
),
'comments' => new TextareaField(array(
'id' => 2,
'label'=> '',
'required'=>false,
'default'=>'',
'configuration' => array(
'html' => true,
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
'placeholder' => __('Optional reason for the transfer'),
),
)
),
);
$this->setFields($fields);
return $this->fields;
}
function isValid() {
if (!parent::isValid())
return false;
// Do additional validations
if (!($dept = $this->getDept()))
$this->getField('dept')->addError(
__('Unknown department'));
return !$this->errors();
}
function render($options) {
switch(strtolower($options['template'])) {
case 'simple':
$inc = STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
break;
default:
throw new Exception(sprintf(__('%s: Unknown template style %s'),
get_class(), $options['template']));
}
$form = $this;
include $inc;
}
function getDept() {
if (!isset($this->_dept)) {
if (($id = $this->getField('dept')->getClean()))
$this->_dept = Dept::lookup($id);
}
return $this->_dept;
}
}
/**
* FieldUnchanged
*
* Thrown in the to_database() method to indicate the value should not be
* saved in the database (it wasn't changed in the request)
*/
class FieldUnchanged extends Exception {}