Newer
Older
function display($value) {
$config = $this->getConfiguration();
if ($config['html'])
return Format::safe_html($value);
else
return nl2br(Format::htmlchars($value));
function searchable($value) {
$value = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $value);
$value = Format::htmldecode(Format::striptags($value));
return Format::searchable($value);
}
function export($value) {
return (!$value) ? $value : Format::html2text($value);
}
function parse($value) {
$config = $this->getConfiguration();
if ($config['html'])
return Format::sanitize($value);
else
return $value;
}
}
class PhoneField extends FormField {
static $widget = 'PhoneNumberWidget';
function getConfigurationOptions() {
return array(
'ext' => new BooleanField(array(
'label'=>__('Extension'), 'default'=>true,
'desc'=>__('Add a separate field for the extension'),
),
)),
'digits' => new TextboxField(array(
'label'=>__('Minimum length'), 'default'=>7,
'hint'=>__('Fewest digits allowed in a valid phone number'),
'configuration'=>array('validator'=>'number', 'size'=>5),
)),
'format' => new ChoiceField(array(
'label'=>__('Display format'), 'default'=>'us',
'choices'=>array(''=>'-- '.__('Unformatted').' --',
'us'=>__('United States')),
function hasSpecialSearch() {
return false;
}
function validateEntry($value) {
parent::validateEntry($value);
$config = $this->getConfiguration();
# Run validator against $this->value for email type
list($phone, $ext) = explode("X", $value, 2);
if ($phone && (
!is_numeric($phone) ||
strlen($phone) < $config['digits']))
$this->_errors[] = __("Enter a valid phone number");
$this->_errors[] = __("Enter a valid phone extension");
$this->_errors[] = __("Enter a phone number for the extension");
function parse($value) {
// NOTE: Value may have a legitimate 'X' to separate the number and
// extension parts. Don't remove the 'X'
$val = preg_replace('/[^\dX]/', '', $value);
// Pass completely-incorrect string for validation error
return $val ?: $value;
$config = $this->getConfiguration();
list($phone, $ext) = explode("X", $value, 2);
switch ($config['format']) {
case 'us':
$phone = Format::phone($phone);
break;
}
if ($ext)
$phone.=" x$ext";
return $phone;
}
}
class BooleanField extends FormField {
static $widget = 'CheckboxWidget';
function getConfigurationOptions() {
return array(
'desc' => new TextareaField(array(
'id'=>1, 'label'=>__('Description'), 'required'=>false, 'default'=>'',
'hint'=>__('Text shown inline with the widget'),
'configuration'=>array('rows'=>2)))
);
}
function to_database($value) {
return ($value) ? '1' : '0';
}
function parse($value) {
return $this->to_php($value);
}
return ($value) ? __('Yes') : __('No');
function getSearchMethods() {
return array(
'set' => __('checked'),
'set.not' => __('unchecked'),
);
}
function getSearchMethodWidgets() {
return array(
'set' => null,
'set.not' => null,
);
}
}
class ChoiceField extends FormField {
static $widget = 'ChoicesWidget';
function getConfigurationOptions() {
return array(
'choices' => new TextareaField(array(
'id'=>1, 'label'=>__('Choices'), 'required'=>false, 'default'=>'',
'hint'=>__('List choices, one per line. To protect against spelling changes, specify key:value names to preserve entries if the list item names change'),
'configuration'=>array('html'=>false)
)),
'default' => new TextboxField(array(
'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
'hint'=>__('(Enter a key). Value selected from the list initially'),
'configuration'=>array('size'=>20, 'length'=>40),
)),
'prompt' => new TextboxField(array(
'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
'hint'=>__('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40,
'translatable'=>$this->getTranslateTag('prompt'),
),
'multiselect' => new BooleanField(array(
'id'=>1, 'label'=>'Multiselect', 'required'=>false, 'default'=>false,
'configuration'=>array(
'desc'=>'Allow multiple selections')
)),
return $this->to_php($value ?: null);
}
function to_database($value) {
if (!is_array($value)) {
$choices = $this->getChoices();
if (isset($choices[$value]))
$value = array($value => $choices[$value]);
}
if (is_array($value))
return $value;
}
function to_php($value) {
if (is_string($value))
$value = JsonDataParser::parse($value) ?: $value;
// CDATA table may be built with comma-separated key,value,key,value
if (is_string($value)) {
$values = array();
$choices = $this->getChoices();
foreach (explode(',', $value) as $V) {
if (isset($choices[$V]))
$values[$V] = $choices[$V];
if (array_filter($values))
$value = $values;
$config = $this->getConfiguration();
if (!$config['multiselect'] && is_array($value) && count($value) < 2) {
reset($value);
if (!is_array($value))
$value = $this->getChoice($value);
if (is_array($value))
return implode(', ', $value);
return (string) $value;
$selection = array();
if ($value && is_array($value)) {
} elseif (isset($choices[$value]))
$selection[] = $choices[$value];
elseif ($this->get('default'))
$selection[] = $choices[$this->get('default')];
return $selection;
function getChoices($verbose=false) {
if ($this->_choices === null || $verbose) {
// Allow choices to be set in this->ht (for configurationOptions)
$this->_choices = $this->get('choices');
if (!$this->_choices) {
$this->_choices = array();
$config = $this->getConfiguration();
$choices = explode("\n", $config['choices']);
foreach ($choices as $choice) {
// Allow choices to be key: value
list($key, $val) = explode(':', $choice);
if ($val == null)
$val = $key;
$this->_choices[trim($key)] = trim($val);
}
// Add old selections if nolonger available
// This is necessary so choices made previously can be
// retained
$values = ($a=$this->getAnswer()) ? $a->getValue() : array();
if ($values && is_array($values)) {
foreach ($values as $k => $v) {
if (!isset($this->_choices[$k])) {
if ($verbose) $v .= ' (retired)';
$this->_choices[$k] = $v;
}
}
}
}
}
return $this->_choices;
function lookupChoice($value) {
return null;
}
function getSearchMethods() {
return array(
'set' => __('has a value'),
'notset' => __('does not have a value'),
'includes' => __('includes'),
'!includes' => __('does not include'),
);
}
function getSearchMethodWidgets() {
return array(
'set' => null,
'includes' => array('ChoiceField', array(
'choices' => $this->getChoices(),
'configuration' => array('multiselect' => true),
)),
'!includes' => array('ChoiceField', array(
'choices' => $this->getChoices(),
'configuration' => array('multiselect' => true),
)),
function getSearchQ($method, $value, $name=false) {
$name = $name ?: $this->get('name');
switch ($method) {
case '!includes':
return Q::not(array("{$name}__in" => array_keys($value)));
case 'includes':
return new Q(array("{$name}__in" => array_keys($value)));
default:
return parent::getSearchQ($method, $value, $name);
}
}
}
class DatetimeField extends FormField {
static $widget = 'DatetimePickerWidget';
function to_database($value) {
// Store time in gmt time, unix epoch format
return (string) $value;
}
function to_php($value) {
if (!$value)
return $value;
else
return (int) $value;
}
function asVar($value, $id=false) {
if (!$value) return null;
return new FormattedDate((int) $value, 'UTC', false, false);
}
function asVarType() {
return 'FormattedDate';
}
function toString($value) {
global $cfg;
$config = $this->getConfiguration();
// If GMT is set, convert to local time zone. Otherwise, leave
// unchanged (default TZ is UTC)
if ($config['time'])
return Format::datetime($value, false, !$config['gmt'] ? 'UTC' : false);
return Format::date($value, false, false, !$config['gmt'] ? 'UTC' : false);
function export($value) {
$config = $this->getConfiguration();
if (!$value)
return '';
else
return Format::date($value, false, 'y-MM-dd HH:mm:ss', !$config['gmt'] ? 'UTC' : false);
function getConfigurationOptions() {
return array(
'time' => new BooleanField(array(
'id'=>1, 'label'=>__('Time'), 'required'=>false, 'default'=>false,
'desc'=>__('Show time selection with date picker')))),
'id'=>2, 'label'=>__('Timezone Aware'), 'required'=>false,
'desc'=>__("Show date/time relative to user's timezone")))),
'id'=>3, 'label'=>__('Earliest'), 'required'=>false,
'hint'=>__('Earliest date selectable'))),
'id'=>4, 'label'=>__('Latest'), 'required'=>false,
'default'=>null, 'hint'=>__('Latest date selectable'))),
'id'=>5, 'label'=>__('Allow Future Dates'), 'required'=>false,
'desc'=>__('Allow entries into the future' /* Used in the date field */)),
)),
);
}
function validateEntry($value) {
$config = $this->getConfiguration();
parent::validateEntry($value);
if (!$value) return;
if ($config['min'] and $value < $config['min'])
$this->_errors[] = __('Selected date is earlier than permitted');
elseif ($config['max'] and $value > $config['max'])
$this->_errors[] = __('Selected date is later than permitted');
// strtotime returns -1 on error for PHP < 5.1.0 and false thereafter
elseif ($value === -1 or $value === false)
$this->_errors[] = __('Enter a valid date');
function getSearchMethods() {
return array(
'set' => __('has a value'),
'notset' => __('does not have a value'),
'equal' => __('on'),
'notequal' => __('not on'),
'before' => __('before'),
'after' => __('after'),
'between' => __('between'),
'ndaysago' => __('in the last n days'),
'ndays' => __('in the next n days'),
);
}
function getSearchMethodWidgets() {
$config_notime = $config = $this->getConfiguration();
$config_notime['time'] = false;
return array(
'set' => null,
'notset' => null,
'equal' => array('DatetimeField', array(
'configuration' => $config_notime,
)),
'notequal' => array('DatetimeField', array(
'configuration' => $config_notime,
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
)),
'before' => array('DatetimeField', array(
'configuration' => $config,
)),
'after' => array('DatetimeField', array(
'configuration' => $config,
)),
'between' => array('InlineformField', array(
'form' => array(
'left' => new DatetimeField(),
'text' => new FreeTextField(array(
'configuration' => array('content' => 'and'))
),
'right' => new DatetimeField(),
),
)),
'ndaysago' => array('InlineformField', array(
'form' => array(
'until' => new TextboxField(array(
'configuration' => array('validator'=>'number', 'size'=>4))
),
'text' => new FreeTextField(array(
'configuration' => array('content' => 'days'))
),
),
)),
'ndays' => array('InlineformField', array(
'form' => array(
'until' => new TextboxField(array(
'configuration' => array('validator'=>'number', 'size'=>4))
),
'text' => new FreeTextField(array(
'configuration' => array('content' => 'days'))
),
),
)),
);
}
function getSearchQ($method, $value, $name=false) {
$name = $name ?: $this->get('name');
switch ($method) {
case 'after':
return new Q(array("{$name}__gte" => $value));
case 'before':
return new Q(array("{$name}__lt" => $value));
case 'between':
return new Q(array(
"{$name}__gte" => $value['left'],
"{$name}__lte" => $value['right'],
));
case 'ndaysago':
return new Q(array(
"{$name}__lt" => SqlFunction::NOW(),
"{$name}__gte" => SqlExpression::minus(SqlFunction::NOW(), SqlInterval::DAY($value['until'])),
));
case 'ndays':
return new Q(array(
"{$name}__gt" => SqlFunction::NOW(),
"{$name}__lte" => SqlExpression::plus(SqlFunction::NOW(), SqlInterval::DAY($value['until'])),
));
default:
return parent::getSearchQ($method, $value, $name);
}
}
/**
* This is kind-of a special field that doesn't have any data. It's used as
* a field to provide a horizontal section break in the display of a form
*/
class SectionBreakField extends FormField {
static $widget = 'SectionBreakWidget';
function hasData() {
return false;
}
function isBlockLevel() {
return true;
}
}
class ThreadEntryField extends FormField {
static $widget = 'ThreadEntryWidget';
function isChangeable() {
return false;
}
function isBlockLevel() {
return true;
}
function isPresentationOnly() {
return true;
}
function hasSpecialSearch() {
return false;
}
function getConfigurationOptions() {
global $cfg;
$attachments = new FileUploadField();
$fileupload_config = $attachments->getConfigurationOptions();
if ($cfg->getAllowedFileTypes())
$fileupload_config['extensions']->set('default', $cfg->getAllowedFileTypes());
foreach ($fileupload_config as $C) {
$C->set('visibility', new VisibilityConstraint(new Q(array(
'attachments__eq'=>true,
)), VisibilityConstraint::HIDDEN));
}
return array(
'attachments' => new BooleanField(array(
'label'=>__('Enable Attachments'),
'default'=>$cfg->allowAttachments(),
'configuration'=>array(
'desc'=>__('Enables attachments, regardless of channel'),
'validators' => function($self, $value) {
if (!ini_get('file_uploads'))
$self->addError(__('The "file_uploads" directive is disabled in php.ini'));
}
+ $fileupload_config;
function isAttachmentsEnabled() {
$config = $this->getConfiguration();
return $config['attachments'];
}
}
class PriorityField extends ChoiceField {
function getWidget($widgetClass=false) {
$widget = parent::getWidget($widgetClass);
if ($widget->value instanceof Priority)
$widget->value = $widget->value->getId();
return $widget;
}
function hasIdValue() {
return true;
}
function getChoices($verbose=false) {
$sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE
.' ORDER BY priority_urgency DESC';
$choices = array('' => '— '.__('Default').' —');
if (!($res = db_query($sql)))
return $choices;
while ($row = db_fetch_row($res))
$choices[$row[0]] = $row[1];
return $choices;
}
function parse($id) {
return $this->to_php(null, $id);
}
function to_php($value, $id=false) {
if (is_array($id)) {
reset($id);
$id = key($id);
}
elseif ($id === false)
$id = $value;
if ($id)
return Priority::lookup($id);
}
function to_database($prio) {
return ($prio instanceof Priority)
? array($prio->getDesc(), $prio->getId())
: $prio;
}
function toString($value) {
return ($value instanceof Priority) ? $value->getDesc() : $value;
}
function searchable($value) {
// Priority isn't searchable this way
return null;
}
function getConfigurationOptions() {
$choices = $this->getChoices();
$choices[''] = __('System Default');
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
'hint'=>__('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
'default' => new ChoiceField(array(
'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
'choices' => $choices,
'hint'=>__('Default selection for this field'),
'configuration'=>array('size'=>20, 'length'=>40),
)),
function getConfiguration() {
global $cfg;
$config = parent::getConfiguration();
if (!isset($config['default']))
$config['default'] = $cfg->getDefaultPriorityId();
return $config;
}
FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
'priority' => array(__('Priority Level'), PriorityField),
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
class DepartmentField extends ChoiceField {
function getWidget() {
$widget = parent::getWidget();
if ($widget->value instanceof Dept)
$widget->value = $widget->value->getId();
return $widget;
}
function hasIdValue() {
return true;
}
function getChoices() {
global $cfg;
$choices = array();
if (($depts = Dept::getDepartments()))
foreach ($depts as $id => $name)
$choices[$id] = $name;
return $choices;
}
function parse($id) {
return $this->to_php(null, $id);
}
function to_php($value, $id=false) {
if (is_array($id)) {
reset($id);
$id = key($id);
}
return $id;
}
function to_database($dept) {
return ($dept instanceof Dept)
? array($dept->getName(), $dept->getId())
: $dept;
}
function toString($value) {
return (string) $value;
}
function searchable($value) {
return null;
}
function getConfigurationOptions() {
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
'hint'=>__('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
);
}
}
FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
return array(
'department' => array(__('Department'), DepartmentField),
);
});
class AssigneeField extends ChoiceField {
function getWidget() {
$widget = parent::getWidget();
if (is_object($widget->value))
$widget->value = $widget->value->getId();
return $widget;
}
function hasIdValue() {
return true;
}
function getChoices() {
global $cfg;
$choices = array(__('Agents') => new ArrayObject(), __('Teams') => new ArrayObject());
$A = current($choices);
if (($agents = Staff::getAvailableStaffMembers()))
foreach ($agents as $id => $name)
$A['s'.$id] = $name;
next($choices);
$T = current($choices);
if (($teams = Team::getTeams()))
foreach ($teams as $id => $name)
$T['t'.$id] = $name;
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
return $choices;
}
function parse($id) {
return $this->to_php(null, $id);
}
function to_php($value, $id=false) {
if (is_array($id)) {
reset($id);
$id = key($id);
}
return $id;
}
function to_database($value) {
return (is_object($value))
? array($value->getName(), $value->getId())
: $value;
}
function toString($value) {
return (string) $value;
}
function searchable($value) {
return null;
}
function getConfigurationOptions() {
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
'hint'=>__('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
);
}
}
FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
return array(
'assignee' => array(__('Assignee'), AssigneeField),
);
});
class TicketStateField extends ChoiceField {
'name' => /* @trans, @context "ticket state name" */ 'Open',
'verb' => /* @trans, @context "ticket state action" */ 'Open'
'name' => /* @trans, @context "ticket state name" */ 'Closed',
'verb' => /* @trans, @context "ticket state action" */ 'Close'
// Private states
static $_privatestates = array(
'name' => /* @trans, @context "ticket state name" */ 'Archived',
'verb' => /* @trans, @context "ticket state action" */ 'Archive'
'name' => /* @trans, @context "ticket state name" */ 'Deleted',
'verb' => /* @trans, @context "ticket state action" */ 'Delete'
);
function hasIdValue() {
return true;
}
function isChangeable() {
return false;
}
function getChoices($verbose=false) {
static $_choices;
if (!isset($_choices)) {
// Translate and cache the choices
foreach (static::$_states as $k => $v)
$_choices[$k] = _P('ticket state name', $v['name']);
$this->ht['default'] = '';
}
return $_choices;
}
function getChoice($state) {
if ($state && is_array($state))
$state = key($state);
if (isset(static::$_states[$state]))
return _P('ticket state name', static::$_states[$state]['name']);
if (isset(static::$_privatestates[$state]))
return _P('ticket state name', static::$_privatestates[$state]['name']);
}
function getConfigurationOptions() {
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=> __('Prompt'), 'required'=>false, 'default'=>'',
'hint'=> __('Leading text shown before a value is selected'),
'configuration'=>array('size'=>40, 'length'=>40),
)),
);
}
static function getVerb($state) {
if (isset(static::$_states[$state]))
return _P('ticket state action', static::$_states[$state]['verb']);
if (isset(static::$_privatestates[$state]))
return _P('ticket state action', static::$_privatestates[$state]['verb']);
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
}
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) {
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
$this->ht['default'] = '';
if (!$this->_choices) {
foreach (static::$_flags as $k => $v)
$this->_choices[$k] = $v['name'];
}
return $this->_choices;
}
function getConfigurationOptions() {
return array(
'prompt' => new TextboxField(array(
'id'=>2, 'label'=>'Prompt', 'required'=>false, 'default'=>'',
'hint'=>'Leading text shown before a value is selected',
'configuration'=>array('size'=>40, 'length'=>40),
)),
);
}
}
FormField::addFieldTypes('Dynamic Fields', function() {
return array(
'flags' => array('Ticket Flags', TicketFlagField, false),
);
});
class FileUploadField extends FormField {
static $widget = 'FileUploadWidget';
protected $attachments;
static function getFileTypes() {
static $filetypes;
if (!isset($filetypes))
$filetypes = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml');
return $filetypes;
}
function getConfigurationOptions() {
// Compute size selections
$sizes = array('262144' => '— '.__('Small').' —');
$next = 512 << 10;
$max = strtoupper(ini_get('upload_max_filesize'));
$limit = (int) $max;
if (!$limit) $limit = 2 << 20; # 2M default value
elseif (strpos($max, 'K')) $limit <<= 10;
elseif (strpos($max, 'M')) $limit <<= 20;
elseif (strpos($max, 'G')) $limit <<= 30;
while ($next <= $limit) {
// Select the closest, larger value (in case the
// current value is between two)
$sizes[$next] = Format::file_size($next);
$next *= 2;
}
// Add extra option if top-limit in php.ini doesn't fall
// at a power of two
if ($next < $limit * 2)
$sizes[$limit] = Format::file_size($limit);
foreach (self::getFileTypes() as $type=>$info) {
$types[$type] = $info['description'];
}
return array(
'size' => new ChoiceField(array(
'label'=>__('Maximum File Size'),
'hint'=>__('Choose maximum size of a single file uploaded to this field'),
'default'=>$cfg->getMaxFileSize(),
'mimetypes' => new ChoiceField(array(
'label'=>__('Restrict by File Type'),
'hint'=>__('Optionally, choose acceptable file types.'),
'required'=>false,
'choices'=>$types,
'configuration'=>array('multiselect'=>true,'prompt'=>__('No restrictions'))
'extensions' => new TextareaField(array(
'label'=>__('Additional File Type Filters'),
'hint'=>__('Optionally, enter comma-separated list of additional file types, by extension. (e.g .doc, .pdf).'),
'configuration'=>array('html'=>false, 'rows'=>2),
)),
'max' => new TextboxField(array(
'label'=>__('Maximum Files'),
'hint'=>__('Users cannot upload more than this many files.'),
'default'=>false,
'required'=>false,
'validator'=>'number',
'configuration'=>array('size'=>8, 'length'=>4, 'placeholder'=>__('No limit')),
function hasSpecialSearch() {
return false;
}
/**
* Called from the ajax handler for async uploads via web clients.
*/
function ajaxUpload($bypass=false) {
$config = $this->getConfiguration();
$files = AttachmentFile::format($_FILES['upload'],
// For numeric fields assume configuration exists
!is_numeric($this->get('id')));
if (count($files) != 1)
Http::response(400, 'Send one file at a time');
$file = array_shift($files);
$file['name'] = urldecode($file['name']);