Skip to content
Snippets Groups Projects
class.forms.php 90.5 KiB
Newer Older
  • Learn to ignore specific revisions
  •             'format' => new ChoiceField(array(
    
                    'label'=>__('Display format'), 'default'=>'us',
                    'choices'=>array(''=>'-- '.__('Unformatted').' --',
                        'us'=>__('United States')),
    
        function hasSpecialSearch() {
            return false;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function validateEntry($value) {
            parent::validateEntry($value);
    
            $config = $this->getConfiguration();
    
    Jared Hancock's avatar
    Jared Hancock committed
            # 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']) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                if (!is_numeric($ext))
    
                    $this->_errors[] = __("Enter a valid phone extension");
    
    Jared Hancock's avatar
    Jared Hancock committed
                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;
    
    Jared Hancock's avatar
    Jared Hancock committed
        function toString($value) {
    
            $config = $this->getConfiguration();
    
    Jared Hancock's avatar
    Jared Hancock committed
            list($phone, $ext) = explode("X", $value, 2);
    
            switch ($config['format']) {
            case 'us':
                $phone = Format::phone($phone);
                break;
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
            if ($ext)
                $phone.=" x$ext";
            return $phone;
        }
    }
    
    class BooleanField extends FormField {
    
        static $widget = 'CheckboxWidget';
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function getConfigurationOptions() {
            return array(
                'desc' => new TextareaField(array(
    
                    'id'=>1, 'label'=>__('Description'), 'required'=>false, 'default'=>'',
                    'hint'=>__('Text shown inline with the widget'),
    
    Jared Hancock's avatar
    Jared Hancock committed
                    'configuration'=>array('rows'=>2)))
            );
        }
    
        function to_database($value) {
            return ($value) ? '1' : '0';
        }
    
    
        function parse($value) {
            return $this->to_php($value);
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
        function to_php($value) {
    
            return $value ? true : false;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function toString($value) {
    
            return ($value) ? __('Yes') : __('No');
    
    
        function getSearchMethods() {
            return array(
                'set' =>        __('checked'),
                'set.not' =>    __('unchecked'),
            );
        }
    
        function getSearchMethodWidgets() {
            return array(
                'set' => null,
                'set.not' => null,
            );
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class ChoiceField extends FormField {
    
        static $widget = 'ChoicesWidget';
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        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')
                )),
    
        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))
    
    Peter Rotich's avatar
    Peter Rotich committed
                $value = JsonDataEncoder::encode($value);
    
    
            return $value;
        }
    
        function to_php($value) {
    
            if (is_string($value))
                $array = JsonDataParser::parse($value) ?: $value;
            else
                $array = $value;
            $config = $this->getConfiguration();
    
            if (!$config['multiselect']) {
                if (is_array($array) && count($array) < 2) {
                    reset($array);
                    return key($array);
                }
                if (is_string($array) && strpos($array, ',') !== false) {
                    list($array,) = explode(',', $array, 2);
                }
    
        function toString($value) {
    
            $selection = $this->getChoice($value);
    
            return is_array($selection)
                ? (implode(', ', array_filter($selection)) ?: $value)
    
        }
    
        function getChoice($value) {
    
            $choices = $this->getChoices();
    
            $selection = array();
            if ($value && is_array($value)) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                $selection = $value;
    
            } elseif (isset($choices[$value]))
                $selection[] = $choices[$value];
            elseif ($this->get('default'))
                $selection[] = $choices[$this->get('default')];
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        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);
                    }
    
    Peter Rotich's avatar
    Peter Rotich committed
                    // 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 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);
            }
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class DatetimeField extends FormField {
    
        static $widget = 'DatetimePickerWidget';
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        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 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);
    
    Jared Hancock's avatar
    Jared Hancock committed
            else
    
                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);
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getConfigurationOptions() {
            return array(
                'time' => new BooleanField(array(
    
                    'id'=>1, 'label'=>__('Time'), 'required'=>false, 'default'=>false,
    
    Jared Hancock's avatar
    Jared Hancock committed
                    'configuration'=>array(
    
                        'desc'=>__('Show time selection with date picker')))),
    
    Jared Hancock's avatar
    Jared Hancock committed
                'gmt' => new BooleanField(array(
    
                    'id'=>2, 'label'=>__('Timezone Aware'), 'required'=>false,
    
    Jared Hancock's avatar
    Jared Hancock committed
                    'configuration'=>array(
    
                        'desc'=>__("Show date/time relative to user's timezone")))),
    
    Jared Hancock's avatar
    Jared Hancock committed
                'min' => new DatetimeField(array(
    
                    'id'=>3, 'label'=>__('Earliest'), 'required'=>false,
                    'hint'=>__('Earliest date selectable'))),
    
    Jared Hancock's avatar
    Jared Hancock committed
                'max' => new DatetimeField(array(
    
                    'id'=>4, 'label'=>__('Latest'), 'required'=>false,
    
                    'default'=>null, 'hint'=>__('Latest date selectable'))),
    
    Jared Hancock's avatar
    Jared Hancock committed
                'future' => new BooleanField(array(
    
                    'id'=>5, 'label'=>__('Allow Future Dates'), 'required'=>false,
    
    Jared Hancock's avatar
    Jared Hancock committed
                    'default'=>true, 'configuration'=>array(
    
                        'desc'=>__('Allow entries into the future' /* Used in the date field */)),
                )),
    
    Jared Hancock's avatar
    Jared Hancock committed
            );
        }
    
        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');
    
    Jared Hancock's avatar
    Jared Hancock committed
            elseif ($config['max'] and $value > $config['max'])
    
                $this->_errors[] = __('Selected date is later than permitted');
    
    Jared Hancock's avatar
    Jared Hancock committed
            // 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,
    
                )),
                '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();
    
    Peter Rotich's avatar
    Peter Rotich committed
            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(),
    
                        'desc'=>__('Enables attachments on tickets, 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() {
            $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';
        }
    
        function getChoices() {
    
            global $cfg;
            $this->ht['default'] = $cfg->getDefaultPriorityId();
    
    
            $sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE
                  .' ORDER BY priority_urgency DESC';
    
            $choices = array();
    
            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);
            }
    
            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() {
    
        return array(
    
            'priority' => array(__('Priority Level'), PriorityField),
    
    
    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() {
    
            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']);
    
            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() {
            $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);
    
    
            $types = array();
    
            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(),
    
                    '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),
                )),
                'max' => new TextboxField(array(
    
                    'label'=>__('Maximum Files'),
                    'hint'=>__('Users cannot upload more than this many files.'),
    
                    'default'=>false,
                    'required'=>false,
                    'validator'=>'number',
    
                    'configuration'=>array('size'=>8, 'length'=>4, 'placeholder'=>__('No limit')),
    
        function hasSpecialSearch() {
            return false;
        }
    
    
        /**
         * Called from the ajax handler for async uploads via web clients.
         */
        function ajaxUpload($bypass=false) {
    
            $config = $this->getConfiguration();
    
            $files = AttachmentFile::format($_FILES['upload'],
                // For numeric fields assume configuration exists
    
                !is_numeric($this->get('id')));
    
            if (count($files) != 1)
                Http::response(400, 'Send one file at a time');
            $file = array_shift($files);
            $file['name'] = urldecode($file['name']);
    
    
            if (!$bypass && !$this->isValidFileType($file['name'], $file['type']))
    
                Http::response(415, 'File type is not allowed');
    
            $config = $this->getConfiguration();
            if (!$bypass && $file['size'] > $config['size'])
                Http::response(413, 'File is too large');
    
    
            if (!($id = AttachmentFile::upload($file)))
    
                Http::response(500, 'Unable to store file: '. $file['error']);
    
        /**
         * Called from FileUploadWidget::getValue() when manual upload is used
         * for browsers which do not support the HTML5 way of uploading async.
         */
        function uploadFile($file) {
    
            if (!$this->isValidFileType($file['name'], $file['type']))
    
                throw new FileUploadError(__('File type is not allowed'));
    
            $config = $this->getConfiguration();
            if ($file['size'] > $config['size'])
                throw new FileUploadError(__('File size is too large'));
    
            return AttachmentFile::upload($file);
        }
    
        /**
         * Called from API and email routines and such to handle attachments
         * sent other than via web upload
         */
        function uploadAttachment(&$file) {
    
            if (!$this->isValidFileType($file['name'], $file['type']))
    
                throw new FileUploadError(__('File type is not allowed'));
    
            if (is_callable($file['data']))
                $file['data'] = $file['data']();
            if (!isset($file['size'])) {
                // bootstrap.php include a compat version of mb_strlen
                if (extension_loaded('mbstring'))
                    $file['size'] = mb_strlen($file['data'], '8bit');
                else
                    $file['size'] = strlen($file['data']);
            }
    
            $config = $this->getConfiguration();
            if ($file['size'] > $config['size'])
                throw new FileUploadError(__('File size is too large'));
    
            if (!$id = AttachmentFile::save($file))
                throw new FileUploadError(__('Unable to save file'));
    
            return $id;
        }
    
        function isValidFileType($name, $type=false) {
            $config = $this->getConfiguration();
    
    
            // Check MIME type - file ext. shouldn't be solely trusted.
            if ($type && $config['__mimetypes']
                    && in_array($type, $config['__mimetypes']))
    
            // Return true if all file types are allowed (.*)
    
            if (!$config['__extensions'] || in_array('.*', $config['__extensions']))
    
            $allowed = $config['__extensions'];
    
            $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    
            return ($ext && is_array($allowed) && in_array(".$ext", $allowed));
        }
    
    
        function getFiles() {
            if (!isset($this->attachments) && ($a = $this->getAnswer())
                && ($e = $a->getEntry()) && ($e->get('id'))
            ) {
                $this->attachments = 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();
        }
    
    
        function getConfiguration() {
            $config = parent::getConfiguration();
    
            $_types = self::getFileTypes();
    
            $mimetypes = array();
            $extensions = array();
            if (isset($config['mimetypes']) && is_array($config['mimetypes'])) {
                foreach ($config['mimetypes'] as $type=>$desc) {
                    foreach ($_types[$type]['types'] as $mime=>$exts) {
                        $mimetypes[$mime] = true;
    
                        if (is_array($exts))
                            foreach ($exts as $ext)
                                $extensions['.'.$ext] = true;
    
                    }
                }
            }
            if (strpos($config['extensions'], '.*') !== false)
                $config['extensions'] = '';
    
    
            if (is_string($config['extensions'])) {
                foreach (preg_split('/\s+/', str_replace(',',' ', $config['extensions'])) as $ext) {
                    if (!$ext) {
                        continue;
                    }
                    elseif (strpos($ext, '/')) {
    
                        $mimetypes[$ext] = true;
    
                    }
                    else {
                        if ($ext[0] != '.')
                            $ext = '.' . $ext;
                        // Add this to the MIME types list so it can be exported to
                        // the @accept attribute
                        if (!isset($extensions[$ext]))
                            $mimetypes[$ext] = true;
    
                        $extensions[$ext] = true;
                    }
    
                $config['__extensions'] = array_keys($extensions);
            }
            elseif (is_array($config['extensions'])) {
                $config['__extensions'] = $config['extensions'];
    
            }
    
            // 'mimetypes' is the array represented from the user interface,
            // '__mimetypes' is a complete list of supported MIME types.
            $config['__mimetypes'] = array_keys($mimetypes);
            return $config;
        }
    
    
        // When the field is saved to database, encode the ID listing as a json
        // array. Then, inspect the difference between the files actually
        // attached to this field
        function to_database($value) {
            $this->getFiles();
            if (isset($this->attachments)) {
                $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);
        }
    
    class InlineFormData extends ArrayObject {
        var $_form;
    
        function __construct($form, array $data=array()) {
            parent::__construct($data);
            $this->_form = $form;
        }
    
        function getVar($tag) {
            foreach ($this->_form->getFields() as $f) {
                if ($f->get('name') == $tag)
                    return $this[$f->get('id')];
            }
        }
    }
    
    
    class InlineFormField extends FormField {
        static $widget = 'InlineFormWidget';
    
        var $_iform = null;
    
        function validateEntry($value) {
            if (!$this->getInlineForm()->isValid()) {
    
                $this->_errors[] = __('Correct errors in the inline form');