Newer
Older
),
)),
)
+ $attachments->getConfigurationOptions();
function isAttachmentsEnabled() {
$config = $this->getConfiguration();
return $config['attachments'];
}
}
class PriorityField extends ChoiceField {
function getWidget() {
$widget = parent::getWidget();
if ($widget->value instanceof Priority)
$widget->value = $widget->value->getId();
return $widget;
}
function hasIdValue() {
return true;
}
function isChangeable() {
return $this->getForm()->get('type') != 'T' ||
$this->get('name') != 'priority';
}
global $cfg;
$this->ht['default'] = $cfg->getDefaultPriorityId();
$sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE
.' ORDER BY priority_urgency DESC';
if (!($res = db_query($sql)))
return $choices;
while ($row = db_fetch_row($res))
$choices[$row[0]] = $row[1];
return $choices;
}
function parse($id) {
return $this->to_php(null, $id);
}
function to_php($value, $id=false) {
return Priority::lookup($id);
}
function to_database($prio) {
return ($prio instanceof Priority)
? array($prio->getDesc(), $prio->getId())
: $prio;
}
function toString($value) {
return ($value instanceof Priority) ? $value->getDesc() : $value;
}
function searchable($value) {
// Priority isn't searchable this way
return null;
}
function getConfigurationOptions() {
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() {
'priority' => array(__('Priority Level'), PriorityField),
class TicketStateField extends ChoiceField {
'name' => /* @trans, @context "ticket state name" */ 'Open',
'verb' => /* @trans, @context "ticket state action" */ 'Open'
'name' => /* @trans, @context "ticket state name" */ 'Resolved',
'verb' => /* @trans, @context "ticket state action" */ 'Resolve'
'name' => /* @trans, @context "ticket state name" */ 'Closed',
'verb' => /* @trans, @context "ticket state action" */ 'Close'
// Private states
static $_privatestates = array(
'name' => /* @trans, @context "ticket state name" */ 'Archived',
'verb' => /* @trans, @context "ticket state action" */ 'Archive'
'name' => /* @trans, @context "ticket state name" */ 'Deleted',
'verb' => /* @trans, @context "ticket state action" */ 'Delete'
);
function hasIdValue() {
return true;
}
function isChangeable() {
return false;
}
function getChoices() {
static $_choices;
if (!isset($_choices)) {
// Translate and cache the choices
foreach (static::$_states as $k => $v)
$_choices[$k] = _P('ticket state name', $v['name']);
$this->ht['default'] = '';
}
return $_choices;
}
function getChoice($state) {
if ($state && is_array($state))
$state = key($state);
if (isset(static::$_states[$state]))
return _P('ticket state name', static::$_states[$state]['name']);
if (isset(static::$_privatestates[$state]))
return _P('ticket state name', static::$_privatestates[$state]['name']);
}
function getConfigurationOptions() {
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=> __('Prompt'), 'required'=>false, 'default'=>'',
'hint'=> __('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
);
}
static function getVerb($state) {
if (isset(static::$_states[$state]))
return _P('ticket state action', static::$_states[$state]['verb']);
if (isset(static::$_privatestates[$state]))
return _P('ticket state action', static::$_privatestates[$state]['verb']);
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
}
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() {
$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),
);
});
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
class FileUploadField extends FormField {
static $widget = 'FileUploadWidget';
protected $attachments;
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)
$diff = $next - $config['max_file_size'];
$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);
return array(
'size' => new ChoiceField(array(
'label'=>'Maximum File Size',
'hint'=>'Maximum size of a single file uploaded to this field',
'default'=>$cfg->getMaxFileSize(),
'choices'=>$sizes
)),
'extensions' => new TextareaField(array(
'label'=>'Allowed Extensions',
'hint'=>'Enter allowed file extensions separated by a comma.
e.g .doc, .pdf. To accept all files enter wildcard
<b><i>.*</i></b> — i.e dotStar (NOT Recommended).',
'default'=>$cfg->getAllowedFileTypes(),
'configuration'=>array('html'=>false, 'rows'=>2),
)),
'max' => new TextboxField(array(
'label'=>'Maximum Files',
'hint'=>'Users cannot upload more than this many files',
'default'=>false,
'required'=>false,
'validator'=>'number',
'configuration'=>array('size'=>4, 'length'=>4),
))
);
}
/**
* Called from the ajax handler for async uploads via web clients.
*/
function ajaxUpload($bypass=false) {
$config = $this->getConfiguration();
$files = AttachmentFile::format($_FILES['upload'],
// For numeric fields assume configuration exists
!is_numeric($this->get('id')));
if (count($files) != 1)
Http::response(400, 'Send one file at a time');
$file = array_shift($files);
$file['name'] = urldecode($file['name']);
if (!$bypass && !$this->isValidFileType($file['name']))
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 (!($id = AttachmentFile::upload($file)))
Http::response(500, 'Unable to store file: '. $file['error']);
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
/**
* 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']))
throw new FileUploadError(__('File type is not allowed'));
$config = $this->getConfiguration();
if ($file['size'] > $config['size'])
throw new FileUploadError(__('File size is too large'));
return AttachmentFile::upload($file);
}
/**
* Called from API and email routines and such to handle attachments
* sent other than via web upload
*/
function uploadAttachment(&$file) {
if (!$this->isValidFileType($file['name']))
throw new FileUploadError(__('File type is not allowed'));
if (is_callable($file['data']))
$file['data'] = $file['data']();
if (!isset($file['size'])) {
// bootstrap.php include a compat version of mb_strlen
if (extension_loaded('mbstring'))
$file['size'] = mb_strlen($file['data'], '8bit');
else
$file['size'] = strlen($file['data']);
}
$config = $this->getConfiguration();
if ($file['size'] > $config['size'])
throw new FileUploadError(__('File size is too large'));
if (!$id = AttachmentFile::save($file))
throw new FileUploadError(__('Unable to save file'));
return $id;
}
function isValidFileType($name, $type=false) {
$config = $this->getConfiguration();
// Return true if all file types are allowed (.*)
if (strpos($config['extensions'], '.*') || !$config['extensions'])
return true;
$allowed = array_map('trim', explode(',', strtolower($config['extensions'])));
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
//TODO: Check MIME type - file ext. shouldn't be solely trusted.
return ($ext && is_array($allowed) && in_array(".$ext", $allowed));
}
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
function getFiles() {
if (!isset($this->attachments) && ($a = $this->getAnswer())
&& ($e = $a->getEntry()) && ($e->get('id'))
) {
$this->attachments = new GenericAttachments(
// Combine the field and entry ids to make the key
sprintf('%u', crc32('E'.$this->get('id').$e->get('id'))),
'E');
}
return $this->attachments ? $this->attachments->getAll() : array();
}
// When the field is saved to database, encode the ID listing as a json
// array. Then, inspect the difference between the files actually
// attached to this field
function to_database($value) {
$this->getFiles();
if (isset($this->attachments)) {
$ids = array();
// Handle deletes
foreach ($this->attachments->getAll() as $f) {
if (!in_array($f['id'], $value))
$this->attachments->delete($f['id']);
else
$ids[] = $f['id'];
}
// Handle new files
foreach ($value as $id) {
if (!in_array($id, $ids))
$this->attachments->upload($id);
}
}
return JsonDataEncoder::encode($value);
}
function parse($value) {
// Values in the database should be integer file-ids
return array_map(function($e) { return (int) $e; },
$value ?: array());
}
function to_php($value) {
return JsonDataParser::decode($value);
}
function display($value) {
$links = array();
foreach ($this->getFiles() as $f) {
$hash = strtolower($f['key']
. md5($f['id'].session_id().strtolower($f['key'])));
$links[] = sprintf('<a class="no-pjax" href="file.php?h=%s">%s</a>',
$hash, Format::htmlchars($f['name']));
}
return implode('<br/>', $links);
}
function toString($value) {
$files = array();
foreach ($this->getFiles() as $f) {
$files[] = $f['name'];
}
return implode(', ', $files);
}
function __construct($field) {
$this->field = $field;
$this->name = $field->getFormName();
$this->value = $this->getValue();
if (!isset($this->value) && is_object($this->field->getAnswer()))
$this->value = $this->field->getAnswer()->getValue();
if (!isset($this->value) && isset($this->field->value))
$data = $this->field->getSource();
// Search for HTML form name first
if (isset($data[$this->name]))
return $data[$this->name];
elseif (isset($data[$this->field->get('name')]))
return $data[$this->field->get('name')];
return null;
}
}
class TextboxWidget extends Widget {
function render($mode=false) {
$config = $this->field->getConfiguration();
if (isset($config['size']))
$size = "size=\"{$config['size']}\"";
if (isset($config['length']) && $config['length'])
$maxlength = "maxlength=\"{$config['length']}\"";
if (isset($config['classes']))
$classes = 'class="'.$config['classes'].'"';
if (isset($config['autocomplete']))
$autocomplete = 'autocomplete="'.($config['autocomplete']?'on':'off').'"';
if (isset($config['disabled']))
$disabled = 'disabled="disabled"';
<input type="<?php echo static::$input_type; ?>"
id="<?php echo $this->name; ?>"
<?php echo implode(' ', array_filter(array(
$size, $maxlength, $classes, $autocomplete, $disabled)))
.' placeholder="'.$config['placeholder'].'"'; ?>
name="<?php echo $this->name; ?>"
value="<?php echo Format::htmlchars($this->value); ?>"/>
</span>
<?php
}
}
class PasswordWidget extends TextboxWidget {
static $input_type = 'password';
function parseValue() {
// Show empty box unless failed POST
if ($_SERVER['REQUEST_METHOD'] == 'POST'
&& $this->field->getForm()->isValid())
parent::parseValue();
else
$this->value = '';
}
}
function render($mode=false) {
$class = $cols = $rows = $maxlength = "";
if (isset($config['rows']))
$rows = "rows=\"{$config['rows']}\"";
if (isset($config['cols']))
$cols = "cols=\"{$config['cols']}\"";
if (isset($config['length']) && $config['length'])
$maxlength = "maxlength=\"{$config['length']}\"";
if (isset($config['html']) && $config['html'])
$class = 'class="richtext no-bar small"';
<span style="display:inline-block;width:100%">
<textarea <?php echo $rows." ".$cols." ".$maxlength." ".$class
.' placeholder="'.$config['placeholder'].'"'; ?>
name="<?php echo $this->name; ?>"><?php
echo Format::htmlchars($this->value);
?></textarea>
</span>
<?php
}
}
class PhoneNumberWidget extends Widget {
function render($mode=false) {
$config = $this->field->getConfiguration();
list($phone, $ext) = explode("X", $this->value);
?>
<input type="text" name="<?php echo $this->name; ?>" value="<?php
echo Format::htmlchars($phone); ?>"/><?php
// Allow display of extension field even if disabled if the phone
// number being edited has an extension
if ($ext || $config['ext']) { ?> <?php echo __('Ext'); ?>:
echo $this->name; ?>-ext" value="<?php echo Format::htmlchars($ext);
?>" size="5"/>
$data = $this->field->getSource();
$base = parent::getValue();
if ($base === null)
return $base;
$ext = $data["{$this->name}-ext"];
// NOTE: 'X' is significant. Don't change it
}
}
class ChoicesWidget extends Widget {
if (!($val = (string) $this->field))
$val = sprintf('<span class="faded">%s</span>', __('None'));
echo $val;
return;
}
$config = $this->field->getConfiguration();
// Determine the value for the default (the one listed if nothing is
// selected)
$prompt = $config['prompt'] ?: __('Select');
// We don't consider the 'default' when rendering in 'search' mode
if (!strcasecmp($mode, 'search')) {
$def_val = $prompt;
} else {
$def_key = $this->field->get('default');
if (!$def_key && $config['default'])
$def_key = $config['default'];
$have_def = isset($choices[$def_key]);
$def_val = $have_def ? $choices[$def_key] : $prompt;
$values = $this->value;
if (!is_array($values)) {
$values = array($values => $this->field->getChoice($values));
}
if ($values === null)
$values = $have_def ? array($def_key => $choices[$def_key]) : array();
?>
<select name="<?php echo $this->name; ?>[]"
id="<?php echo $this->name; ?>"
data-prompt="<?php echo $prompt; ?>"
<?php if ($config['multiselect'])
echo ' multiple="multiple" class="multiselect"'; ?>>
<?php if (!$have_def && !$config['multiselect']) { ?>
<option value="<?php echo $def_key; ?>">— <?php
echo $def_val; ?> —</option>
<?php }
if (!$have_def && $key == $def_key)
continue; ?>
<option value="<?php echo $key; ?>" <?php
if (isset($values[$key])) echo 'selected="selected"';
?>><?php echo $name; ?></option>
<?php } ?>
</select>
<?php
if ($config['multiselect']) {
?>
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.min.js"></script>
<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.css"/>
<script type="text/javascript">
$(function() {
$("#<?php echo $this->name; ?>")
.multiselect({'noneSelectedText':'<?php echo $prompt; ?>'});
});
</script>
<?php
}
function getValue() {
$value = parent::getValue();
if (!$value) return null;
// Assume multiselect
$values = array();
$choices = $this->field->getChoices();
if (is_array($value)) {
foreach($value as $k => $v) {
if (isset($choices[$v]))
$values[$v] = $choices[$v];
}
}
return $values;
}
}
class CheckboxWidget extends Widget {
function __construct($field) {
parent::__construct($field);
$this->name = '_field-checkboxes';
}
function render($mode=false) {
if (!isset($this->value))
$this->value = $this->field->get('default');
?>
<input 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']) { ?>
<em style="display:inline-block"><?php
echo Format::htmlchars($config['desc']); ?></em>
<?php }
}
function getValue() {
$data = $this->field->getSource();
if (count($data))
return @in_array($this->field->get('id'), $data[$this->name]);
return parent::getValue();
}
}
class DatetimePickerWidget extends Widget {
function render($mode=false) {
$config = $this->field->getConfiguration();
if ($this->value) {
$this->value = is_int($this->value) ? $this->value :
strtotime($this->value);
if ($config['gmt'])
$this->value += 3600 *
$_SESSION['TZ_OFFSET']+($_SESSION['TZ_DST']?date('I',$this->value):0);
list($hr, $min) = explode(':', date('H:i', $this->value));
$this->value = Format::date($cfg->getDateFormat(), $this->value);
}
?>
<input type="text" name="<?php echo $this->name; ?>"
value="<?php echo Format::htmlchars($this->value); ?>" size="12"
autocomplete="off" class="dp" />
<script type="text/javascript">
$(function() {
$('input[name="<?php echo $this->name; ?>"]').datepicker({
<?php
if ($config['min'])
echo "minDate: new Date({$config['min']}000),";
if ($config['max'])
echo "maxDate: new Date({$config['max']}000),";
elseif (!$config['future'])
echo "maxDate: new Date().getTime(),";
?>
numberOfMonths: 2,
showButtonPanel: true,
buttonImage: './images/cal.png',
dateFormat: $.translate_format('<?php echo $cfg->getDateFormat(); ?>')
});
});
</script>
<?php
if ($config['time'])
// TODO: Add time picker -- requires time picker or selection with
// Misc::timeDropdown
echo ' ' . Misc::timeDropdown($hr, $min, $this->name . ':time');
}
/**
* Function: getValue
* Combines the datepicker date value and the time dropdown selected
* time value into a single date and time string value.
*/
function getValue() {
$data = $this->field->getSource();
$config = $this->field->getConfiguration();
if ($datetime = parent::getValue()) {
$datetime = is_int($datetime) ? $datetime :
strtotime($datetime);
if ($datetime && isset($data[$this->name . ':time'])) {
list($hr, $min) = explode(':', $data[$this->name . ':time']);
$datetime += $hr * 3600 + $min * 60;
}
if ($datetime && $config['gmt'])
$datetime -= (int) (3600 * $_SESSION['TZ_OFFSET'] +
($_SESSION['TZ_DST'] ? date('I',$datetime) : 0));
}
class SectionBreakWidget extends Widget {
function render($mode=false) {
?><div class="form-header section-break"><h3><?php
echo Format::htmlchars($this->field->get('label'));
?></h3><em><?php echo Format::htmlchars($this->field->get('hint'));
?></em></div>
<?php
}
}
class ThreadEntryWidget extends Widget {
function render($client=null) {
global $cfg;
?><div style="margin-bottom:0.5em;margin-top:0.5em"><strong><?php
echo Format::htmlchars($this->field->get('label'));
?></strong>:</div>
<textarea style="width:100%;" name="<?php echo $this->field->get('name'); ?>"
placeholder="<?php echo Format::htmlchars($this->field->get('hint')); ?>"
<?php if (!$client) { ?>
data-draft-namespace="ticket.staff"
<?php } else { ?>
data-draft-namespace="ticket.client"
data-draft-object-id="<?php echo substr(session_id(), -12); ?>"
<?php } ?>
class="richtext draft draft-delete ifhtml"
cols="21" rows="8" style="width:80%;"><?php echo
Format::htmlchars($this->value); ?></textarea>
$config = $this->field->getConfiguration();
if (!$config['attachments'])
return;
$attachments = $this->getAttachments($config);
print $attachments->render($client);
foreach ($attachments->getMedia() as $type=>$urls) {
foreach ($urls as $url)
Form::emitMedia($url, $type);
function getAttachments($config=false) {
if (!$config)
$config = $this->field->getConfiguration();
return new FileUploadField(array(
'id'=>'attach',
'name'=>'attach:' . $this->field->get('id'),
'configuration'=>$config)
);
}
class FileUploadWidget extends Widget {
static $media = array(
'css' => array(
'/css/filedrop.css',
),
);
function render($how) {
$config = $this->field->getConfiguration();
$name = $this->field->getFormName();
$id = substr(md5(spl_object_hash($this)), 10);
$attachments = $this->field->getFiles();
$files = array();
foreach ($this->value ?: array() as $fid) {
$found = false;
foreach ($attachments as $f) {
if ($f['id'] == $fid) {
$files[] = $f;
$found = true;
break;
}
}
if (!$found && ($file = AttachmentFile::lookup($fid))) {
$files[] = array(
'id' => $file->getId(),
'name' => $file->getName(),
'type' => $file->getType(),
'size' => $file->getSize(),
);
}
}
?><div id="<?php echo $id;
?>" class="filedrop"><div class="files"></div>
<div class="dropzone"><i class="icon-upload"></i>
Drop files here or <a href="#" class="manual">choose
them</a>
<input type="file" class="multifile" multiple id="file-<?php echo $id; ?>" style="display:none;"/>
</div></div>
$(function(){$('#<?php echo $id; ?> .dropzone').filedropbox({
url: 'ajax.php/form/upload/<?php echo $this->field->get('id') ?>',
link: $('#<?php echo $id; ?>').find('a.manual'),
fallback_id: 'file-<?php echo $id; ?>',
allowedfileextensions: '<?php echo $config['extensions'];
?>'.split(/,\s*/),
maxfiles: <?php echo $config['max'] ?: 20; ?>,
maxfilesize: <?php echo ($config['size'] ?: 1048576) / 1048576; ?>,
name: '<?php echo $name; ?>[]',
files: <?php echo JsonDataEncoder::encode($files); ?>
});});
</script>
<?php
}
function getValue() {
$data = $this->field->getSource();
$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 {
$ids[] = $this->field->uploadFile($file);
}
catch (FileUploadError $ex) {}
}
return array_merge($ids, parent::getValue() ?: array());
}
// If no value was sent, assume an empty list
elseif ($data && is_array($data) && !isset($data[$this->name]))
return parent::getValue();
}
}
class FileUploadError extends Exception {}