golden hour
/home/doctorbruno/public_html/doctorbruno.info/taeionline/support/include
⬆️ Go Up
Upload
File/Folder
Size
Actions
.MANIFEST
194.97 KB
Del
OK
.htaccess
14 B
Del
OK
JSON.php
33.13 KB
Del
OK
PasswordHash.php
6.92 KB
Del
OK
Spyc.php
31.74 KB
Del
OK
UniversalClassLoader.php
8.61 KB
Del
OK
ajax.admin.php
7.29 KB
Del
OK
ajax.config.php
4.61 KB
Del
OK
ajax.content.php
9.76 KB
Del
OK
ajax.draft.php
13.03 KB
Del
OK
ajax.email.php
2.33 KB
Del
OK
ajax.export.php
1006 B
Del
OK
ajax.filter.php
874 B
Del
OK
ajax.forms.php
13.84 KB
Del
OK
ajax.i18n.php
5.1 KB
Del
OK
ajax.kbase.php
2.93 KB
Del
OK
ajax.note.php
2.05 KB
Del
OK
ajax.orgs.php
11.73 KB
Del
OK
ajax.plugins.php
2.15 KB
Del
OK
ajax.schedule.php
4.29 KB
Del
OK
ajax.search.php
12.72 KB
Del
OK
ajax.sequence.php
3.2 KB
Del
OK
ajax.staff.php
11.62 KB
Del
OK
ajax.tasks.php
33.87 KB
Del
OK
ajax.thread.php
8.81 KB
Del
OK
ajax.tickets.php
79.7 KB
Del
OK
ajax.tips.php
1.66 KB
Del
OK
ajax.upgrader.php
2.24 KB
Del
OK
ajax.users.php
18.33 KB
Del
OK
api.cron.php
1.06 KB
Del
OK
api.tickets.php
10.36 KB
Del
OK
class.2fa.php
7.76 KB
Del
OK
class.ajax.php
1.45 KB
Del
OK
class.api.php
15.31 KB
Del
OK
class.app.php
1.49 KB
Del
OK
class.attachment.php
6.95 KB
Del
OK
class.auth.php
50.73 KB
Del
OK
class.avatar.php
6.44 KB
Del
OK
class.banlist.php
2.58 KB
Del
OK
class.base32.php
4.07 KB
Del
OK
class.businesshours.php
7.08 KB
Del
OK
class.canned.php
9.05 KB
Del
OK
class.captcha.php
1.73 KB
Del
OK
class.category.php
11.14 KB
Del
OK
class.charset.php
3.41 KB
Del
OK
class.cli.php
9.48 KB
Del
OK
class.client.php
15.39 KB
Del
OK
class.collaborator.php
5.65 KB
Del
OK
class.company.php
2.65 KB
Del
OK
class.config.php
60.36 KB
Del
OK
class.controller.php
1.15 KB
Del
OK
class.cron.php
3.6 KB
Del
OK
class.crypto.php
18.92 KB
Del
OK
class.csrf.php
2.38 KB
Del
OK
class.dept.php
33.21 KB
Del
OK
class.dispatcher.php
7.12 KB
Del
OK
class.draft.php
6.23 KB
Del
OK
class.dynamic_forms.php
64.98 KB
Del
OK
class.email.php
49 KB
Del
OK
class.error.php
1.69 KB
Del
OK
class.export.php
32.92 KB
Del
OK
class.faq.php
15.05 KB
Del
OK
class.file.php
34.59 KB
Del
OK
class.filter.php
31.9 KB
Del
OK
class.filter_action.php
23.67 KB
Del
OK
class.format.php
44.3 KB
Del
OK
class.forms.php
195.58 KB
Del
OK
class.http.php
7.16 KB
Del
OK
class.i18n.php
24.28 KB
Del
OK
class.import.php
6.9 KB
Del
OK
class.json.php
2.71 KB
Del
OK
class.knowledgebase.php
5.8 KB
Del
OK
class.list.php
42 KB
Del
OK
class.lock.php
4.05 KB
Del
OK
class.log.php
1.55 KB
Del
OK
class.mail.php
35.76 KB
Del
OK
class.mailer.php
25.75 KB
Del
OK
class.mailfetch.php
9.72 KB
Del
OK
class.mailparse.php
30.99 KB
Del
OK
class.message.php
6.42 KB
Del
OK
class.migrater.php
5.2 KB
Del
OK
class.misc.php
7.5 KB
Del
OK
class.model.php
2.3 KB
Del
OK
class.nav.php
14.15 KB
Del
OK
class.note.php
2.39 KB
Del
OK
class.oauth2.php
4.04 KB
Del
OK
class.organization.php
22.45 KB
Del
OK
class.orm.php
120.9 KB
Del
OK
class.osticket.php
20.64 KB
Del
OK
class.ostsession.php
21.86 KB
Del
OK
class.page.php
10.59 KB
Del
OK
class.pagenate.php
5.53 KB
Del
OK
class.passwd.php
1.21 KB
Del
OK
class.pdf.php
3.83 KB
Del
OK
class.plugin.php
36.13 KB
Del
OK
class.priority.php
1.81 KB
Del
OK
class.queue.php
103.51 KB
Del
OK
class.report.php
11.76 KB
Del
OK
class.role.php
11.27 KB
Del
OK
class.schedule.php
46.28 KB
Del
OK
class.search.php
61.66 KB
Del
OK
class.sequence.php
7.27 KB
Del
OK
class.session.php
19.25 KB
Del
OK
class.setup.php
3.55 KB
Del
OK
class.signal.php
4.16 KB
Del
OK
class.sla.php
9.11 KB
Del
OK
class.staff.php
60.21 KB
Del
OK
class.task.php
56.21 KB
Del
OK
class.team.php
12.31 KB
Del
OK
class.template.php
23.45 KB
Del
OK
class.thread.php
108.96 KB
Del
OK
class.thread_actions.php
17.08 KB
Del
OK
class.ticket.php
167.39 KB
Del
OK
class.timezone.php
21.94 KB
Del
OK
class.topic.php
20.06 KB
Del
OK
class.translation.php
34.79 KB
Del
OK
class.upgrader.php
13.76 KB
Del
OK
class.user.php
43.4 KB
Del
OK
class.usersession.php
7.55 KB
Del
OK
class.util.php
10.07 KB
Del
OK
class.validator.php
12.94 KB
Del
OK
class.variable.php
11.93 KB
Del
OK
class.xml.php
3.23 KB
Del
OK
class.yaml.php
1.15 KB
Del
OK
cli
-
Del
OK
client
-
Del
OK
config
-
Del
OK
fpdf
-
Del
OK
htmLawed.php
53.53 KB
Del
OK
html2text.php
33.71 KB
Del
OK
i18n
-
Del
OK
index.php
37 B
Del
OK
laminas-mail
-
Del
OK
mpdf
-
Del
OK
mysqli.php
9.55 KB
Del
OK
ost-config.php
5.63 KB
Del
OK
ost-sampleconfig.php
6.24 KB
Del
OK
pear
-
Del
OK
plugins
-
Del
OK
staff
-
Del
OK
tnef_decoder.php
19.82 KB
Del
OK
upgrader
-
Del
OK
Edit: class.forms.php
<?php /********************************************************************* class.forms.php osTicket forms framework Jared Hancock <jared@osticket.com> Copyright (c) 2006-2013 osTicket http://www.osticket.com Released under the GNU General Public License WITHOUT ANY WARRANTY. See LICENSE.TXT for details. vim: expandtab sw=4 ts=4 sts=4: **********************************************************************/ /** * Form template, used for designing the custom form and for entering custom * data for a ticket */ class Form { static $renderer = 'GridFluidLayout'; static $id = 0; var $options = array(); var $fields = array(); var $title = ''; var $notice = ''; var $instructions = ''; var $validators = array(); var $_errors = null; var $_source = false; function __construct($source=null, $options=array()) { $this->options = $options; if (isset($options['title'])) $this->title = $options['title']; if (isset($options['instructions'])) $this->instructions = $options['instructions']; if (isset($options['notice'])) $this->notice = $options['notice']; if (isset($options['id'])) $this->id = $options['id']; // Use POST data if source was not specified $this->_source = $source ?: $_POST; } function getFormId() { return @$this->id ?: static::$id; } function setId($id) { $this->id = $id; } function setNotice($notice) { $this->notice = $notice; } function data($source) { foreach ($this->fields as $name=>$f) if (isset($source[$name])) $f->value = $source[$name]; } function setFields($fields) { if (!is_array($fields) && !$fields instanceof Traversable) return; $this->fields = $fields; foreach ($fields as $k=>$f) { $f->setForm($this); if (!$f->get('name') && $k && !is_numeric($k)) $f->set('name', $k); } } function getFields() { return $this->fields; } function getField($name) { $fields = $this->getFields(); foreach($fields as $f) if(!strcasecmp($f->get('name'), $name)) return $f; if (isset($fields[$name])) return $fields[$name]; } function hasField($name) { return $this->getField($name); } function hasAnyEnabledFields() { return $this->hasAnyVisibleFields(false); } function hasAnyVisibleFields($user=false) { $visible = 0; $isstaff = $user instanceof Staff; foreach ($this->getFields() as $F) { if (!$user) { // Assume hasAnyEnabledFields if ($F->isEnabled()) $visible++; } elseif($isstaff) { if ($F->isVisibleToStaff()) $visible++; } elseif ($F->isVisibleToUsers()) { $visible++; } } return $visible > 0; } function getTitle() { return $this->title; } function getNotice() { return $this->notice; } function getInstructions() { return Format::htmldecode($this->instructions); } function getSource() { return $this->_source; } function setSource($source) { $this->_source = $source; } /** * Validate the form and indicate if there no errors. * * Parameters: * $filter - (callback) function to receive each field and return * boolean true if the field's errors are significant */ function isValid($include=false) { if (!isset($this->_errors)) { $this->_errors = array(); $this->validate($this->getClean()); foreach ($this->getFields() as $field) if ($field->errors() && (!$include || $include($field))) $this->_errors[$field->get('id')] = $field->errors(); } return !$this->_errors; } function validate($clean_data) { // Validate the whole form so that errors can be added to the // individual fields and collected below. foreach ($this->validators as $V) { $V($this); } } function getClean($validate=true) { if (!$this->_clean) { $this->_clean = array(); foreach ($this->getFields() as $key=>$field) { if (!$field->hasData()) continue; // Prefer indexing by field.id if indexing numerically if (is_int($key) && $field->get('id')) $key = $field->get('id'); $this->_clean[$key] = $this->_clean[$field->get('name')] = $field->getClean($validate); } unset($this->_clean[""]); } return $this->_clean; } /* * Transform form data to database ready clean data. * */ function to_db($clean=null, $validate=true) { if (!$clean && !$this->isValid() && !($clean=$this->getClean($validate))) return false; $data = []; foreach ($clean as $name => $val) { if (!($f = $this->getField($name))) continue; try { $data[$name] = $f->to_database($val); } catch (FieldUnchanged $e) { // Unset field if it's unchanged...mainly // useful for Secret/PasswordField unset($data[$name]); } } return $data; } /* * Process the form input and return clean data. * * It's similar to to_db but forms downstream can use it to skip or add * extra validations */ function process($validate=true) { return $this->to_db($validate); } 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=[]) { 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($options=array()) { if (isset($options['title'])) $this->title = $options['title']; if (isset($options['instructions'])) $this->instructions = $options['instructions']; if (isset($options['notice'])) $this->notice = $options['notice']; $form = $this; $template = $options['template'] ?: 'dynamic-form.tmpl.php'; if (isset($options['staff']) && $options['staff']) include(STAFFINC_DIR . 'templates/' . $template); else include(CLIENTINC_DIR . 'templates/' . $template); echo $this->getMedia(); } 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; } } } } } function emitJavascript($options=array()) { // Check if we need to emit javascript if (!($fid=$this->getFormId())) return; ?> <script type="text/javascript"> $(function() { <?php //XXX: We ONLY want to watch field on this form. We'll only // watch form inputs if form_id is specified. Current FORM API // doesn't generate the entire form (just fields) if ($fid) { ?> $(document).off('change.<?php echo $fid; ?>'); $(document).on('change.<?php echo $fid; ?>', 'form#<?php echo $fid; ?> :input', function() { //Clear any current errors... var errors = $('#field'+$(this).attr('id')+'_error'); if (errors.length) errors.slideUp('fast', function (){ $(this).remove(); }); //TODO: Validation input inplace or via ajax call // and set any new errors AND visibilty changes } ); <?php } ?> }); </script> <?php } static function emitMedia($url, $type) { if ($url[0] == '/') $url = ROOT_PATH . substr($url, 1); switch (strtolower($type)) { case 'css': ?> <link rel="stylesheet" type="text/css" href="<?php echo $url; ?>"/><?php break; case 'js': ?> <script type="text/javascript" src="<?php echo $url; ?>"></script><?php break; } } /** * getState * * Retrieves an array of information which can be passed to the * ::loadState method later to recreate the current state of the form * fields and values. */ function getState() { $info = array(); foreach ($this->getFields() as $f) { // Skip invisible fields if (!$f->isVisible()) continue; // Skip fields set to default values $v = $f->getClean(); $d = $f->get('default'); if ($v == $d) continue; // Skip empty values if (!$v) continue; $info[$f->get('name') ?: $f->get('id')] = $f->to_database($v); } return $info; } /** * loadState * * Reset this form to the state previously recorded by the ::getState() * method */ function loadState($state) { foreach ($this->getFields() as $f) { $name = $f->get('name'); $f->reset(); if (isset($state[$name])) { $f->value = $f->to_php($state[$name]); } } } /* * Initialize a generic static form */ static function instantiate() { $r = new ReflectionClass(get_called_class()); return $r->newInstanceArgs(func_get_args()); } } /** * SimpleForm * Wrapper for inline/static forms. * */ class SimpleForm extends Form { function __construct($fields=array(), $source=null, $options=array()) { parent::__construct($source, $options); if (isset($options['type'])) $this->type = $options['type']; $this->setFields($fields); } function getId() { return $this->getFormId(); } } class CustomForm extends SimpleForm { function getFields() { global $thisstaff, $thisclient; $options = $this->options; $user = $options['user'] ?: $thisstaff ?: $thisclient; $isedit = ($options['mode'] == 'edit'); $fields = array(); foreach (parent::getFields() as $field) { if ($isedit && !$field->isEditable($user)) continue; $fields[] = $field; } return $fields; } } 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' ?>"> <caption><?php echo Format::htmlchars($this->title ?: $form->getTitle()); ?> <div><small><?php echo Format::viewableImages($form->getInstructions()); ?></small></div> <?php if ($form->getNotice()) echo sprintf('<div><small><p id="msg_warning">%s</p></small></div>', Format::htmlchars($form->getNotice())); ?> </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; //Layout and rendering options $options = $this->options; foreach ($form->getFields() as $f) { $layout = $this->getLayout($f); $size = $layout->getWidth() ?: 12; if ($offs = $layout->getOffset()) { $size += $offs; } if ($cols < $size || $layout->isBreakForced()) { if ($row) echo '</tr>'; echo '<tr>'; $cols = $row_size; $row++; } // Render the cell $cols -= $size; $attrs = array('colspan' => $size, 'rowspan' => $layout->getHeight(), 'style' => '"'.$layout->getOption('style').'"'); if ($offs) { ?> <td colspan="<?php echo $offs; ?>"></td> <?php } ?> <td class="cell" <?php echo Format::array_implode('=', ' ', array_filter($attrs)); ?> data-field-id="<?php echo $f->get('id'); ?>"> <fieldset class="field <?php if (!$f->isVisible()) echo 'hidden'; ?>" id="field<?php echo $f->getWidget()->id; ?>" data-field-id="<?php echo $f->get('id'); ?>"> <?php $label = $f->get('label'); ?> <label class="<?php if ($f->isRequired()) echo 'required'; ?>" for="<?php echo $f->getWidget()->id; ?>"> <?php echo $label ? (Format::htmlchars($label).':') : ' '; ?> <?php if ($f->isRequired()) { ?> <span class="error">*</span> </label> <?php } if ($f->get('hint')) { ?> <div class="field-hint-text"> <?php echo Format::htmlchars($f->get('hint')); ?> </div> <?php } $f->render($options); if ($f->errors()) foreach ($f->errors() as $e) echo sprintf('<div class="error">%s</div>', Format::htmlchars($e)); ?> </fieldset> </td> <?php } if ($row) echo '</tr>'; echo '</tbody></table>'; return ob_get_clean(); } function asBlock($form) {} } /** * Basic container for field and form layouts. By default every cell takes * a whole output row and does not imply any sort of width. */ class Cell { function isBreakForced() { return true; } function getWidth() { return false; } function getHeight() { return 1; } function getOffset() { return 0; } function getOption($prop) { return false; } } /** * Fluid grid layout, meaning each cell renders to the right of the previous * cell (for left-to-right layouts). A width in columns can be specified for * each cell along with an offset from the previous cell. A height of columns * along with an optional break is supported. */ class GridFluidCell extends Cell { var $span; var $options; function __construct($span, $options=array()) { $this->span = $span; $this->options = $options + array( 'rows' => 1, # rowspan 'offset' => 0, # skip some columns 'break' => false, # start on a new row ); } function isBreakForced() { return $this->options['break']; } function getWidth() { return $this->span; } function getHeight() { return $this->options['rows']; } function getOffset() { return $this->options['offset']; } function getOption($prop) { return $this->options[$prop]; } } require_once(INCLUDE_DIR . "class.json.php"); class FormField { static $widget = false; var $ht = array( 'label' => false, 'required' => false, 'default' => false, 'configuration' => array(), ); var $_form; var $_cform; var $_clean; var $_errors = array(); var $_widget; var $answer; var $parent; var $presentation_only = false; static $types = array( /* @trans */ 'Basic Fields' => array( 'text' => array( /* @trans */ 'Short Answer', 'TextboxField'), 'memo' => array( /* @trans */ 'Long Answer', 'TextareaField'), 'thread' => array( /* @trans */ 'Thread Entry', 'ThreadEntryField', false), 'datetime' => array(/* @trans */ 'Date and Time', 'DatetimeField'), 'timezone' => array(/* @trans */ 'Timezone', 'TimezoneField'), 'phone' => array( /* @trans */ 'Phone Number', 'PhoneField'), 'bool' => array( /* @trans */ 'Checkbox', 'BooleanField'), 'choices' => array( /* @trans */ 'Choices', 'ChoiceField'), 'files' => array( /* @trans */ 'File Upload', 'FileUploadField'), 'break' => array( /* @trans */ 'Section Break', 'SectionBreakField'), 'info' => array( /* @trans */ 'Information', 'FreeTextField'), ), ); static $more_types = array(); static $uid = null; static function _uid() { return ++self::$uid; } 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($validate=true) { if (!isset($this->_clean)) { $this->_clean = (isset($this->value)) // XXX: The widget value may be parsed already if this is // linked to dynamic data via ::getAnswer() ? $this->value : $this->parse($this->getWidget()->value); if ($vs = $this->get('cleaners')) { if (is_array($vs)) { foreach ($vs as $cleaner) if (is_callable($cleaner)) $this->_clean = call_user_func_array( $cleaner, array($this, $this->_clean)); } elseif (is_callable($vs)) $this->_clean = call_user_func_array( $vs, array($this, $this->_clean)); } if (!isset($this->_clean) && ($d = $this->get('default'))) $this->_clean = $d; if ($this->isVisible() && $validate) $this->validateEntry($this->_clean); } return $this->_clean; } function reset() { $this->value = $this->_clean = $this->_widget = null; } function getValue() { return $this->getWidget()->getValue(); } function errors() { return $this->_errors; } function resetErrors() { $this->_errors = []; return !($this->_errors); } function addError($message, $index=false) { if ($index) $this->_errors[$index] = $message; else $this->_errors[] = $message; // Update parent form errors for the field if ($this->_form) $this->_form->addError($this->errors(), $this->get('id')); } 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->getLocal('label') ? sprintf(__('%s is a required field'), $this->getLocal('label')) : __('This is a required field'); # Perform declared validators for the field if ($vs = $this->get('validators')) { if (is_array($vs)) { foreach ($vs as $validator) if (is_callable($validator)) $validator($this, $value); } elseif (is_callable($vs)) $vs($this, $value); } } /** * isVisible * * If this field has visibility configuration, then it will parse the * constraints with the visibility configuration to determine if the * field is visible and should be considered for validation */ function isVisible() { if ($this->get('visibility') instanceof VisibilityConstraint) { return $this->get('visibility')->isVisible($this); } return true; } /** * Check if the user has edit rights * */ function isEditable($user=null) { // Internal editable flag used by internal forms e.g internal lists if (!$user && isset($this->ht['editable'])) return $this->ht['editable']; if ($user instanceof Staff) $flag = DynamicFormField::FLAG_AGENT_EDIT; else $flag = DynamicFormField::FLAG_CLIENT_EDIT; return (($this->get('flags') & $flag) != 0); } /** * isStorable * * Indicate if this field data is storable locally (default).Some field's data * might beed to be stored elsewhere for optimization reasons at the * application level. * */ function isStorable() { return (($this->get('flags') & DynamicFormField::FLAG_EXT_STORED) == 0); } function isRequired() { return $this->get('required'); } /** * 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). to_php is used for each field returned from * ::getConfigurationOptions(), and when the whole configuration is * built, to_config() is called and receives the config array. The array * should be returned, perhaps with modifications, and will be JSON * encoded and stashed in the database. */ function to_config($value) { return $value; } /** * to_database * * Determines the value to be stored in the database. The database * backend for all fields is a text field, so this method should return * a text value or NULL to represent the value of the field. The * ::to_php() method will convert this value back to PHP. * * Paremeters: * $value - PHP value of the field's content */ function to_database($value) { return $value; } /** * toString * * Converts the PHP value created in ::parse() or ::to_php() to a * pretty-printed value to show to the user. This is especially useful * for something like dates which are stored considerably different in * the database from their respective human-friendly versions. * Furthermore, this method allows for internationalization and * localization. * * Parametes: * $value - PHP value of the field's content */ function toString($value) { return (string) $value; } function __toString() { return $this->toString($this->value); } /** * When data for this field is deleted permanently from some storage * backend (like a database), other associated data may need to be * cleaned as well. This hook allows fields to participate when the data * for a field is cleaned up. */ function db_cleanup($field=false) { } /** * Returns an HTML friendly value for the data in the field. */ function display($value) { return Format::htmlchars($this->toString($value ?: $this->value)); } /** * Returns a value suitable for exporting to a foreign system. Mostly * useful for things like dates and phone numbers which should be * formatted using a standard when exported */ function export($value) { return $this->toString($value); } /** * Fetch a value suitable for embedding the value of this field in an * email template. Reference implementation uses ::to_php(); */ function asVar($value, $id=false) { return $this->to_php($value, $id); } /** * Fetch the var type used with the email templating system's typeahead * feature. This helps with variable expansion if supported by this * field's ::asVar() method. This method should return a valid classname * which implements the `TemplateVariable` interface. */ function asVarType() { return false; } /** * Describe the difference between the to two values. Note that the * values should be passed through ::parse() or to_php() before * utilizing this method. */ function whatChanged($before, $after) { if ($before) $desc = __('changed from <strong>%1$s</strong> to <strong>%2$s</strong>'); else $desc = __('set to <strong>%2$s</strong>'); return sprintf($desc, $this->display($before), $this->display($after)); } /** * Convert the field data to something matchable by filtering. The * primary use of this is for ticket filtering. */ function getFilterData() { return $this->toString($this->getClean()); } /** * Fetches a value that represents this content in a consistent, * searchable format. This is used by the search engine system and * backend. */ function searchable($value) { return Format::searchable($this->toString($value)); } function getKeys($value) { return $this->to_database($value); } /** * Fetches a list of options for searching. The values returned from * this method are passed to the widget's `::render()` method so that * the widget can be affected by this setting. For instance, date fields * might have a 'between' search option which should trigger rendering * of two date widgets for search results. */ function getSearchMethods() { return array( 'set' => __('has a value'), 'nset' => __('does not have a value'), 'equal' => __('is'), 'nequal' => __('is not'), 'contains' => __('contains'), 'match' => __('matches'), ); } function getSearchMethodWidgets() { return array( 'set' => null, 'nset' => null, 'equal' => array('TextboxField', array('configuration' => array('size' => 40))), 'nequal' => array('TextboxField', array('configuration' => array('size' => 40))), 'contains' => array('TextboxField', array('configuration' => array('size' => 40))), 'match' => array('TextboxField', array( 'placeholder' => __('Valid regular expression'), 'configuration' => array('size'=>30), 'validators' => function($self, $v) { if (false === @preg_match($v, ' ') && false === @preg_match("/$v/", ' ')) $self->addError(__('Cannot compile this regular expression')); })), ); } /** * This is used by the searching system to build a query for the search * engine. The function should return a criteria listing to match * content saved by the field by the `::to_database()` function. */ function getSearchQ($method, $value, $name=false) { $criteria = array(); $Q = new Q(); $name = $name ?: $this->get('name'); switch ($method) { case 'nset': $Q->negate(); case 'set': $criteria[$name . '__isnull'] = false; break; case 'nequal': $Q->negate(); case 'equal': $criteria[$name] = $value; break; case 'contains': $criteria[$name . '__contains'] = $value; break; case 'match': $criteria[$name . '__regex'] = $value; break; } return $Q->add($criteria); } function getSearchWidget($method) { $methods = $this->getSearchMethodWidgets(); $info = $methods[$method]; if (is_array($info)) { $class = $info[0]; return new $class($info[1]); } return $info; } function describeSearchMethod($method) { switch ($method) { case 'set': return __('%s has a value'); case 'nset': return __('%s does not have a value'); case 'equal': return __('%s is %s' /* describes an equality */); case 'nequal': return __('%s is not %s' /* describes an inequality */); case 'contains': return __('%s contains "%s"'); case 'match': return __('%s matches pattern %s'); case 'includes': return __('%s in (%s)'); case '!includes': return __('%s not in (%s)'); } } function describeSearch($method, $value, $name=false) { $name = $name ?: $this->get('name'); $desc = $this->describeSearchMethod($method); switch ($method) { case 'set': case 'nset': return sprintf($desc, $name); default: return sprintf($desc, $name, $this->toString($value)); } } function addToQuery($query, $name=false) { return $query->values($name ?: $this->get('name')); } /** * Similary to to_php() and parse(), except a row from a queryset is * passed. The value returned should be what would be retured from * parse() or to_php() */ function from_query($row, $name=false) { return $row[$name ?: $this->get('name')]; } /** * If the field can be used in a quick filter. To be used, it should * also implement getQuickFilterChoices() which should return a list of * choices to appear in a quick filter drop-down */ function supportsQuickFilter() { return false; } /** * Fetch a keyed array of quick filter choices. The keys should be * passed later to ::applyQuickFilter() to apply the quick filter to a * query. The values should be localized titles for the choices. */ function getQuickFilterChoices() { return array(); } /** * Apply a quick filter selection of this field to the query. The * modified query should be returned. Optionally, the orm path / field * name can be passed. */ function applyQuickFilter($query, $choice, $name=false) { return $query; } function getLabel() { return $this->get('label'); } function getSortKeys($path) { return array($path); } function getOrmPath($name=false, $query=null) { return CustomQueue::getOrmPath($name ?:$this->get('name'), $query); } function applyOrderBy($query, $reverse=false, $name=false) { $col = sprintf('%s%s', $reverse ? '-' : '', $this->getOrmPath($name, $query)); return $query->order_by($col); } /** * getImpl * * Magic method that will return an implementation instance of this * field based on the simple text value of the 'type' value of this * field instance. The list of registered fields is determined by the * global get_dynamic_field_types() function. The data from this model * will be used to initialize the returned instance. * * For instance, if the value of this field is 'text', a TextField * instance will be returned. */ function getImpl($parent=null) { // 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); return $inst; } function __call($what, $args) { // XXX: Throw exception if $this->parent is not set if (!$this->parent) throw new Exception(sprintf(__('%s: Call to undefined function'), $what)); // 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; } /** * Fetch a pseudo-random id for this form field. It is used when * rendering the widget in the @name attribute emitted in the resulting * HTML. The form element is based on the form id, field id and name, * and the current user's session id. Therefore, the same form fields * will yield differing names for different users. This is used to ward * off bot attacks as it makes it very difficult to predict and * correlate the form names to the data they represent. */ function getFormName() { $default = $this->get('name') ?: $this->get('id'); if ($this->_form && is_numeric($fid = $this->_form->getFormId())) return substr(md5( session_id() . "-form-field-id-$fid-$default-" . SECRET_SALT), -14); elseif (is_numeric($this->get('id'))) return substr(md5( session_id() . '-field-id-'.$this->get('id') . '-' . SECRET_SALT), -16); return $default; } function getFormNames() { // All possible names - this is important for inline data injection $names = array_filter([ 'hash' => $this->getFormName(), 'name' => $this->get('name'), 'id' => $this->get('id')]); // Force pseudo-random name for Dynamicforms on POST (Web Tickets) if (0 && $_POST && !defined('APICALL') && isset($this->ht['form']) && ($this->ht['form'] instanceof DynamicForm)) return [$names['hash']]; return $names; } 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()) { return; } function getMedia() { $widget = $this->getWidget(); return $widget::$media; } 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 (!isset($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; } /** * 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; } function getConfigurationForm($source=null) { if (!$this->_cform) { $type = static::getFieldType($this->get('type')); $clazz = $type[1]; $T = new $clazz($this->ht); $config = $this->getConfiguration(); $this->_cform = new SimpleForm($T->getConfigurationOptions(), $source); if (!$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'); } } } return $this->_cform; } function configure($prop, $value) { $this->getConfiguration(); $this->_config[$prop] = $value; } function getWidget($widgetClass=false) { if (!static::$widget) throw new Exception(__('Widget not defined for this field')); if (!isset($this->_widget)) { $wc = $widgetClass ?: $this->get('widget') ?: static::$widget; $this->_widget = new $wc($this); $this->_widget->parseValue(); } return $this->_widget; } function getSelectName() { $name = $this->get('name') ?: 'field_'.$this->get('id'); if ($this->hasIdValue()) $name .= '_id'; return $name; } function getTranslateTag($subtag) { return _H(sprintf('field.%s.%s%s', $subtag, $this->get('id'), $this->get('form_id') ? '' : '*internal*')); } function getLocal($subtag, $default=false) { $tag = $this->getTranslateTag($subtag); $T = CustomDataTranslation::translate($tag); return $T != $tag ? $T : ($default ?: $this->get($subtag)); } function getEditForm($source=null) { $fields = array( 'field' => $this, 'comments' => new TextareaField(array( 'id' => 2, 'label'=> '', 'required' => false, 'default' => '', 'configuration' => array( 'html' => true, 'size' => 'small', 'placeholder' => __('Optional reason for the update'), ) )) ); return new SimpleForm($fields, $source); } function getChanges() { $new = $this->getValue(); $old = $this->answer ? $this->answer->getValue() : $this->get('default'); return ($old != $new) ? array($this->to_database($old), $this->to_database($new)) : false; } function save() { if (!($changes=$this->getChanges())) return true; if (!($a = $this->answer)) return false; $val = $changes[1]; if (is_array($val)) { $a->set('value', $val[0]); $a->set('value_id', $val[1]); } else { $a->set('value', $val); } if (!$a->save(true)) return false; $this->_clean = $this->_widget = null; return $this->parent->save(); } static function init($config) { return new Static($config); } } 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, ' ')) { $value = $wrapped; } if ($value == '//iu') return ''; return $value; }, 'validators' => function($self, $v) { if (false === @preg_match($v, ' ')) $self->addError(__('Cannot compile this regular expression')); })), 'validator-error' => new TextboxField(array( 'id'=>4, 'label'=>__('Validation Error'), 'default'=>'', 'configuration'=>array('size'=>40, 'length'=>60, 'translatable'=>$this->getTranslateTag('validator-error') ), '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 validateEntry($value) { //check to see if value is the string '0' $value = ($value === '0') ? '0' : Format::htmlchars($this->toString($value ?: $this->value)); parent::validateEntry($value); $config = $this->getConfiguration(); $validators = array( '' => '', 'noop' => array( function($a, &$b) { return true; } ), 'formula' => array(array('Validator', 'is_formula'), __('Content cannot start with the following characters: = - + @')), 'email' => array(array('Validator', 'is_valid_email'), __('Enter a valid email address')), 'phone' => array(array('Validator', 'is_phone'), __('Enter a valid phone number')), 'ip' => array(array('Validator', 'is_ip'), __('Enter a valid IP address')), 'number' => array(array('Validator', 'is_numeric'), __('Enter a number')), 'password' => array(array('Validator', 'check_passwd'), __('Invalid Password')), 'regex' => array( function($v) use ($config) { $regex = $config['regex']; return @preg_match($regex, $v); }, $config['validator-error'] ?? __('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; // If no validators are set and not an instanceof AdvancedSearchForm // force formula validation if (!$valid && !($this->getForm() instanceof AdvancedSearchForm)) $valid = 'formula'; $func = $validators[$valid]; $error = $err = null; // If validator is number and the value is 0 set to 0 (int) for is_numeric if ($valid == 'number' && $value == '0') $value = 0; if ($config['validator-error']) $error = $this->getLocal('validator-error', $config['validator-error']); if (is_array($func) && is_callable($func[0])) if (!call_user_func_array($func[0], array($value, &$err))) $this->_errors[] = $error ?: $err ?: $func[1]; } function parse($value) { return Format::strip_emoticons(Format::striptags($value)); } function display($value) { return ($value === '0') ? '0' : Format::htmlchars($this->toString($value ?: $this->value), true); } } class PasswordField extends TextboxField { static $widget = 'PasswordWidget'; function __construct($options=array()) { parent::__construct($options); if (!isset($options['validator'])) $this->set('validator', 'password'); } protected function getMasterKey() { return SECRET_SALT; } protected function getSubKey() { $config = $this->getConfiguration(); return $config['key'] ?: 'pwfield'; } function parse($value) { // Don't trim the value return $value; } function to_database($value) { // If not set in UI, don't save the empty value if (!$value) throw new FieldUnchanged(); return Crypto::encrypt($value, $this->getMasterKey(), $this->getSubKey()); } function to_php($value) { return Crypto::decrypt($value, $this->getMasterKey(), $this->getSubKey()); } } class TextareaField extends FormField { static $widget = 'TextareaWidget'; function getConfigurationOptions() { return array( 'cols' => new TextboxField(array( 'id'=>1, 'label'=>__('Width').' '.__('(chars)'), 'required'=>true, 'default'=>40)), 'rows' => new TextboxField(array( 'id'=>2, 'label'=>__('Height').' '.__('(rows)'), 'required'=>false, 'default'=>4)), 'length' => new TextboxField(array( 'id'=>3, 'label'=>__('Max Length'), 'required'=>false, 'default'=>0)), 'html' => new BooleanField(array( '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 validateEntry($value) { parent::validateEntry($value); if (!$value) return; $config = $this->getConfiguration(); $validators = array( '' => array(array('Validator', 'is_formula'), __('Content cannot start with the following characters: = - + @')), 'choices' => array( function($val) { $val = str_replace('"', '', JsonDataEncoder::encode($val)); $regex = "/^(?! )[A-z0-9 _-]+:{1}[^\n]+$/"; foreach (explode('\r\n', $val) as $v) { if (!preg_match($regex, $v)) return false; } return true; }, __('Each choice requires a key and has to be on a new line. (eg. key:value)') ), ); // Support configuration forms, as well as GUI-based form fields if (!($valid = $this->get('validator')) && isset($config['validator'])) $valid = $config['validator']; if (!isset($validators[$valid])) return; $func = $validators[$valid]; $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)) $this->_errors[] = $error; } function display($value) { $config = $this->getConfiguration(); if ($config['html']) return Format::safe_html($value); else return nl2br(Format::htmlchars($value, true)); } function searchable($value) { $body = new HtmlThreadEntryBody($value); return $body->getSearchable(); } 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, 'configuration'=>array( '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 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"); if ($ext && $config['ext']) { if (!is_numeric($ext)) $this->_errors[] = __("Enter a valid phone extension"); elseif (!$phone) $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; } function toString($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); } function to_php($value) { return $value ? true : false; } function toString($value) { return ($value) ? __('Yes') : __('No'); } function asVar($value, $id=false) { return $this->toString($value); } function getClean($validate=true) { if (!isset($this->_clean)) { $this->_clean = (isset($this->value)) ? $this->value : $this->getValue(); if ($this->isVisible() && $validate) $this->validateEntry($this->_clean); } return $this->_clean; } function getSearchMethods() { return array( 'set' => __('checked'), 'nset' => __('unchecked'), ); } function describeSearchMethod($method) { $methods = $this->get('descsearchmethods'); if (isset($methods[$method])) return $methods[$method]; return parent::describeSearchMethod($method); } function getSearchMethodWidgets() { return array( 'set' => null, 'nset' => null, ); } function getSearchQ($method, $value, $name=false) { $name = $name ?: $this->get('name'); switch ($method) { case 'set': return new Q(array($name => '1')); case 'nset': return new Q(array($name => '0')); default: return parent::getSearchQ($method, $value, $name); } } function supportsQuickFilter() { return true; } function getQuickFilterChoices() { return array( true => __('Checked'), false => __('Not Checked'), ); } function applyQuickFilter($query, $qf_value, $name=false) { return $query->filter(array( $name ?: $this->get('name') => (int) $qf_value, )); } } class ChoiceField extends FormField { static $widget = 'ChoicesWidget'; var $_choices; 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.</br><b>Note:</b> If you have more than two choices, use a List instead.'), 'validator'=>'choices', 'configuration'=>array( 'html' => false, 'disabled' => 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') )), ); } function parse($value) { 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)) $value = JsonDataEncoder::encode($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) && strpos($value, ',')) { $values = array(); $choices = $this->getChoices(); $vals = array_map('trim', explode(',', $value)); foreach ($vals as $V) { if (isset($choices[$V])) $values[$V] = $choices[$V]; } if (array_filter($values)) $value = $values; elseif($vals) list($value) = $vals; } $config = $this->getConfiguration(); if (!$config['multiselect'] && is_array($value) && count($value) < 2) { reset($value); $value = key($value); } return $value; } function toString($value) { if (!is_array($value)) $value = $this->getChoice($value); if (is_array($value)) return implode(', ', $value); return (string) $value; } function getKeys($value) { if (!is_array($value)) $value = $this->getChoice($value); if (is_array($value)) return implode(', ', array_keys($value)); return (string) $value; } function asVar($value, $id=false) { $value = $this->to_php($value); return $this->toString($this->getChoice($value)); } function getChanges() { $new = $this->to_database($this->getValue()); $old = $this->to_database($this->answer ? $this->answer->getValue() : $this->get('default')); // Compare old and new return ($old == $new) ? false : array($old, $new); } 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); $added = array_filter($added); $deleted = array_filter($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; } function getChoice($value) { $choices = $this->getChoices(); $selection = array(); if ($value && is_array($value)) { $selection = $value; } elseif (isset($choices[$value])) $selection[$value] = $choices[$value]; elseif (($v=$this->get('default')) && isset($choices[$v])) $selection[$v] = $choices[$v]; return $selection; } function getChoices($verbose=false, $options=array()) { 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, 2); 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, 'nset' => 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'); $val = $value; if ($value && is_array($value)) $val = '"?(?<![0-9])'.implode('("|,|$)|"?(?<![0-9])', array_keys($value)).'("|,|$)'; switch ($method) { case '!includes': return Q::not(array("{$name}__regex" => $val)); case 'includes': return new Q(array("{$name}__regex" => $val)); 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); } } function supportsQuickFilter() { return true; } function getQuickFilterChoices() { return $this->getChoices(); } function applyQuickFilter($query, $qf_value, $name=false) { global $thisstaff; $field = new AssigneeChoiceField(); //special assignment quick filters switch (true) { case ($qf_value == 'assigned'): case ($qf_value == '!assigned'): $result = $field->getSearchQ($qf_value, $qf_value); return $query->filter($result); case (strpos($qf_value, 's') !== false): case (strpos($qf_value, 't') !== false): case ($qf_value == 'M'): case ($qf_value == 'T'): $value = array($qf_value => $qf_value); $result = $field->getSearchQ('includes', $value); return $query->filter($result); break; } return $query->filter(array( $name ?: $this->get('name') => $qf_value, )); } } class NumericField extends FormField { function getSearchMethods() { return array( 'equal' => __('Equal'), 'greater' => __('Greater Than'), 'less' => __('Less Than'), ); } function getSearchMethodWidgets() { return array( 'equal' => array('TextboxField', array( 'configuration' => array( 'validator' => 'number', 'size' => 6 ), )), 'greater' => array('TextboxField', array( 'configuration' => array( 'validator' => 'number', 'size' => 6 ), )), 'less' => array('TextboxField', array( 'configuration' => array( 'validator' => 'number', 'size' => 6 ), )), ); } function getSearchQ($method, $value, $name=false) { switch ($method) { case 'equal': return new Q(array( "{$name}__exact" => intval($value) )); break; case 'greater': return Q::any(array( "{$name}__gt" => intval($value) )); break; case 'less': return Q::any(array( "{$name}__lt" => intval($value) )); break; default: return parent::getSearchQ($method, $value, $name); } } } class DatetimeField extends FormField { static $widget = 'DatetimePickerWidget'; var $min = null; var $max = null; static function intervals($count=2, $i='') { $intervals = array( 'i' => _N('minute', 'minutes', $count), 'h' => _N('hour', 'hours', $count), 'd' => _N('day','days', $count), 'w' => _N('week', 'weeks', $count), 'm' => _N('month', 'months', $count), ); return $i ? $intervals[$i] : $intervals; } static function periods($period='') { $periods = array( 'td' => __('Today'), 'yd' => __('Yesterday'), 'tw' => __('This Week'), 'tm' => __('This Month'), 'tq' => __('This Quarter'), 'ty' => __('This Year'), 'lw' => __('Last Week'), 'lm' => __('Last Month'), 'lq' => __('Last Quarter'), 'ly' => __('Last Year'), ); return $period ? $periods[$period] : $periods; } // Get php DatateTime object of the field - null if value is empty function getDateTime($value=null) { return Format::parseDateTime($value ?: $this->value); } // Get effective timezone for the field function getTimeZone() { global $cfg; $config = $this->getConfiguration(); $timezone = new DateTimeZone($config['timezone'] ?: $cfg->getTimezone()); return $timezone; } function getMinDateTime() { if (!isset($this->min)) { $config = $this->getConfiguration(); $this->min = $config['min'] ? Format::parseDateTime($config['min']) : false; } return $this->min; } function getMaxDateTime() { if (!isset($this->max)) { $config = $this->getConfiguration(); $this->max = $config['max'] ? Format::parseDateTime($config['max']) : false; } return $this->max; } static function getPastPresentLabels() { return array(__('Create Date'), __('Reopen Date'), __('Close Date'), __('Last Update')); } function to_database($value) { // Store time in format given by Date Picker (DateTime::W3C) return $value; } function to_php($value) { if (!is_numeric($value) && strtotime($value) <= 0) return 0; return $value; } function display($value) { global $cfg; if (!$value || !($datetime = Format::parseDateTime($value))) return ''; $config = $this->getConfiguration(); $format = $config['format'] ?: false; if ($config['gmt']) return $this->format((int) $datetime->format('U'), $format); // Force timezone if field has one. if ($config['timezone']) { $timezone = new DateTimezone($config['timezone']); $datetime->setTimezone($timezone); } $value = $this->format($datetime->format('U'), $datetime->getTimezone()->getName(), $format); // No need to show timezone if (!$config['time'] || $format) return $value; // Display is NOT timezone aware show entry's timezone. return sprintf('%s (%s)', $value, $datetime->format('T')); } function from_query($row, $name=false) { $value = parent::from_query($row, $name); $timestamp = is_int($value) ? $value : (int) strtotime($value); return ($timestamp > 0) ? $timestamp : ''; } function format($timestamp, $timezone=false, $format=false) { if (!$timestamp || $timestamp <= 0) return ''; $config = $this->getConfiguration(); if ($config['time']) $formatted = Format::datetime($timestamp, false, $format, $timezone); else $formatted = Format::date($timestamp, false, $format, $timezone); return $formatted; } function toString($value) { if (is_array($value)) return ''; $timestamp = is_int($value) ? $value : (int) strtotime($value); if ($timestamp <= 0) return ''; return $this->format($timestamp); } function asVar($value, $id=false) { global $cfg; if (!$value) return null; $datetime = $this->getDateTime($value); $config = $this->getConfiguration(); if (!$config['gmt'] || !$config['time']) $timezone = $datetime->getTimezone()->getName(); else $timezone = false; return new FormattedDate($value, array( 'timezone' => $timezone, 'format' => $config['time'] ? 'long' : 'short' ) ); } function asVarType() { return 'FormattedDate'; } function getConfigurationOptions() { return array( 'time' => new BooleanField(array( 'id'=>1, 'label'=>__('Time'), 'required'=>false, 'default'=>false, 'configuration'=>array( 'desc'=>__('Show time selection with date picker')))), 'timezone' => new TimezoneField(array( 'id'=>2, 'label'=>__('Timezone'), 'required'=>false, 'hint'=>__('Timezone of the date time selection'), 'configuration' => array('autodetect'=>false, 'prompt' => __("User's timezone")), 'visibility' => new VisibilityConstraint( new Q(array('time__eq'=> true)), VisibilityConstraint::HIDDEN ), )), 'gmt' => new BooleanField(array( 'id'=>3, 'label'=>__('Timezone Aware'), 'required'=>false, 'configuration'=>array( 'desc'=>__("Show date/time relative to user's timezone")))), 'min' => new DatetimeField(array( 'id'=>4, 'label'=>__('Earliest'), 'required'=>false, 'hint'=>__('Earliest date selectable'))), 'max' => new DatetimeField(array( 'id'=>5, 'label'=>__('Latest'), 'required'=>false, 'default'=>null, 'hint'=>__('Latest date selectable'))), 'future' => new BooleanField(array( 'id'=>6, 'label'=>__('Allow Future Dates'), 'required'=>false, 'default'=>true, 'configuration'=>array( 'desc'=>__('Allow entries into the future' /* Used in the date field */)), )), ); } function validateEntry($value) { global $cfg; $config = $this->getConfiguration(); parent::validateEntry($value); if (!$value || !($datetime = Format::parseDateTime($value))) return; // Get configured min/max (if any) $min = $this->getMinDateTime(); $max = $this->getMaxDateTime(); if (!$datetime) { $this->_errors[] = __('Enter a valid date'); } elseif ($min and $datetime < $min) { $this->_errors[] = sprintf('%s (%s)', __('Selected date is earlier than permitted'), Format::date($min->getTimestamp(), false, false, $min->getTimezone()->getName() ?: 'UTC') ); } elseif ($max and $datetime > $max) { $this->_errors[] = sprintf('%s (%s)', __('Selected date is later than permitted'), Format::date($max->getTimestamp(), false, false, $max->getTimezone()->getName() ?: 'UTC') ); } } // SearchableField interface ------------------------------ function getSearchMethods() { return array( 'set' => __('has a value'), 'nset' => __('does not have a value'), 'equal' => __('on'), 'nequal' => __('not on'), 'before' => __('before'), 'after' => __('after'), 'between' => __('between'), 'period' => __('period'), 'ndaysago' => __('in the last n days'), 'ndays' => __('in the next n days'), 'future' => __('in the future'), 'past' => __('in the past'), 'distfut' => __('more than n days from now'), 'distpast' => __('more than n days ago'), ); } function getSearchMethodWidgets() { $config_notime = $config = $this->getConfiguration(); $config_notime['time'] = false; $nday_form = function($x=5) { return array( 'until' => new TextboxField(array( 'configuration' => array('validator'=>'number', 'size'=>4)) ), 'int' => new ChoiceField(array( 'default' => 'd', 'choices' => self::intervals($x), )), ); }; return array( 'set' => null, 'nset' => null, 'past' => null, 'future' => null, 'equal' => array('DatetimeField', array( 'configuration' => $config_notime, )), 'nequal' => array('DatetimeField', array( 'configuration' => $config_notime, )), 'before' => array('DatetimeField', array( 'configuration' => $config, )), 'after' => array('DatetimeField', array( 'configuration' => $config, )), 'between' => array('InlineformField', array( 'form' => array( 'left' => new DatetimeField($config + array('required' => true)), 'text' => new FreeTextField(array( 'configuration' => array('content' => __('and'))) ), 'right' => new DatetimeField($config + array('required' => true)), ), 'configuration' => array( 'error' => '', ), )), 'period' => array('ChoiceField', array( 'choices' => self::periods(), )), 'ndaysago' => array('InlineformField', array('form'=>$nday_form())), 'ndays' => array('InlineformField', array('form'=>$nday_form())), 'distfut' => array('InlineformField', array('form'=>$nday_form())), 'distpast' => array('InlineformField', array('form'=>$nday_form())), ); } function getSearchQ($method, $value, $name=false) { global $cfg; static $intervals = array( 'm' => 'MONTH', 'w' => 'WEEK', 'd' => 'DAY', 'h' => 'HOUR', 'i' => 'MINUTE', ); $name = $name ?: $this->get('name'); $now = SqlFunction::NOW(); $config = $this->getConfiguration(); if (is_int($value)) $value = DateTime::createFromFormat('U', !$config['gmt'] ? Misc::gmtime($value) : $value) ?: $value; elseif (is_string($value) && strlen($value) > 2) $value = Format::parseDateTime($value) ?: $value; switch ($method) { 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, )); case 'future': $value = $now; case 'after': return new Q(array("{$name}__gte" => $value)); case 'past': $value = $now; case 'before': return new Q(array("{$name}__lt" => $value)); case 'between': $left = Format::parseDateTime($value['left']); $right = Format::parseDateTime($value['right']); if (!$left || !$right) return null; // TODO: allow time selection for between $left = $left->setTime(00, 00, 00); $right = $right->setTime(23, 59, 59); // Convert time to db timezone $dbtz = new DateTimeZone($cfg->getDbTimezone()); $left->setTimezone($dbtz); $right->setTimezone($dbtz); return new Q(array( "{$name}__gte" => $left->format('Y-m-d H:i:s'), "{$name}__lte" => $right->format('Y-m-d H:i:s'), )); case 'ndaysago': $int = $intervals[$value['int'] ?: 'd'] ?: 'DAY'; $interval = new SqlInterval($int, $value['until']); return new Q(array( "{$name}__range" => array($now->minus($interval), $now), )); case 'ndays': $int = $intervals[$value['int'] ?: 'd'] ?: 'DAY'; $interval = new SqlInterval($int, $value['until']); return new Q(array( "{$name}__range" => array($now, $now->plus($interval)), )); // Distant past and future ranges case 'distpast': $int = $intervals[$value['int'] ?: 'd'] ?: 'DAY'; $interval = new SqlInterval($int, $value['until']); return new Q(array( "{$name}__lte" => $now->minus($interval), )); case 'distfut': $int = $intervals[$value['int'] ?: 'd'] ?: 'DAY'; $interval = new SqlInterval($int, $value['until']); return new Q(array( "{$name}__gte" => $now->plus($interval), )); case 'period': // User's effective timezone $tz = new DateTimeZone($cfg->getTimezone()); // Get the period range boundaries in user's tz $period = Misc::date_range($value, Misc::gmtime('now'), $tz); // Convert boundaries to db time $dbtz = new DateTimeZone($cfg->getDbTimezone()); $start = $period->start->setTimezone($dbtz); $end = $period->end->setTimezone($dbtz); // Set the range return new Q(array( "{$name}__range" => array( $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s') ) )); break; default: return parent::getSearchQ($method, $value, $name); } } 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 'distfut': return __('%1$s after %2$s from now' /* occurs after a window (like 3 days) */); case 'distpast': return __('%1$s before %2$s ago' /* occurs previous to a window (like 3 days) */); case 'between': return __('%1$s between %2$s and %3$s'); case 'future': return __('%1$s is in the future'); case 'past': return __('%1$s is in the past'); case 'period': return __('%1$s is %2$s'); default: return parent::describeSearchMethod($method); } } function describeSearch($method, $value, $name=false) { $name = $name ?: $this->get('name'); $desc = $this->describeSearchMethod($method); switch ($method) { case 'between': return sprintf($desc, $name, $this->toString($value['left']), $this->toString($value['right'])); case 'ndays': case 'ndaysago': case 'distfut': case 'distpast': $interval = sprintf('%s %s', $value['until'], self::intervals($value['until'], $value['int'])); return sprintf($desc, $name, $interval); break; case 'future': case 'past': return sprintf($desc, $name); case 'before': case 'after': return sprintf($desc, $name, $this->toString($value)); case 'period': return sprintf($desc, $name, self::periods($value) ?: $value); default: return parent::describeSearch($method, $value, $name); } } function supportsQuickFilter() { return true; } function getQuickFilterChoices() { return array( 'h' => __('Today'), 'm' => __('Tomorrow'), 'g' => __('Yesterday'), 'l7' => __('Last 7 days'), 'l30' => __('Last 30 days'), 'n7' => __('Next 7 days'), 'n30' => __('Next 30 days'), /* Ugh. These boundaries are so difficult in SQL 'w' => __('This Week'), 'm' => __('This Month'), 'lw' => __('Last Week'), 'lm' => __('Last Month'), 'nw' => __('Next Week'), 'nm' => __('Next Month'), */ ); } function applyQuickFilter($query, $qf_value, $name=false) { $name = $name ?: $this->get('name'); $now = SqlFunction::NOW(); $midnight = Misc::dbtime(time() - (time() % 86400)); switch ($qf_value) { case 'l7': return $query->filter([ "{$name}__range" => array($now->minus(SqlInterval::DAY(7)), $now), ]); case 'l30': return $query->filter([ "{$name}__range" => array($now->minus(SqlInterval::DAY(30)), $now), ]); case 'n7': return $query->filter([ "{$name}__range" => array($now, $now->plus(SqlInterval::DAY(7))), ]); case 'n30': return $query->filter([ "{$name}__range" => array($now, $now->plus(SqlInterval::DAY(30))), ]); case 'g': $midnight -= 86400; // Fall through to the today case case 'm': if ($qf_value === 'm') $midnight += 86400; // Fall through to the today case case 'h': $midnight = DateTime::createFromFormat('U', $midnight); return $query->filter([ "{$name}__range" => array($midnight, SqlExpression::plus($midnight, SqlInterval::DAY(1))), ]); } } } /** * TimeField for time selection * */ class TimeField extends FormField { static $widget = 'TimePickerWidget'; var $start = null; var $end = null; function getTimeZone() { global $cfg; $config = $this->getConfiguration(); return new DateTimeZone($config['timezone'] ?: $cfg->getTimezone()); } } /** * 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; } function isEditableToStaff() { return $this->isVisibleToStaff(); } function isEditableToUsers() { return $this->isVisibleToUsers(); } } class ThreadEntryField extends FormField { static $widget = 'ThreadEntryWidget'; function isChangeable() { return false; } function isBlockLevel() { return true; } function isPresentationOnly() { return true; } function getMedia() { $config = $this->getConfiguration(); $media = parent::getMedia() ?: array(); if ($config['attachments']) $media = array_merge_recursive($media, FileUploadWidget::$media); return $media; } function getConfiguration() { global $cfg; $config = parent::getConfiguration(); $config['html'] = (bool) ($cfg && $cfg->isRichTextEnabled()); return $config; } 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->getLocal('hint')) $this->set('placeholder', Format::striptags($hint)); $this->set('hint', null); $widget = parent::getWidget($widgetClass); return $widget; } } class TopicField extends ChoiceField { var $topics; var $_choices; function getTopics() { global $thisstaff; if (!isset($this->topics)) $this->topics = $thisstaff->getTopicNames(); return $this->topics; } function getTopic($id) { if ($this->getTopics() && isset($this->topics[$id])) return Topic::lookup($id); } function getWidget($widgetClass=false) { $default = $this->get('default'); $widget = parent::getWidget($widgetClass); if ($widget->value instanceof Topic) $widget->value = $widget->value->getId(); elseif (!isset($widget->value) && $default) $widget->value = $default; return $widget; } function hasIdValue() { return true; } function getChoices($verbose=false, $options=array()) { if (!isset($this->_choices)) { $this->_choices = $this->getTopics(); } return $this->_choices; } function parse($id) { return $this->to_php(null, $id); } function to_php($value, $id=false) { if ($value instanceof Topic) return $value; if (is_array($id)) { reset($id); $id = key($id); } elseif (is_array($value)) list($value, $id) = $value; elseif ($id === false) $id = $value; return $this->getTopic($id); } function to_database($topic) { return ($topic instanceof Topic) ? array($topic->getName(), $topic->getId()) : $topic; } function display($topic, &$styles=null) { if (!$topic instanceof Topic) return parent::display($topic); return Format::htmlchars($topic->getName()); } function toString($value) { if (!($value instanceof Topic) && is_numeric($value)) $value = $this->getTopic($value); return ($value instanceof Topic) ? $value->getName() : $value; } function whatChanged($before, $after) { return parent::whatChanged($before, $after); } function searchable($value) { return null; } function getKeys($value) { return ($value instanceof Topic) ? array($value->getId()) : null; } function asVar($value, $id=false) { return $this->to_php($value, $id); } function getConfiguration() { global $cfg; $config = parent::getConfiguration(); if (!isset($config['default'])) $config['default'] = $cfg->getDefaultTopicId(); return $config; } } class SLAField extends ChoiceField { var $slas; var $_choices; function getSLAs() { if (!isset($this->slas)) $this->slas = SLA::objects(); return $this->slas; } function getSLA($id) { if ($this->getSLAs() && ($s=$this->slas->findFirst(array('id' => $id)))) return $s; return SLA::lookup($id); } function getWidget($widgetClass=false) { $default = $this->get('default'); $widget = parent::getWidget($widgetClass); if ($widget->value instanceof SLA) $widget->value = $widget->value->getId(); elseif (!isset($widget->value) && $default) $widget->value = $default; return $widget; } function hasIdValue() { return true; } function getChoices($verbose=false, $options=array()) { if (!isset($this->_choices)) { $choices = array(); foreach ($this->getSLAs() as $s) $choices[$s->getId()] = $s->getName(); $this->_choices = $choices; } return $this->_choices; } function parse($id) { return $this->to_php(null, $id); } function to_php($value, $id=false) { if ($value instanceof SLA) return $value; if (is_array($id)) { reset($id); $id = key($id); } elseif (is_array($value)) list($value, $id) = $value; elseif ($id === false) $id = $value; return $this->getSLA($id); } function to_database($sla) { return ($sla instanceof SLA) ? array($sla->getName(), $sla->getId()) : $sla; } function display($sla, &$styles=null) { if (!$sla instanceof SLA) return parent::display($sla); return Format::htmlchars($sla->getName()); } function toString($value) { if (!($value instanceof SLA) && is_numeric($value)) $value = $this->getSLA($value); return ($value instanceof SLA) ? $value->getName() : $value; } function whatChanged($before, $after) { return parent::whatChanged($before, $after); } function searchable($value) { return null; } function getKeys($value) { return ($value instanceof SLA) ? array($value->getId()) : null; } function asVar($value, $id=false) { return $this->to_php($value, $id); } function getConfiguration() { global $cfg; $config = parent::getConfiguration(); if (!isset($config['default'])) $config['default'] = $cfg->getDefaultSLAId(); return $config; } } class PriorityField extends ChoiceField { var $priorities; var $_choices; function getPriorities() { if (!isset($this->priorities)) $this->priorities = Priority::objects(); return $this->priorities; } function getPriority($id) { if ($this->getPriorities() && ($p=$this->priorities->findFirst(array('priority_id' => $id)))) return $p; return Priority::lookup($id); } 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, $options=array()) { if (!isset($this->_choices)) { $choices = array(); foreach ($this->getPriorities() as $p) $choices[$p->getId()] = $p->getDesc(); $this->_choices = $choices; } return $this->_choices; } function parse($id) { return $this->to_php(null, $id); } function to_php($value, $id=false) { if ($value instanceof Priority) return $value; if (is_array($id)) { reset($id); $id = key($id); } elseif (is_array($value)) { list($value, $id) = $value; } elseif ($id === false && is_numeric($value)) $id = $value; if (is_numeric($id)) return $this->getPriority($id); return $value; } function to_database($value) { if ($value instanceof Priority) return array($value->getDesc(), $value->getId()); if (is_array($value)) return array(current($value), key($value)); return $value; } function display($prio, &$styles=null) { if (!$prio instanceof Priority) return parent::display($prio); if (is_array($styles)) $styles += array( 'background-color' => $prio->getColor() ); return Format::htmlchars($prio->getDesc()); } function toString($value) { return ($value instanceof Priority) ? $value->getDesc() : $value; } function whatChanged($before, $after) { return parent::whatChanged($before, $after); } function searchable($value) { // Priority isn't searchable this way return null; } function getKeys($value) { return ($value instanceof Priority) ? array($value->getId()) : null; } function asVar($value, $id=false) { return $this->to_php($value, $id); } function getConfigurationOptions() { $choices = $this->getChoices(); 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; } function applyOrderBy($query, $reverse=false, $name=false) { if ($query->model == 'Ticket' && $name == 'cdata__priority') { // Order by the priority urgency field $col = 'cdata__:priority__priority_urgency'; $reverse = !$reverse; } else { $col = $name ?: CustomQueue::getOrmPath($this->get('name'), $query); } if ($reverse) $col = "-$col"; return $query->order_by($col); } } FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() { return array( 'priority' => array(__('Priority Level'), 'PriorityField'), ); }); class TimezoneField extends ChoiceField { static $widget = 'TimezoneWidget'; function hasIdValue() { return false; } function getChoices($verbose=false, $options=array()) { global $cfg; $choices = array(); foreach (DateTimeZone::listIdentifiers() as $zone) $choices[$zone] = str_replace('/',' / ',$zone); return $choices; } function whatChanged($before, $after) { return parent::whatChanged($before, $after); } function searchable($value) { return null; } function getConfigurationOptions() { return array( 'autodetect' => new BooleanField(array( 'id'=>1, 'label'=>__('Auto Detect'), 'required'=>false, 'default'=>true, 'configuration'=>array( 'desc'=>__('Add Auto Detect Button')) )), '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), )), ); } } class DepartmentField extends ChoiceField { function getWidget($widgetClass=false) { $widget = $this->_widget ?: parent::getWidget($widgetClass); if (is_object($widget->value)) $widget->value = $widget->value->getId(); return $widget; } function hasIdValue() { return true; } function getChoices($verbose=false, $options=array()) { global $cfg, $thisstaff; $config = $this->getConfiguration(); $staff = $config['staff'] ?: $thisstaff; $selected = $this->getWidget(); if($selected && $selected->value) { if(is_array($selected->value)) { foreach ($selected->value as $k => $v) { $current_id = $k; $current_name = $v; } } else { $current_id = $selected->value; $current_name = Dept::getNameById($current_id); } } $choices = array(); //get all depts unfiltered $depts = $config['hideDisabled'] ? Dept::getDepartments(array('activeonly' => true)) : Dept::getDepartments(null, true, Dept::DISPLAY_DISABLED); //get staff depts based on permissions if ($staff) { $active = $staff->getDepartmentNames(true); if ($staff->hasPerm(Dept::PERM_DEPT)) return $depts; } //filter custom department fields when there is no staff else { $userDepts = Dept::getDepartments(array('publiconly' => true, 'activeonly' => true)); return $userDepts; } //add selected dept to list if($current_id) $active[$current_id] = $current_name; else return $active; foreach ($depts as $id => $name) { $choices[$id] = $name; if(!array_key_exists($id, $active) && $current_id) unset($choices[$id]); } return $choices; } function display($dept, &$styles=null) { if (!is_numeric($dept) && is_string($dept)) return Format::htmlchars($dept); elseif ($dept instanceof Dept) return Format::htmlchars($dept->getName()); return parent::display($dept); } function parse($id) { return $this->to_php(null, $id); } function getValue() { if (($value = parent::getValue()) && ($id=$this->getClean())) return is_array($value) ? $value[$id] : $value; } function to_php($value, $id=false) { if ($id) { if (is_array($id)) { reset($id); $id = key($id); } return $id; } else { return $value; } } function to_database($dept) { if ($dept instanceof Dept) return array($dept->getName(), $dept->getId()); if (!is_array($dept)) { $choices = $this->getChoices(); if (in_array($dept, $choices)) { $deptId = array_search($dept, $choices); $dept = array($dept, $deptId); } } return $dept ?: array(); } function toString($value) { if (!is_array($value)) $value = $this->getChoice($value); if (is_array($value)) return implode(', ', $value); return (string) $value; } function getChoice($value) { $choices = $this->getChoices(); $selection = array(); if ($value && is_array($value)) { $selection = $value; } elseif (isset($choices[$value])) { $selection[] = $choices[$value]; } return $selection; } 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 = null; var $_criteria = null; function getWidget($widgetClass=false) { $widget = parent::getWidget($widgetClass); $value = $widget->value; if (is_object($value)) { $id = $value->getId(); if ($value instanceof Staff) $widget->value = 's'.$id; elseif ($value instanceof Team) $widget->value = 't'.$id; } return $widget; } function getCriteria() { if (!isset($this->_criteria)) { $this->_criteria = array('available' => true, 'namesOnly' => true); if (($c=parent::getCriteria())) $this->_criteria = array_merge($this->_criteria, $c); } return $this->_criteria; } function hasIdValue() { return true; } function setChoices($choices) { $this->_choices = $choices; } function display($value) { if ($this->getAnswer() && is_string($this->getAnswer()->value)) { $v = JsonDataParser::parse($this->getAnswer()->value); if (is_array($v)) $value = $v[key($v)]; } return $value; } function getAssignees($options=array()) { global $thisstaff; $config = $this->getConfiguration(); $criteria = $this->getCriteria(); $dept = $config['dept'] ?: null; $staff = $config['staff'] ?: $thisstaff; $assignees = array(); switch (strtolower($config['target'])) { case 'agents': if ($dept) foreach ($dept->getAssignees(array('staff' => $staff)) as $a) $assignees['s'.$a->getId()] = $a; else foreach (Staff::getStaffMembers(array('staff' => $staff)) as $id => $name) $assignees['s'.$id] = $name; break; case 'teams': foreach (Team::getActiveTeams() ?: array() as $id => $name) $assignees['t'.$id] = $name; break; default: // both agents and teams $assignees = array( __('Agents') => new ArrayObject(), __('Teams') => new ArrayObject()); $A = current($assignees); $criteria = $this->getCriteria(); $agents = array(); if ($dept) foreach ($dept->getAssignees(array('staff' => $staff)) as $a) $A['s'.$a->getId()] = $a; else foreach (Staff::getStaffMembers(array('staff' => $staff)) as $a => $name) $A['s'.$a] = $name; next($assignees); $T = current($assignees); if (($teams = Team::getActiveTeams())) foreach ($teams as $id => $name) $T['t'.$id] = $name; break; } return $assignees; } function getChoices($verbose=false, $options=array()) { if (!isset($this->_choices)) $this->_choices = $this->getAssignees($options); return $this->_choices; } function getChoice($value) { $choices = $this->getChoices(); $selection = array(); if ($value && is_object($value)) { $keys = null; if ($value instanceof Staff) $keys = array('Agents', 's'.$value->getId()); elseif ($value instanceof Team) $keys = array('Teams', 't'.$value->getId()); if ($keys && isset($choices[$keys[0]])) $selection = $choices[$keys[0]][$keys[1]]; if (!empty($selection)) return $selection; } return parent::getChoice($value); } function getQuickFilterChoices() { $choices = $this->getChoices(); $names = array(); foreach ($choices as $value) { foreach ($value as $key => $value) $names[$key] = is_object($value) ? $value->name : $value; } return $names; } function getValue() { if (($value = parent::getValue()) && ($id=$this->getClean())) { $name = (is_object($value[key($value)]) && get_class($value[key($value)]) == 'AgentsName') ? $value[key($value)]->name : $value[key($value)]; $key = (($value[key($value)] instanceof AgentsName) ? 's' : 't').substr(key($value), 1); return array(array($key => $name), substr(key($value), 1)); } else return null; } function parse($id) { return $this->to_php(null, $id); } function to_php($value, $id=false) { if (is_string($value)) $value = JsonDataParser::parse($value) ?: $value; if (is_string($value) && strpos($value, ',')) { $values = array(); list($key, $V) = array_map('trim', explode(',', $value)); $values[$key] = $V; $value = $values; } $type = ''; if (is_array($id)) { reset($id); $id = key($id); $type = $id[0]; $id = substr($id, 1); } if (is_array($value)) { $type = key($value)[0]; if (!$id) $id = substr(key($value), 1); } if (!$type && is_numeric($value)) return Staff::lookup($value); switch ($type) { case 's': return Staff::lookup($id); case 't': return Team::lookup($id); case 'd': return Dept::lookup($id); default: return $id; } } function to_database($value) { if (is_object($value)) { $id = $value->getId(); if ($value instanceof Staff) { $key = 's'.$id; $name = $value->getName()->name; } elseif ($value instanceof Team) { $key = 't'.$id; $name = $value->getName(); } return JsonDataEncoder::encode(array($key => $name)); } if (is_array($value)) { return JsonDataEncoder::encode($value[0]); } return $value; } function toString($value) { return (string) $value; } function searchable($value) { return null; } function getKeys($value) { $value = $this->to_database($value); if (is_array($value)) return $value[0]; return (string) $value; } function asVar($value, $id=false) { $v = $this->to_php($value, $id); return $v ? $v->getName() : null; } function getChanges() { $new = $this->to_database($this->getValue()); $old = $this->to_database($this->answer ? $this->answer->getValue() : $this->get('default')); // Compare old and new return ($old == $new) ? false : array($old, $new); } function whatChanged($before, $after) { if ($before) $before = array($before->getName()); if ($after) $after = array($after->getName()); return parent::whatChanged($before, $after); } 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 { static $_states = array( 'open' => array( 'name' => /* @trans, @context "ticket state name" */ 'Open', 'verb' => /* @trans, @context "ticket state action" */ 'Open' ), 'closed' => array( 'name' => /* @trans, @context "ticket state name" */ 'Closed', 'verb' => /* @trans, @context "ticket state action" */ 'Close' ) ); // Private states static $_privatestates = array( 'archived' => array( 'name' => /* @trans, @context "ticket state name" */ 'Archived', 'verb' => /* @trans, @context "ticket state action" */ 'Archive' ), 'deleted' => array( '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, $options=array()) { static $_choices; $states = static::$_states; if ($this->options['private_too']) $states += static::$_privatestates; if (!isset($_choices)) { // Translate and cache the choices foreach ($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']); return $state; } 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']); } } 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, $options=array()) { $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; protected $files; static function getFileTypes() { static $filetypes; if (!isset($filetypes)) { if (function_exists('apcu_fetch')) { $key = md5(SECRET_SALT . GIT_VERSION . 'filetypes'); $filetypes = apcu_fetch($key); } if (!$filetypes) $filetypes = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml'); if ($key) apcu_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); $types = array(); foreach (self::getFileTypes() as $type=>$info) { $types[$type] = $info['description']; } global $cfg; 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(), 'choices'=>$sizes )), '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), )), 'strictmimecheck' => new BooleanField([ 'id' => 4, 'label'=>__('Strict Mime Type Check'), 'required' => false, 'default' => false, 'hint' => 'File Mime Type associations is OS dependent', 'configuration' => ['desc' => __('Enable strict Mime Type check')]]), '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')), )) ); } /** * 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']); $config = $this->getConfiguration(); if (!self::isValidFile($file, $config['strictmimecheck'])) Http::response(413, 'Invalid File'); 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']); $id = $F->getId(); // This file is allowed for attachment in this session $_SESSION[':uploadedFiles'][$id] = $F->getName(); return $id; } /** * 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 (!self::isValidFile($file, $config['strictmimecheck'])) throw new FileUploadError(__('Invalid File')); 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 (!isset($file['data']) && isset($file['data_cbk']) && is_callable($file['data_cbk'])) $file['data'] = $file['data_cbk'](); if (!isset($file['size']) && isset($file['data'])) { // 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; } /** * Strict mode can be enabled in Admin Panel > Settings > Tickets * * PS: Please note that the a mismatch can happen if the mime types * database is not up to date or a little different compared to what the * browser reports. **/ static function isValidFile($file, $strict = false) { // Strict mime check if ($strict && !empty($file['type']) && FileObject::mimecmp($file['tmp_name'], $file['type'])) return false; // Check invalid image hacks if ($file['tmp_name'] && stripos($file['type'], 'image/') === 0 && !exif_imagetype($file['tmp_name'])) return false; return true; } 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'], true)) return true; // Return true if all file types are allowed (.*) if (!$config['__extensions'] || in_array('.*', $config['__extensions'])) return true; $allowed = $config['__extensions']; $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); return ($ext && is_array($allowed) && in_array(".$ext", $allowed)); } function getAttachments() { 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', abs(crc32('E'.$this->get('id').$e->get('id')))), 'E'); } return $this->attachments ?: array(); } function setAttachments(GenericAttachments $att) { $this->attachments = $att; } function getFiles() { if (!isset($this->files)) { $files = array(); foreach ($this->getAttachments() as $a) { if ($a && ($f=$a->getFile())) $files[] = $f; } foreach ($this->getClean(false) ?: array() as $key => $value) $files[] = array('id' => $key, 'name' => $value); $this->files = $files; } return $this->files; } function getConfiguration() { global $cfg; $config = parent::getConfiguration(); // If no size present default to system setting $config['size'] ??= $cfg->getMaxFileSize(); $_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, '/')) { $mimetypes[$ext] = true; } 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->getAttachments(); if (isset($this->attachments) && $this->attachments) { $this->attachments->keepOnlyFileIds($value); } return JsonDataEncoder::encode($value) ?? NULL; } function parse($value) { return $value; } function to_php($value) { return is_array($value) ? $value : JsonDataParser::decode($value); } function display($value) { $links = array(); foreach ($this->getAttachments() as $a) { $links[] = sprintf('<a class="no-pjax" href="%s"><i class="icon-paperclip icon-flip-horizontal"></i> %s</a>', Format::htmlchars($a->file->getDownloadUrl()), Format::htmlchars($a->getFilename())); } return implode('<br/>', $links); } function toString($value) { $files = array(); foreach ($this->getFiles() as $f) { $files[] = $f->name; } return implode(', ', $files); } function db_cleanup($field=false) { if ($this->getAttachments()) { $this->attachments->deleteAll(); } } function asVar($value, $id=false) { if (($attachments = $this->getAttachments())) $attachments = $attachments->all(); return new FileFieldAttachments($attachments ?: array()); } function asVarType() { return 'FileFieldAttachments'; } function whatChanged($before, $after) { $B = (array) $before; $A = (array) $after; $added = array_diff($A, $B); $deleted = array_diff($B, $A); $added = Format::htmlchars(array_values($added)); $deleted = Format::htmlchars(array_values($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; } } class FileFieldAttachments { var $attachments; function __construct($attachments) { $this->attachments = $attachments; } function __toString() { $files = array(); foreach ($this->getAttachments() as $a) { $files[] = $a->getFilename(); } return implode(', ', $files); } function getAttachments() { return $this->attachments ?: array(); } function getVar($tag) { switch ($tag) { case 'names': return $this->__toString(); case 'files': throw new OOBContent(OOBContent::FILES, $this->getAttachments()); } } static function getVarScope() { return array( 'names' => __('List of file names'), 'files' => __('Attached files'), ); } } class ColorChoiceField extends FormField { static $widget = 'ColorPickerWidget'; } 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()) { $config = $this->getConfiguration(); $this->_errors[] = isset($config['error']) ? $config['error'] : __('Correct any errors below and try again.'); } } 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()); // Ensure unique, but predictable form and field IDs $form->setId(sprintf('%u', crc32($this->get('name')) >> 1)); } 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(); } } class Widget { static $media = null; function __construct($field) { $this->field = $field; $this->name = $field->getFormName(); $this->id = '_' . $this->name; } function parseValue() { $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)) $this->value = $this->field->value; } function getValue() { $data = $this->field->getSource(); foreach ($this->field->getFormNames() as $name) if (isset($data[$name])) return $data[$name]; return null; } /** * 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($id='%s') { return sprintf('%s.val()', $id); } /** * getJsComparator * * Used with the dependent fields to get comparison expression * */ function getJsComparator($value, $id) { if (strpos($value, '|') !== false) return sprintf('$.inArray(%s, %s) !== -1', $this->getJsValueGetter($id), JsonDataEncoder::encode(explode('|', $value))); return sprintf('%s == %s', $this->getJsValueGetter($id), JsonDataEncoder::encode($value)); } } class TextboxWidget extends Widget { static $input_type = 'text'; function render($options=array(), $extraConfig=false) { $config = $this->field->getConfiguration(); if (is_array($extraConfig)) { foreach ($extraConfig as $k=>$v) if (!isset($config[$k]) || !$config[$k]) $config[$k] = $v; } // Input attributes $attrs = array(); foreach ($config as $k => $v) { switch ($k) { case 'autocomplete': if (is_numeric($v)) $v = $v ? 'on' : 'off'; $attrs[$k] = '"'.$v.'"'; break; case 'disabled'; $attrs[$k] = '"disabled"'; break; case 'translatable': if ($v) $attrs['data-translate-tag'] = '"'.$v.'"'; break; case 'length': $k = 'maxlength'; case 'size': case 'maxlength': if ($v && is_numeric($v)) $attrs[$k] = '"'.$v.'"'; break; case 'class': case 'classes': $attrs['class'] = '"'.$v.'"'; break; case 'inputmode': case 'pattern': $attrs[$k] = '"'.$v.'"'; break; } } // autofocus $autofocus = ''; if (isset($config['autofocus'])) $autofocus = 'autofocus'; // placeholder $attrs['placeholder'] = sprintf('"%s"', Format::htmlchars($this->field->getLocal('placeholder', $config['placeholder']))); $type = static::$input_type; $types = array( 'email' => 'email', 'phone' => 'tel', ); if ($type == 'text' && isset($types[$config['validator']])) $type = $types[$config['validator']]; ?> <input type="<?php echo $type; ?>" id="<?php echo $this->id; ?>" <?php echo $autofocus .' '.Format::array_implode('=', ' ', array_filter($attrs)); ?> name="<?php echo $this->name; ?>" value="<?php echo Format::htmlchars($this->value, true); ?>"/> <?php } } class TextboxSelectionWidget extends TextboxWidget { //TODO: Support multi-input e.g comma separated inputs function render($options=array(), $extraConfig=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 (isset($this->field->value)) { $extra['placeholder'] = str_repeat('•', strlen($this->field->value)); } return parent::render($mode, $extra); } function parseValue() { parent::parseValue(); // Show empty box unless failed POST if ($_SERVER['REQUEST_METHOD'] != 'POST' || !$this->field->getForm()->isValid()) $this->value = ''; } } class TextareaWidget extends Widget { function render($options=array()) { $config = $this->field->getConfiguration(); // process textarea attributes $attrs = array(); foreach ($config as $k => $v) { switch ($k) { case 'rows': case 'cols': case 'length': case 'maxlength': if ($v && is_numeric($v)) $attrs[$k] = '"'.$v.'"';; break; case 'context': $attrs['data-root-context'] = '"'.$v.'"'; break; case 'class': // This might conflict with html attr below $attrs[$k] = '"'.$v.'"'; break; case 'html': if ($v) { $class = array('richtext', 'no-bar'); $class[] = @$config['size'] ?: 'small'; $attrs['class'] = '"'.implode(' ', $class).'"'; $this->value = Format::viewableImages($this->value); } break; } } // placeholder $attrs['placeholder'] = sprintf('"%s"', Format::htmlchars($this->field->getLocal('placeholder', $config['placeholder']))); ?> <span style="display:inline-block;width:100%"> <textarea <?php echo Format::array_implode('=', ' ', array_filter($attrs)); ?> id="<?php echo $this->id; ?>" name="<?php echo $this->name; ?>"><?php echo Format::htmlchars($this->value, ($config['html'])); ?></textarea> </span> <?php } function parseValue() { parent::parseValue(); if (isset($this->value)) { $value = $this->value; $config = $this->field->getConfiguration(); // Trim empty spaces based on text input type. // Preserve original input if not empty. if ($config['html']) $this->value = trim($value, " <>br/\t\n\r") ? $value : ''; else $this->value = trim($value) ? $value : ''; } } } class PhoneNumberWidget extends Widget { function render($options=array()) { $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'); ?>: <input type="text" name="<?php echo $this->name; ?>-ext" value="<?php echo Format::htmlchars($ext); ?>" size="5"/> <?php } } function getValue() { $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 if ($ext) $ext = 'X'.$ext; return $base . $ext; } } class ChoicesWidget extends Widget { function render($options=array()) { $mode = null; if (isset($options['mode'])) $mode = $options['mode']; elseif (isset($this->field->options['render_mode'])) $mode = $this->field->options['render_mode']; if ($mode == 'view') { $val = (string) $this->field; echo sprintf('<span id="field_%s" %s >%s</span>', $this->id, $val ? '': 'class="faded"', $val ?: __('None')); return; } $config = $this->field->getConfiguration(); if ($mode == 'search') { $config['multiselect'] = true; } // Determine the value for the default (the one listed if nothing is // selected) $choices = $this->field->getChoices(true, $options); $prompt = ($config['prompt']) ? $this->field->getLocal('prompt', $config['prompt']) : __('Select' /* Used as a default prompt for a custom drop-down list */); $have_def = false; // We don't consider the 'default' when rendering in 'search' mode if (!strcasecmp($mode, 'search')) { $def_val = $prompt; } else { $showdefault = true; if ($mode != 'create') $showdefault = false; $def_key = $this->field->get('default'); if (!$def_key && isset($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 && !$showdefault) ? $choices[$def_key] : $prompt; } $values = $this->value; if (!is_array($values) && isset($values)) { $values = array($values => $this->field->getChoice($values)); } if (!is_array($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 Format::htmlchars($prompt); ?>" <?php if ($config['disabled']) echo ' disabled="disabled"'; ?> <?php if ($config['multiselect']) echo ' multiple="multiple"'; ?>> <?php if ($showdefault || (!$have_def && !$config['multiselect'])) { ?> <option value="<?php echo $showdefault ? '' : $def_key; ?>">— <?php echo $def_val; ?> —</option> <?php } $this->emitChoices($choices, $values, $have_def, $def_key); ?> </select> <?php if ($config['multiselect']) { ?> <script type="text/javascript"> $(function() { $("#<?php echo $this->id; ?>") .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 Format::htmlchars($name); ?></option> <?php } } function emitComplexChoices($choices, $values=array(), $have_def=false, $def_key=null) { foreach ($choices as $label => $group) { if (!count($group)) continue; ?> <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 Format::htmlchars($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; elseif (!$k && $v) return $v; } } } return $values; } function getJsValueGetter($id='%s') { return sprintf('%s.find(":selected").val()', $id); } } /** * 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) { static $uid = 1; if (!isset($this->value)) $this->value = $this->field->get('default'); $config = $this->field->getConfiguration(); $type = $config['multiple'] ? 'checkbox' : 'radio'; $classes = array('checkbox'); if (isset($config['classes'])) $classes = array_merge($classes, (array) $config['classes']); foreach ($choices as $k => $v) { if (is_array($v)) { $this->renderSectionBreak($k); $this->emitChoices($v); continue; } $id = sprintf("%s-%s", $this->id, $uid++); ?> <label class="<?php echo implode(' ', $classes); ?>" for="<?php echo $id; ?>"> <input id="<?php echo $id; ?>" 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) { echo Format::viewableImages($v); } ?> </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 } } } /** * TimezoneWidget extends ChoicesWidget to add auto-detect and select2 search * options * **/ class TimezoneWidget extends ChoicesWidget { function render($options=array()) { parent::render($options); $config = $this->field->getConfiguration(); if (@$config['autodetect']) { ?> <button type="button" class="action-button" onclick="javascript: $('head').append($('<script>').attr('src', '<?php echo ROOT_PATH; ?>js/jstz.min.js')); var recheck = setInterval(function() { if (window.jstz !== undefined) { clearInterval(recheck); var zone = jstz.determine(); $('#<?php echo $this->id; ?>').val(zone.name()).trigger('change'); } }, 100); return false;" style="vertical-align:middle"> <i class="icon-map-marker"></i> <?php echo __('Auto Detect'); ?> </button> <?php } ?> <script type="text/javascript"> $(function() { $('#<?php echo $this->id; ?>').select2({ allowClear: true, width: '300px' }); }); </script> <?php } } class CheckboxWidget extends Widget { function __construct($field) { parent::__construct($field); $this->name = '_field-checkboxes'; } function render($options=array()) { $config = $this->field->getConfiguration(); if (!isset($this->value)) $this->value = $this->field->get('default'); $classes = array('checkbox'); if (isset($config['classes'])) $classes = array_merge($classes, (array) $config['classes']); ?> <label class="<?php echo implode(' ', $classes); ?>"> <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']); } ?> </label> <?php } function getValue() { $data = $this->field->getSource(); if (is_array($data)) { if (isset($data[$this->name])) return @in_array($this->field->get('id'), $data[$this->name]); // initial value set on source if (isset($data[$this->field->get('id')])) return $data[$this->field->get('id')]; } if (!$data && isset($this->value)) return $this->value; return parent::getValue(); } function getJsValueGetter($id='%s') { return sprintf('%s.is(":checked")', $id); } } class DatetimePickerWidget extends Widget { function render($options=array()) { global $cfg; $config = $this->field->getConfiguration(); $timezone = $this->field->getTimezone(); $dateFormat = $cfg->getDateFormat(true); $timeFormat = $cfg->getTimeFormat(true); if (!isset($this->value) && ($default=$this->field->get('default'))) $this->value = $default; if ($this->value == 0) $this->value = ''; if ($this->value) { $datetime = Format::parseDateTime($this->value); if ($config['time']) // Convert to user's timezone for update. $datetime->setTimezone($timezone); // Get formatted date $this->value = Format::date($datetime->getTimestamp(), false, false, $timezone ? $timezone->getName() : 'UTC'); // Get formatted time if ($config['time']) { $this->value .=' '.Format::time($datetime->getTimestamp(), false, $timeFormat, $timezone ? $timezone->getName() : 'UTC'); } } else { // For timezone display purposes $datetime = new DateTime('now'); $datetime->setTimezone($timezone); } ?> <input type="text" name="<?php echo $this->name; ?>" id="<?php echo $this->id; ?>" style="display:inline-block;width:auto" value="<?php echo $this->value; ?>" size="<?php $config['time'] ? 20 : 12; ?>" autocomplete="off" class="dp" /> <?php // Timezone hint // Show timzone hit by default but allow field to turn it off. $showtimezone = true; if (isset($config['showtimezone'])) $showtimezone = $config['showtimezone']; if ($datetime && $showtimezone) { echo sprintf(' <span class="faded">(<a href="#" data-placement="top" data-toggle="tooltip" title="%s">%s</a>)</span>', $datetime->getTimezone()->getName(), $datetime->format('T')); } ?> <script type="text/javascript"> $(function() { $('input[name="<?php echo $this->name; ?>"]').<?php echo $config['time'] ? 'datetimepicker':'datepicker';?>({ <?php if ($dt=$this->field->getMinDateTime()) echo sprintf("minDate: new Date(%s),\n", $dt->format('U')*1000); if ($dt=$this->field->getMaxDateTime()) echo sprintf("maxDate: new Date(%s),\n", $dt->format('U')*1000); elseif (!$config['future']) echo "maxDate: new Date().getTime(),\n"; // Set time options if ($config['time']) { // Set Timezone echo sprintf("timezone: %s,\n", ($datetime->getOffset()/60)); echo sprintf(" controlType: 'select',\n timeInput: true,\n timeFormat: \"%s\",\n", Format::dtfmt_php2js($timeFormat)); } ?> numberOfMonths: 2, showButtonPanel: true, buttonImage: './images/cal.png', showOn:'both', dateFormat: '<?php echo Format::dtfmt_php2js($dateFormat); ?>' }); }); </script> <?php } /** * Function: getValue * Combines the datepicker date value and the time dropdown selected * time value into a single date and time string value in DateTime::W3C */ function getValue() { global $cfg; if ($value = parent::getValue()) { if (($dt = Format::parseDateTime($value))) { // Effective timezone for the selection if (($timezone = $this->field->getTimezone())) $dt->setTimezone($timezone); // Format date time to universal format $value = $dt->format('Y-m-d H:i:s T'); } } return $value; } } class TimePickerWidget extends Widget { function render($options=array()) { $config = $this->field->getConfiguration(); if (!isset($this->value) && ($default=$this->field->get('default'))) $this->value = $default; // For timezone display purposes only - for now $datetime = new DateTime('now'); // Selection timezone $datetime->setTimezone($this->field->getTimeZone()); if ($this->value) { // TODO: Reformat time here to match settings } ?> <input type="text" name="<?php echo $this->name; ?>" id="<?php echo $this->id; ?>" style="display:inline-block;width:auto" value="<?php echo $this->value; ?>" size="10" autocomplete="off" /> <?php // Timezone hint // Show timzone hit by default but allow field to turn it off. $showtimezone = true; if (isset($config['showtimezone'])) $showtimezone = $config['showtimezone']; if ($showtimezone) { echo sprintf(' <span class="faded">(<a href="#" data-placement="top" data-toggle="tooltip" title="%s">%s</a>)</span>', $datetime->getTimezone()->getName(), $datetime->format('T')); } ?> <script type="text/javascript"> $(function() { $('input[name="<?php echo $this->name; ?>"]').timepicker({ <?php // Set time options echo sprintf(" controlType: 'select',\n timeInput: true,\n timeFormat: \"%s\",\n", "hh:mm tt"); echo sprintf("timezone: %s\n", ($datetime->getOffset()/60)); ?> }); }); </script> <?php } /** * Function: getValue */ function getValue() { global $cfg; if ($value = parent::getValue()) { // TODO: Return ISO format. } return $value; } } class SectionBreakWidget extends Widget { function render($options=array()) { ?><div class="form-header section-break"><h3><?php echo Format::htmlchars($this->field->getLocal('label')); ?></h3><em><?php echo Format::display($this->field->getLocal('hint')); ?></em></div> <?php } } class ThreadEntryWidget extends Widget { function render($options=array()) { $config = $this->field->getConfiguration(); $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->name; ?>" placeholder="<?php echo Format::htmlchars($this->field->get('placeholder')); ?>" class="<?php if ($config['html']) echo 'richtext'; ?> draft draft-delete" <?php echo $attrs; ?> cols="21" rows="8" style="width:80%;"><?php echo ThreadEntryBody::clean($this->value ?: $draft); ?></textarea> <?php 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; } function parseValue() { parent::parseValue(); if (isset($this->value)) { $value = $this->value; $config = $this->field->getConfiguration(); // Trim spaces based on text input type. // Preserve original input if not empty. if ($config['html']) $this->value = trim($value, " <>br/\t\n\r") ? $value : ''; else $this->value = trim($value) ? $value : ''; } } } class FileUploadWidget extends Widget { static $media = array( 'css' => array( '/css/filedrop.css', ), ); function render($options=array()) { $config = $this->field->getConfiguration(); $name = $this->field->getFormName(); $id = substr(md5(spl_object_hash($this)), 10); $mimetypes = array_filter($config['__mimetypes'], function($t) { return strpos($t, '/') !== false; } ); $maxfilesize = ($config['size'] ?: 1048576) / 1048576; $files = array(); $new = $this->field->getClean(false); foreach ($this->field->getAttachments() as $att) { unset($new[$att->file_id]); $files[] = array( 'id' => $att->file->getId(), 'name' => $att->getFilename(), 'type' => $att->file->getType(), 'size' => $att->file->getSize(), 'download_url' => $att->file->getDownloadUrl(), ); } // Add in newly added files not yet saved (if redisplaying after an // error) if ($new) { $F = AttachmentFile::objects() ->filter(array('id__in' => array_keys($new))) ->all(); foreach ($F as $f) { $f->tmp_name = $new[$f->getId()]; $files[] = array( 'id' => $f->getId(), 'name' => $f->getName(), 'type' => $f->getType(), 'size' => $f->getSize(), 'download_url' => $f->getDownloadUrl(), ); } } //see if the attachment is saved in the session for this specific field if ($sessionAttachment = $_SESSION[':form-data'][$this->field->get('name')]) { $F = AttachmentFile::objects() ->filter(array('id__in' => array_keys($sessionAttachment))) ->all(); foreach ($F as $f) { $f->tmp_name = $sessionAttachment[$f->getId()]; $files[] = array( 'id' => $f->getId(), 'name' => $f->getName(), 'type' => $f->getType(), 'size' => $f->getSize(), 'download_url' => $f->getDownloadUrl(), ); } } // Set default $field_id $field_id = $this->field->get('id'); // Get Form Type $type = $this->field->getForm()->type; // Determine if for Ticket/Task/Custom if ($type && !is_numeric($field_id)) { if ($type == 'T') $field_id = 'ticket/attach'; elseif ($type == 'A') $field_id = 'task/attach'; } ?><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']); ?>"/> </div></div> <script type="text/javascript"> $(function(){$('#<?php echo $id; ?> .dropzone').filedropbox({ url: 'ajax.php/form/upload/<?php echo $field_id; ?>', link: $('#<?php echo $id; ?>').find('a.manual'), paramname: 'upload[]', fallback_id: 'file-<?php echo $id; ?>', allowedfileextensions: <?php echo JsonDataEncoder::encode( $config['__extensions'] ?: array()); ?>, allowedfiletypes: <?php echo JsonDataEncoder::encode( $mimetypes); ?>, maxfiles: <?php echo $config['max'] ?: 20; ?>, maxfilesize: <?php echo str_replace(',', '.', $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) { try { $F = $this->field->uploadFile($file); $ids[$F->getId()] = $F->getName(); } catch (FileUploadError $ex) {} } return $ids; } // Files uploaded here MUST have been uploaded by this user and // identified in the session // // If no value was sent, assume an empty list if (!($files = parent::getValue())) return array(); if ($_SERVER['REQUEST_METHOD'] == 'POST') { $_files = array(); foreach ($files as $info) { if (@list($id, $name) = explode(',', $info, 2)) $_files[$id] = $name; } $files = $_files; } $allowed = array(); // Files already attached to the field are allowed foreach ($this->field->getFiles() as $F) { // FIXME: This will need special porting in v1.10 $allowed[$F->id] = $F->getName(); } // New files uploaded in this session are allowed if (isset($_SESSION[':uploadedFiles'])) $allowed += $_SESSION[':uploadedFiles']; // Canned attachments initiated by this session if (isset($_SESSION[':cannedFiles'])) $allowed += $_SESSION[':cannedFiles']; // Parse the files and make sure it's allowed. foreach ($files as $id => $name) { if (!isset($allowed[$id])) continue; // Keep the values as the IDs $ids[$id] = $name ?: $allowed[$id]; } return $ids; } } class FileUploadError extends Exception {} class FreeTextField extends FormField { static $widget = 'FreeTextWidget'; protected $attachments; 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', 'label' => __('Attachments'), 'name'=>'files', 'configuration' => array('extensions'=>'') )), ); } function hasData() { return false; } function isBlockLevel() { return true; } function isEditableToStaff() { return $this->isVisibleToStaff(); } function isEditableToUsers() { return $this->isVisibleToUsers(); } /* utils */ function to_config($config) { if ($config && isset($config['attachments'])) $keepers = $config['attachments']; $this->getAttachments()->keepOnlyFileIds($keepers); return $config; } function db_cleanup($field=false) { if ($field && $this->getFiles()) $this->getAttachments()->deleteAll(); } function getAttachments() { if (!isset($this->attachments)) $this->attachments = GenericAttachments::forIdAndType($this->get('id'), 'I'); return $this->attachments ?: array(); } function getFiles() { if (!isset($this->files)) { $files = array(); if (($attachments=$this->getAttachments())) foreach ($attachments->all() as $a) $files[] = $a->getFile(); $this->files = $files; } return $this->files; } } class FreeTextWidget extends Widget { function render($options=array()) { $config = $this->field->getConfiguration(); $class = $config['classes'] ?: 'thread-body bleed'; ?><div class="<?php echo $class; ?>"><?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::display($hint); ?></em><?php } ?> <div><?php echo Format::viewableImages($config['content']); ?></div> </div> <?php if (($attachments = $this->field->getAttachments()) && count($attachments)) { ?> <section class="freetext-files"> <div class="title"><?php echo __('Related Resources'); ?></div> <?php foreach ($attachments->all() as $attach) { $filename = Format::htmlchars($attach->getFilename()); ?> <div class="file"> <a href="<?php echo $attach->file->getDownloadUrl(); ?>" target="_blank" download="<?php echo $filename; ?>" class="truncate no-pjax"> <i class="icon-file"></i> <?php echo $filename; ?> </a> </div> <?php } ?> </section> <?php } } } class ColorPickerWidget extends Widget { static $media = array( 'css' => array( 'css/spectrum.css', ), 'js' => array( 'js/spectrum.js', ), ); function render($options=array()) { ?><input type="color" id="<?php echo $this->id; ?>" name="<?php echo $this->name; ?>" value="<?php echo Format::htmlchars($this->value); ?>"/><?php } } class VisibilityConstraint { static $operators = array( 'eq' => 1, 'neq' => 1, ); 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) { if (!$this->constraint->constraints) return; $func = 'recheck_'.$field->getWidget()->id; $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) { if (!($field = $form->getField($f))) continue; echo sprintf('var %1$s = x = $("#%1$s");', $field->getWidget()->id); } $expression = $this->compileQ($this->constraint, $form); ?> if (<?php echo $expression; ?>) { target.slideDown('fast', function (){ $(this).trigger('show'); }); } else { target.slideUp('fast', function (){ $(this).trigger('hide'); }); } }; <?php foreach ($fields as $f) { if (!($field=$form->getField($f))) continue; $w = $field->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) { // Assume initial visibility if constraint is not provided. if (!$this->constraint->constraints) return $this->initial == self::VISIBLE; return $this->compileQPhp($this->constraint, $field); } static function splitFieldAndOp($field) { if (false !== ($last = strrpos($field, '__'))) { $op = substr($field, $last + 2); if (isset(static::$operators[$op])) $field = substr($field, 0, strrpos($field, '__')); } return array($field, $op); } 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) = self::splitFieldAndOp($c); $field = $form->getField($f); $wval = $field ? $field->getClean() : null; $values = explode('|', $value); switch ($op) { case 'neq': $expr[] = ($wval && !in_array($wval, $values) && $field->isVisible()); break; case 'eq': case null: $expr[] = (in_array($wval, $values) && $field->isVisible()); } } } $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) = self::splitFieldAndOp($c); $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) = self::splitFieldAndOp($c); if (!($field=$form->getField($f))) continue; $widget = $field->getWidget(); $id = $widget->id; switch ($op) { case 'neq': $expr[] = sprintf('(%s.is(":visible") && !(%s))', $id, $widget->getJsComparator($value, $id)); break; case 'eq': case null: $expr[] = sprintf('(%s.is(":visible") && (%s))', $id, $widget->getJsComparator($value, $id)); } } } $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; var $_assignees = null; 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, ), ), ) ), 'refer' => new BooleanField(array( 'id'=>2, 'label'=>'', 'required'=>false, 'default'=>false, 'configuration'=>array( 'desc' => __('Maintain referral access to current assignees')) ) ), 'comments' => new TextareaField(array( 'id' => 3, 'label'=> '', 'required'=>false, 'default'=>'', 'configuration' => array( 'html' => true, 'size' => 'small', 'placeholder' => __('Optional reason for the assignment'), ), ) ), ); if (isset($this->_assignees)) $fields['assignee']->setChoices($this->_assignees); $this->setFields($fields); return $this->fields; } function getField($name) { if (($fields = $this->getFields()) && isset($fields[$name])) return $fields[$name]; } function isValid($include=false) { if (!parent::isValid($include) || !($f=$this->getField('assignee'))) return false; // Do additional assignment validation if (!($assignee = $this->getAssignee())) { $f->addError(__('Unknown assignee')); } elseif ($assignee instanceof Staff) { // Make sure the agent is available if (!$assignee->isAvailable()) $f->addError(__('Agent is unavailable for assignment')); } elseif ($assignee instanceof Team) { // Make sure the team is active and has members if (!$assignee->isActive()) $f->addError(__('Team is disabled')); elseif (!$assignee->getNumMembers()) $f->addError(__('Team does not have members')); } return !$this->errors(); } function render($options=array()) { 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 setAssignees($assignees) { $this->_assignees = $assignees; $this->_fields = array(); } function getAssignees() { return $this->_assignees; } function getAssignee() { if (!isset($this->_assignee)) $this->_assignee = $this->getField('assignee')->getClean(); return $this->_assignee; } function getComments() { return $this->getField('comments')->getClean(); } function refer() { return $this->getField('refer')->getClean(); } } class ClaimForm extends AssignmentForm { var $_fields; function setFields($fields) { $this->_fields = $fields; parent::setFields($fields); } function getFields() { if ($this->_fields) return $this->_fields; $fields = parent::getFields(); // Disable && hide assignee field selection if (isset($fields['assignee'])) { $visibility = new VisibilityConstraint( new Q(array()), VisibilityConstraint::HIDDEN); $fields['assignee']->set('visibility', $visibility); } // Change coments placeholder to reflect claim if (isset($fields['comments'])) { $fields['comments']->configure('placeholder', __('Optional reason for the claim')); } $this->setFields($fields); return $this->fields; } } class ReleaseForm extends Form { static $id = 'unassign'; function getFields() { if ($this->fields) return $this->fields; $fields = array( 'comments' => new TextareaField(array( 'id' => 1, 'label'=> '', 'required'=>false, 'default'=>'', 'configuration' => array( 'html' => true, 'size' => 'small', 'placeholder' => __('Optional reason for releasing assignment'), ), ) ), ); $this->setFields($fields); return $this->fields; } function getField($name) { if (($fields = $this->getFields()) && isset($fields[$name])) return $fields[$name]; } function isValid($include=false) { if (!parent::isValid($include)) return false; return !$this->errors(); } function getComments() { return $this->getField('comments')->getClean(); } } class MarkAsForm extends Form { static $id = 'markAs'; function getFields() { if ($this->fields) return $this->fields; $fields = array( 'comments' => new TextareaField(array( 'id' => 1, 'label'=> '', 'required'=>false, 'default'=>'', 'configuration' => array( 'html' => true, 'size' => 'small', 'placeholder' => __('Optional reason for marking ticket as (un)answered'), ), ) ), ); $this->setFields($fields); return $this->fields; } function getField($name) { if (($fields = $this->getFields()) && isset($fields[$name])) return $fields[$name]; } function isValid($include=false) { if (!parent::isValid($include)) return false; return !$this->errors(); } function getComments() { return $this->getField('comments')->getClean(); } } class ReferralForm extends Form { static $id = 'refer'; var $_target = null; var $_choices = null; var $_prompt = ''; function getFields() { if ($this->fields) return $this->fields; $fields = array( 'target' => new ChoiceField(array( 'id'=>1, 'label' => __('Referee'), 'flags' => hexdec(0X450F3), 'required' => true, 'validator-error' => __('Selection required'), 'choices' => array( 'agent' => __('Agent'), 'team' => __('Team'), 'dept' => __('Department'), ), ) ), 'agent' => new AgentSelectionField(array( 'id'=>2, 'label' => '', 'flags' => hexdec(0X450F3), 'required' => true, 'validator-error' => __('Agent selection required'), 'configuration'=>array('prompt'=>__('Select Agent')), 'visibility' => new VisibilityConstraint( new Q(array('target__eq'=>'agent')), VisibilityConstraint::HIDDEN ), ) ), 'team' => new ChoiceField(array( 'id'=>3, 'label' => '', 'flags' => hexdec(0X450F3), 'required' => true, 'validator-error' => __('Team selection required'), 'configuration'=>array('prompt'=>__('Select Team')), 'visibility' => new VisibilityConstraint( new Q(array('target__eq'=>'team')), VisibilityConstraint::HIDDEN ), ) ), 'dept' => new DepartmentField(array( 'id'=>4, 'label' => '', 'flags' => hexdec(0X450F3), 'required' => true, 'validator-error' => __('Dept. selection required'), 'configuration'=>array('prompt'=>__('Select Department')), 'visibility' => new VisibilityConstraint( new Q(array('target__eq'=>'dept')), VisibilityConstraint::HIDDEN ), ) ), 'comments' => new TextareaField(array( 'id' => 5, 'label'=> '', 'required'=>false, 'default'=>'', 'configuration' => array( 'html' => true, 'size' => 'small', 'placeholder' => __('Optional reason for the referral'), ), ) ), ); $this->setFields($fields); return $this->fields; } function getField($name) { if (($fields = $this->getFields()) && isset($fields[$name])) return $fields[$name]; } function isValid($include=false) { if (!parent::isValid($include) || !($f=$this->getField('target'))) return false; // Do additional assignment validation $referee = $this->getReferee(); switch (true) { case $referee instanceof Staff: // Make sure the agent is available if (!$referee->isAvailable()) $f->addError(__('Agent is unavailable for assignment')); break; case $referee instanceof Team: // Make sure the team is active and has members if (!$referee->isActive()) $f->addError(__('Team is disabled')); elseif (!$referee->getNumMembers()) $f->addError(__('Team does not have members')); break; case $referee instanceof Dept: break; default: $f->addError(__('Unknown selection')); } return !$this->errors(); } function render($options=array()) { 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 setChoices($field, $choices, $prompt='') { if (!($f= $this->getField($field))) return; $f->set('choices', $choices); return $f; } function getReferee() { $target = $this->getField('target')->getClean(); if (!$target || !($f=$this->getField($target))) return null; $id = $f->getClean(); switch($target) { case 'agent': return Staff::lookup($id); case 'team': return Team::lookup($id); case 'dept': return Dept::lookup($id); } } function getComments() { return $this->getField('comments')->getClean(); } } 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 is required'), ) ), 'refer' => new BooleanField(array( 'id'=>2, 'label'=>'', 'required'=>false, 'default'=>false, 'configuration'=>array( 'desc' => __('Maintain referral access to current department')) )), 'comments' => new TextareaField(array( 'id' => 3, 'label'=> '', 'required'=>false, 'default'=>'', 'configuration' => array( 'html' => true, 'size' => 'small', 'placeholder' => __('Optional reason for the transfer'), ), ) ), ); $this->setFields($fields); return $this->fields; } function isValid($include=false) { if (!parent::isValid($include)) return false; // Do additional validations if (!($dept = $this->getDept())) $this->getField('dept')->addError( __('Unknown department')); return !$this->errors(); } function render($options=array()) { 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($this), $options['template'])); } $form = $this; include $inc; } function refer() { return $this->getField('refer')->getClean(); } function getDept() { if (!isset($this->_dept)) { if (($id = $this->getField('dept')->getClean())) $this->_dept = Dept::lookup($id); } return $this->_dept; } function hideDisabled() { global $thisstaff; if ($f = $this->getField('dept')) { $f->configure('staff', $thisstaff); $f->configure('hideDisabled', true); } } } /** * 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 {} ?>
Save