1<?php
2/*********************************************************************
3    class.dynamic_forms.php
4
5    Forms models built on the VerySimpleModel paradigm. Allows for arbitrary
6    data to be associated with tickets. Eventually this model can be
7    extended to associate arbitrary data with registered clients and thread
8    entries.
9
10    Jared Hancock <jared@osticket.com>
11    Copyright (c)  2006-2013 osTicket
12    http://www.osticket.com
13
14    Released under the GNU General Public License WITHOUT ANY WARRANTY.
15    See LICENSE.TXT for details.
16
17    vim: expandtab sw=4 ts=4 sts=4:
18**********************************************************************/
19require_once(INCLUDE_DIR . 'class.orm.php');
20require_once(INCLUDE_DIR . 'class.forms.php');
21require_once(INCLUDE_DIR . 'class.list.php');
22require_once(INCLUDE_DIR . 'class.filter.php');
23require_once(INCLUDE_DIR . 'class.signal.php');
24
25/**
26 * Form template, used for designing the custom form and for entering custom
27 * data for a ticket
28 */
29class DynamicForm extends VerySimpleModel {
30
31    static $meta = array(
32        'table' => FORM_SEC_TABLE,
33        'ordering' => array('title'),
34        'pk' => array('id'),
35        'joins' => array(
36            'fields' => array(
37                'reverse' => 'DynamicFormField.form',
38            ),
39        ),
40    );
41
42    // Registered form types
43    static $types = array(
44        'T' => 'Ticket Information',
45        'U' => 'User Information',
46        'O' => 'Organization Information',
47    );
48
49    const FLAG_DELETABLE    = 0x0001;
50    const FLAG_DELETED      = 0x0002;
51
52    var $_form;
53    var $_fields;
54    var $_has_data = false;
55    var $_dfields;
56
57    function getInfo() {
58        $base = $this->ht;
59        unset($base['fields']);
60        return $base;
61    }
62
63    function getId() {
64        return $this->id;
65    }
66
67    /**
68     * Fetch a list of field implementations for the fields defined in this
69     * form. This method should *always* be preferred over
70     * ::getDynamicFields() to avoid caching confusion
71     */
72    function getFields() {
73        if (!$this->_fields) {
74            $this->_fields = new ListObject();
75            foreach ($this->getDynamicFields() as $f)
76                $this->_fields->append($f->getImpl($f));
77        }
78        return $this->_fields;
79    }
80
81    /**
82     * Fetch the dynamic fields associated with this dynamic form. Do not
83     * use this list for data processing or validation. Use ::getFields()
84     * for that.
85     */
86    function getDynamicFields() {
87        return $this->fields;
88    }
89
90    // Multiple inheritance -- delegate methods not defined to a forms API
91    // Form
92    function __call($what, $args) {
93        $delegate = array($this->getForm(), $what);
94        if (!is_callable($delegate))
95            throw new Exception(sprintf(__('%s: Call to non-existing function'), $what));
96        return call_user_func_array($delegate, $args);
97    }
98
99    function getTitle() {
100        return $this->getLocal('title');
101    }
102
103    function getInstructions() {
104        return $this->getLocal('instructions');
105    }
106
107    /**
108     * Drop field errors clean info etc. Useful when replacing the source
109     * content of the form. This is necessary because the field listing is
110     * cached under some circumstances.
111     */
112    function reset() {
113        foreach ($this->getFields() as $f)
114            $f->reset();
115        return $this;
116    }
117
118    function getForm($source=false) {
119        if ($source)
120            $this->reset();
121        $fields = $this->getFields();
122        $form = new SimpleForm($fields, $source, array(
123            'title' => $this->getLocal('title'),
124            'instructions' => Format::htmldecode($this->getLocal('instructions')),
125            'id' => $this->getId(),
126            'type' => $this->type ?: null,
127        ));
128        return $form;
129    }
130
131    function hasFlag($flag) {
132        return (isset($this->flags) && ($this->flags & $flag) != 0);
133    }
134
135    function isDeleted() {
136        return $this->hasFlag(self::FLAG_DELETED);
137    }
138
139    function isDeletable() {
140        return $this->hasFlag(self::FLAG_DELETABLE);
141    }
142
143    function setFlag($flag) {
144        $this->flags |= $flag;
145    }
146
147    function hasAnyVisibleFields($user=false) {
148        global $thisstaff, $thisclient;
149        $user = $user ?: $thisstaff ?: $thisclient;
150        $visible = 0;
151        $isstaff = $user instanceof Staff;
152        foreach ($this->getFields() as $F) {
153            if ($isstaff) {
154                if ($F->isVisibleToStaff())
155                    $visible++;
156            }
157            elseif ($F->isVisibleToUsers()) {
158                $visible++;
159            }
160        }
161        return $visible > 0;
162    }
163
164    function instanciate($sort=1, $data=null) {
165        $inst = DynamicFormEntry::create(
166            array('form_id'=>$this->get('id'), 'sort'=>$sort)
167        );
168
169        if ($data)
170            $inst->setSource($data);
171
172        $inst->_fields = $this->_fields ?: null;
173
174        return $inst;
175    }
176
177    function disableFields(array $ids) {
178        foreach ($this->getFields() as $F) {
179            if (in_array($F->get('id'), $ids)) {
180                $F->disable();
181            }
182        }
183    }
184
185    function getTranslateTag($subtag) {
186        return _H(sprintf('form.%s.%s', $subtag, $this->id));
187    }
188    function getLocal($subtag) {
189        $tag = $this->getTranslateTag($subtag);
190        $T = CustomDataTranslation::translate($tag);
191        return $T != $tag ? $T : $this->get($subtag);
192    }
193
194    function save($refetch=false) {
195        if (count($this->dirty))
196            $this->set('updated', new SqlFunction('NOW'));
197        if ($rv = parent::save($refetch | $this->dirty))
198            return $this->saveTranslations();
199        return $rv;
200    }
201
202    function delete() {
203
204        if (!$this->isDeletable())
205            return false;
206
207        // Soft Delete: Mark the form as deleted.
208        $this->setFlag(self::FLAG_DELETED);
209        $type = array('type' => 'deleted');
210        Signal::send('object.deleted', $this, $type);
211        return $this->save();
212    }
213
214    function getExportableFields($exclude=array(), $prefix='__') {
215        $fields = array();
216        foreach ($this->getFields() as $f) {
217            // Ignore core fields
218            if ($exclude && in_array($f->get('name'), $exclude))
219                continue;
220            // Ignore non-data fields
221            // FIXME: Consider ::isStorable() too
222            elseif (!$f->hasData() || $f->isPresentationOnly())
223                continue;
224
225            $name = $f->get('name') ?: ('field_'.$f->get('id'));
226            $fields[$prefix.$name] = $f;
227        }
228        return $fields;
229    }
230
231    static function create($ht=false) {
232        $inst = new static($ht);
233        $inst->set('created', new SqlFunction('NOW'));
234        if (isset($ht['fields'])) {
235            $inst->save();
236            foreach ($ht['fields'] as $f) {
237                $field = DynamicFormField::create(array('form' => $inst) + $f);
238                $field->save();
239            }
240        }
241        return $inst;
242    }
243
244    function saveTranslations($vars=false) {
245        global $thisstaff;
246
247        $vars = $vars ?: $_POST;
248        $tags = array(
249            'title' => $this->getTranslateTag('title'),
250            'instructions' => $this->getTranslateTag('instructions'),
251        );
252        $rtags = array_flip($tags);
253        $translations = CustomDataTranslation::allTranslations($tags, 'phrase');
254        foreach ($translations as $t) {
255            $T = $rtags[$t->object_hash];
256            $content = @$vars['trans'][$t->lang][$T];
257            if (!isset($content))
258                continue;
259
260            // Content is not new and shouldn't be added below
261            unset($vars['trans'][$t->lang][$T]);
262
263            $t->text = $content;
264            $t->agent_id = $thisstaff->getId();
265            $t->updated = SqlFunction::NOW();
266            if (!$t->save())
267                return false;
268        }
269        // New translations (?)
270        if ($vars['trans'] && is_array($vars['trans'])) {
271            foreach ($vars['trans'] as $lang=>$parts) {
272                if (!Internationalization::isLanguageEnabled($lang))
273                    continue;
274                foreach ($parts as $T => $content) {
275                    $content = trim($content);
276                    if (!$content)
277                        continue;
278                    $t = CustomDataTranslation::create(array(
279                        'type'      => 'phrase',
280                        'object_hash' => $tags[$T],
281                        'lang'      => $lang,
282                        'text'      => $content,
283                        'agent_id'  => $thisstaff->getId(),
284                        'updated'   => SqlFunction::NOW(),
285                    ));
286                    if (!$t->save())
287                        return false;
288                }
289            }
290        }
291        return true;
292    }
293
294    static function rebuildDynamicDataViews() {
295        return self::ensureDynamicDataViews(true, true);
296    }
297
298    // ensure cdata tables exists
299    static function ensureDynamicDataViews($build=true, $force=false) {
300        $forms = ['TicketForm', 'TaskForm', 'UserForm', 'OrganizationForm'];
301        foreach ($forms as $form) {
302            if ($force && $build)
303                $form::dropDynamicDataView(false);
304            $form::ensureDynamicDataView($build);
305        }
306    }
307
308    static function ensureCdataTables($obj, $data) {
309        // Only perfrom check on real cron call, not autocrons triggered on
310        // agents activity.
311        if ($data['autocron'] === false)
312            self::ensureDynamicDataViews(true);
313    }
314
315    static function ensureDynamicDataView($build=false, $croak=true) {
316
317        if (!($cdata=static::$cdata) || !$cdata['table'])
318            return false;
319
320        $sql = 'SHOW TABLES LIKE \''.$cdata['table'].'\'';
321        // Return true if the cdata table exists
322        if (db_num_rows(db_query($sql)))
323            return true;
324
325        if (!$build && $croak)
326            die(sprintf('%s. %s.',
327                        __('Missing CDATA table'),
328                        __('Get technical support')));
329
330        return $build ? static::buildDynamicDataView($cdata) : false;
331    }
332
333    static function buildDynamicDataView($cdata) {
334        $sql = 'CREATE TABLE IF NOT EXISTS `'.$cdata['table'].'` (PRIMARY KEY
335                ('.$cdata['object_id'].')) DEFAULT CHARSET=utf8 AS '
336             .  static::getCrossTabQuery( $cdata['object_type'], $cdata['object_id']);
337        db_query($sql);
338    }
339
340    static function dropDynamicDataView($rebuild=true) {
341
342        if (!($cdata=static::$cdata) || !$cdata['table'])
343            return false;
344
345        $sql = 'DROP TABLE IF EXISTS `'.$cdata['table'].'`';
346        if (!db_query($sql))
347            return false;
348
349        return  $rebuild ?  static::ensureDynamicDataView($rebuild, false) :
350            true;
351    }
352
353    static function updateDynamicDataView($answer, $data) {
354        // TODO: Detect $data['dirty'] for value and value_id
355        // We're chiefly concerned with Ticket form answers
356
357        $cdata = static::$cdata;
358        if (!$cdata
359                || !$cdata['table']
360                || !($e = $answer->getEntry())
361                || $e->form->get('type') != $cdata['object_type'])
362            return;
363
364        // $record = array();
365        // $record[$f] = $answer->value'
366        // TicketFormData::objects()->filter(array('ticket_id'=>$a))
367        //      ->merge($record);
368        $sql = 'SHOW TABLES LIKE \''.$cdata['table'].'\'';
369        if (!db_num_rows(db_query($sql)))
370            return;
371
372        $f = $answer->getField();
373        $name = $f->get('name') ? $f->get('name')
374            : 'field_'.$f->get('id');
375        $fields = sprintf('`%s`=', $name) . db_input($answer->getSearchKeys());
376        $sql = 'INSERT INTO `'.$cdata['table'].'` SET '.$fields
377            . sprintf(', `%s`= %s',
378                    $cdata['object_id'],
379                    db_input($answer->getEntry()->get('object_id')))
380            .' ON DUPLICATE KEY UPDATE '.$fields;
381        db_query($sql);
382    }
383
384    static function updateDynamicFormEntryAnswer($answer, $data) {
385        if (!$answer
386                || !($e = $answer->getEntry())
387                || !$e->form)
388            return;
389
390        switch ($e->form->get('type')) {
391        case 'T':
392            return TicketForm::updateDynamicDataView($answer, $data);
393        case 'A':
394            return TaskForm::updateDynamicDataView($answer, $data);
395        case 'U':
396            return UserForm::updateDynamicDataView($answer, $data);
397        case 'O':
398            return OrganizationForm::updateDynamicDataView($answer, $data);
399        }
400
401    }
402
403    static function updateDynamicFormField($field, $data) {
404        if (!$field || !$field->form)
405            return;
406
407        switch ($field->form->get('type')) {
408        case 'T':
409            return TicketForm::dropDynamicDataView();
410        case 'A':
411            return TaskForm::dropDynamicDataView();
412        case 'U':
413            return UserForm::dropDynamicDataView();
414        case 'O':
415            return OrganizationForm::dropDynamicDataView();
416        }
417
418    }
419
420    static function getCrossTabQuery($object_type, $object_id='object_id', $exclude=array()) {
421        $fields = static::getDynamicDataViewFields($exclude);
422        return "SELECT entry.`object_id` as `$object_id`, ".implode(',', $fields)
423            .' FROM '.FORM_ENTRY_TABLE.' entry
424            JOIN '.FORM_ANSWER_TABLE.' ans ON ans.entry_id = entry.id
425            JOIN '.FORM_FIELD_TABLE." field ON field.id=ans.field_id
426            WHERE entry.object_type='$object_type' GROUP BY entry.object_id";
427    }
428
429    // Materialized View for custom data (MySQL FlexViews would be nice)
430    //
431    // @see http://code.google.com/p/flexviews/
432    static function getDynamicDataViewFields($exclude) {
433        $fields = array();
434        foreach (static::getInstance()->getFields() as $f) {
435            if ($exclude && in_array($f->get('name'), $exclude))
436                continue;
437
438            $impl = $f->getImpl($f);
439            if (!$impl->hasData() || $impl->isPresentationOnly())
440                continue;
441
442            $id = $f->get('id');
443            $name = ($f->get('name')) ? $f->get('name')
444                : 'field_'.$id;
445
446            if ($impl instanceof ChoiceField || $impl instanceof SelectionField) {
447                $fields[] = sprintf(
448                    'MAX(CASE WHEN field.id=\'%1$s\' THEN REPLACE(REPLACE(REPLACE(REPLACE(coalesce(ans.value_id, ans.value), \'{\', \'\'), \'}\', \'\'), \'"\', \'\'), \':\', \',\') ELSE NULL END) as `%2$s`',
449                    $id, $name);
450            }
451            else {
452                $fields[] = sprintf(
453                    'MAX(IF(field.id=\'%1$s\',coalesce(ans.value_id, ans.value),NULL)) as `%2$s`',
454                    $id, $name);
455            }
456        }
457        return $fields;
458    }
459
460
461
462}
463
464class UserForm extends DynamicForm {
465    static $instance;
466    static $form;
467
468    static $cdata = array(
469            'table' => USER_CDATA_TABLE,
470            'object_id' => 'user_id',
471            'object_type' => ObjectModel::OBJECT_TYPE_USER,
472        );
473
474    static function objects() {
475        $os = parent::objects();
476        return $os->filter(array('type'=>'U'));
477    }
478
479    static function getUserForm() {
480        if (!isset(static::$form)) {
481            static::$form = static::objects()->one();
482        }
483        return static::$form;
484    }
485
486    static function getInstance() {
487        if (!isset(static::$instance))
488            static::$instance = static::getUserForm()->instanciate();
489        return static::$instance;
490    }
491
492    static function getNewInstance() {
493        $o = static::objects()->one();
494        static::$instance = $o->instanciate();
495        return static::$instance;
496    }
497}
498Filter::addSupportedMatches(/* @trans */ 'User Data', function() {
499    $matches = array();
500    foreach (UserForm::getInstance()->getFields() as $f) {
501        if (!$f->hasData())
502            continue;
503        $matches['field.'.$f->get('id')] = __('User').' / '.$f->getLabel();
504        if (($fi = $f->getImpl()) && $fi->hasSubFields()) {
505            foreach ($fi->getSubFields() as $p) {
506                $matches['field.'.$f->get('id').'.'.$p->get('id')]
507                    = __('User').' / '.$f->getLabel().' / '.$p->getLabel();
508            }
509        }
510    }
511    return $matches;
512}, 20);
513
514class TicketForm extends DynamicForm {
515    static $instance;
516
517    static $cdata = array(
518            'table' => TICKET_CDATA_TABLE,
519            'object_id' => 'ticket_id',
520            'object_type' => 'T',
521        );
522
523    static function objects() {
524        $os = parent::objects();
525        return $os->filter(array('type'=>'T'));
526    }
527
528    static function getInstance() {
529        if (!isset(static::$instance))
530            self::getNewInstance();
531        return static::$instance;
532    }
533
534    static function getNewInstance() {
535        $o = static::objects()->one();
536        static::$instance = $o->instanciate();
537        return static::$instance;
538    }
539
540}
541// Add fields from the standard ticket form to the ticket filterable fields
542Filter::addSupportedMatches(/* @trans */ 'Ticket Data', function() {
543    $matches = array();
544    foreach (TicketForm::getInstance()->getFields() as $f) {
545        if (!$f->hasData())
546            continue;
547        $matches['field.'.$f->get('id')] = __('Ticket').' / '.$f->getLabel();
548        if (($fi = $f->getImpl()) && $fi->hasSubFields()) {
549            foreach ($fi->getSubFields() as $p) {
550                $matches['field.'.$f->get('id').'.'.$p->get('id')]
551                    = __('Ticket').' / '.$f->getLabel().' / '.$p->getLabel();
552            }
553        }
554    }
555    return $matches;
556}, 30);
557// Manage materialized view on custom data updates
558Signal::connect('model.created',
559    array('DynamicForm', 'updateDynamicFormEntryAnswer'),
560    'DynamicFormEntryAnswer');
561Signal::connect('model.updated',
562    array('DynamicForm', 'updateDynamicFormEntryAnswer'),
563    'DynamicFormEntryAnswer');
564// Recreate the dynamic view after new or removed fields to the ticket
565// details form
566Signal::connect('model.created',
567    array('DynamicForm', 'updateDynamicFormField'),
568    'DynamicFormField');
569Signal::connect('model.deleted',
570    array('DynamicForm', 'updateDynamicFormField'),
571    'DynamicFormField');
572// If the `name` column is in the dirty list, we would be renaming a
573// column. Delete the view instead.
574Signal::connect('model.updated',
575    array('DynamicForm', 'updateDynamicFormField'),
576    'DynamicFormField',
577    function($o, $d) { return isset($d['dirty'])
578        && (isset($d['dirty']['name']) || isset($d['dirty']['type'])); });
579
580// Check to make sure cdata tables exists
581Signal::connect('cron', array('DynamicForm', 'ensureCdataTables'));
582
583Filter::addSupportedMatches(/* trans */ 'Custom Forms', function() {
584    $matches = array();
585    foreach (DynamicForm::objects()->filter(array('type'=>'G')) as $form) {
586        foreach ($form->getFields() as $f) {
587            if (!$f->hasData())
588                continue;
589            $matches['field.'.$f->get('id')] = $form->getTitle().' / '.$f->getLabel();
590            if (($fi = $f->getImpl()) && $fi->hasSubFields()) {
591                foreach ($fi->getSubFields() as $p) {
592                    $matches['field.'.$f->get('id').'.'.$p->get('id')]
593                        = $form->getTitle().' / '.$f->getLabel().' / '.$p->getLabel();
594                }
595            }
596        }
597    }
598    return $matches;
599}, 9900);
600
601require_once(INCLUDE_DIR . "class.json.php");
602
603class DynamicFormField extends VerySimpleModel {
604
605    static $meta = array(
606        'table' => FORM_FIELD_TABLE,
607        'ordering' => array('sort'),
608        'pk' => array('id'),
609        'select_related' => array('form'),
610        'joins' => array(
611            'form' => array(
612                'null' => true,
613                'constraint' => array('form_id' => 'DynamicForm.id'),
614            ),
615            'answers' => array(
616                'reverse' => 'DynamicFormEntryAnswer.field',
617            ),
618        ),
619    );
620
621    var $_field;
622    var $_disabled = false;
623
624    const FLAG_ENABLED          = 0x00001;
625    const FLAG_EXT_STORED       = 0x00002; // Value stored outside of form_entry_value
626    const FLAG_CLOSE_REQUIRED   = 0x00004;
627
628    const FLAG_MASK_CHANGE      = 0x00010;
629    const FLAG_MASK_DELETE      = 0x00020;
630    const FLAG_MASK_EDIT        = 0x00040;
631    const FLAG_MASK_DISABLE     = 0x00080;
632    const FLAG_MASK_REQUIRE     = 0x10000;
633    const FLAG_MASK_VIEW        = 0x20000;
634    const FLAG_MASK_NAME        = 0x40000;
635
636    const MASK_MASK_INTERNAL    = 0x400B2;  # !change, !delete, !disable, !edit-name
637    const MASK_MASK_ALL         = 0x700F2;
638
639    const FLAG_CLIENT_VIEW      = 0x00100;
640    const FLAG_CLIENT_EDIT      = 0x00200;
641    const FLAG_CLIENT_REQUIRED  = 0x00400;
642
643    const MASK_CLIENT_FULL      = 0x00700;
644
645    const FLAG_AGENT_VIEW       = 0x01000;
646    const FLAG_AGENT_EDIT       = 0x02000;
647    const FLAG_AGENT_REQUIRED   = 0x04000;
648
649    const MASK_AGENT_FULL       = 0x7000;
650
651    // Multiple inheritance -- delegate methods not defined here to the
652    // forms API FormField instance
653    function __call($what, $args) {
654        return call_user_func_array(
655            array($this->getField(), $what), $args);
656    }
657
658    /**
659     * Fetch a forms API FormField instance which represents this designable
660     * DynamicFormField.
661     */
662    function getField() {
663        global $thisstaff;
664
665        // Finagle the `required` flag for the FormField instance
666        $ht = $this->ht;
667        $ht['required'] = ($thisstaff) ? $this->isRequiredForStaff()
668            : $this->isRequiredForUsers();
669
670        if (!isset($this->_field))
671            $this->_field = new FormField($ht);
672        return $this->_field;
673    }
674
675    function getForm() { return $this->form; }
676    function getFormId() { return $this->form_id; }
677
678    /**
679     * setConfiguration
680     *
681     * Used in the POST request of the configuration process. The
682     * ::getConfigurationForm() method should be used to retrieve a
683     * configuration form for this field. That form should be submitted via
684     * a POST request, and this method should be called in that request. The
685     * data from the POST request will be interpreted and will adjust the
686     * configuration of this field
687     *
688     * Parameters:
689     * vars - POST request / data
690     * errors - (OUT array) receives validation errors of the parsed
691     *      configuration form
692     *
693     * Returns:
694     * (bool) true if the configuration was updated, false if there were
695     * errors. If false, the errors were written into the received errors
696     * array.
697     */
698    function setConfiguration($vars, &$errors=array()) {
699        $config = array();
700        foreach ($this->getConfigurationForm($vars)->getFields() as $name=>$field) {
701            $config[$name] = $field->to_php($field->getClean());
702            $errors = array_merge($errors, $field->errors());
703        }
704
705        if (count($errors))
706            return false;
707
708        // See if field impl. need to save or override anything
709        $config = $this->getImpl()->to_config($config);
710        $this->set('configuration', JsonDataEncoder::encode($config));
711        $this->set('hint', Format::sanitize($vars['hint']) ?: NULL);
712
713        return true;
714    }
715
716    function isDeletable() {
717        return !$this->hasFlag(self::FLAG_MASK_DELETE);
718    }
719    function isNameForced() {
720        return $this->hasFlag(self::FLAG_MASK_NAME);
721    }
722    function isPrivacyForced() {
723        return $this->hasFlag(self::FLAG_MASK_VIEW);
724    }
725    function isRequirementForced() {
726        return $this->hasFlag(self::FLAG_MASK_REQUIRE);
727    }
728
729    function  isChangeable() {
730        return !$this->hasFlag(self::FLAG_MASK_CHANGE);
731    }
732
733    function  isEditable() {
734        return $this->hasFlag(self::FLAG_MASK_EDIT);
735    }
736    function disable() {
737        $this->_disabled = true;
738    }
739    function isEnabled() {
740        return !$this->_disabled && $this->hasFlag(self::FLAG_ENABLED);
741    }
742
743    function hasFlag($flag) {
744        return (isset($this->flags) && ($this->flags & $flag) != 0);
745    }
746
747    /**
748     * Describes the current visibility settings for this field. Returns a
749     * comma-separated, localized list of flag descriptions.
750     */
751    function getVisibilityDescription() {
752        $F = $this->flags;
753
754        if (!$this->hasFlag(self::FLAG_ENABLED))
755            return __('Disabled');
756
757        $impl = $this->getImpl();
758
759        $hints = array();
760        $VIEW = self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW;
761        if (($F & $VIEW) == 0) {
762            $hints[] = __('Hidden');
763        }
764        elseif (~$F & self::FLAG_CLIENT_VIEW) {
765            $hints[] = __('Internal');
766        }
767        elseif (~$F & self::FLAG_AGENT_VIEW) {
768            $hints[] = __('For EndUsers Only');
769        }
770        if ($impl->hasData()) {
771            if ($F & (self::FLAG_CLIENT_REQUIRED | self::FLAG_AGENT_REQUIRED)) {
772                $hints[] = __('Required');
773            }
774            else {
775                $hints[] = __('Optional');
776            }
777            if (!($F & (self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT))) {
778                $hints[] = __('Immutable');
779            }
780        }
781        return implode(', ', $hints);
782    }
783    function getTranslateTag($subtag) {
784        return _H(sprintf('field.%s.%s', $subtag, $this->id));
785    }
786    function getLocal($subtag, $default=false) {
787        $tag = $this->getTranslateTag($subtag);
788        $T = CustomDataTranslation::translate($tag);
789        return $T != $tag ? $T : ($default ?: $this->get($subtag));
790    }
791
792    /**
793     * Fetch a list of names to flag settings to make configuring new fields
794     * a bit easier.
795     *
796     * Returns:
797     * <Array['desc', 'flags']>, where the 'desc' key is a localized
798     * description of the flag set, and the 'flags' key is a bit mask of
799     * flags which should be set on the new field to implement the
800     * requirement / visibility mode.
801     */
802    function allRequirementModes() {
803        return array(
804            'a' => array('desc' => __('Optional'),
805                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
806                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT),
807            'b' => array('desc' => __('Required'),
808                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
809                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT
810                    | self::FLAG_CLIENT_REQUIRED | self::FLAG_AGENT_REQUIRED),
811            'c' => array('desc' => __('Required for EndUsers'),
812                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
813                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT
814                    | self::FLAG_CLIENT_REQUIRED),
815            'd' => array('desc' => __('Required for Agents'),
816                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
817                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT
818                    | self::FLAG_AGENT_REQUIRED),
819            'e' => array('desc' => __('Internal, Optional'),
820                'flags' => self::FLAG_AGENT_VIEW | self::FLAG_AGENT_EDIT),
821            'f' => array('desc' => __('Internal, Required'),
822                'flags' => self::FLAG_AGENT_VIEW | self::FLAG_AGENT_EDIT
823                    | self::FLAG_AGENT_REQUIRED),
824            'g' => array('desc' => __('For EndUsers Only'),
825                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_CLIENT_EDIT
826                    | self::FLAG_CLIENT_REQUIRED),
827        );
828    }
829
830    /**
831     * Fetch a list of valid requirement modes for this field. This list
832     * will be filtered based on flags which are not supported or not
833     * allowed for this field.
834     *
835     * Deprecated:
836     * This was used in previous versions when a drop-down list was
837     * presented for editing a field's visibility. The current software
838     * version presents the drop-down list for new fields only.
839     *
840     * Returns:
841     * <Array['desc', 'flags']> Filtered list from ::allRequirementModes
842     */
843    function getAllRequirementModes() {
844        $modes = static::allRequirementModes();
845        if ($this->isPrivacyForced()) {
846            // Required to be internal
847            foreach ($modes as $m=>$info) {
848                if ($info['flags'] & (self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW))
849                    unset($modes[$m]);
850            }
851        }
852
853        if ($this->isRequirementForced()) {
854            // Required to be required
855            foreach ($modes as $m=>$info) {
856                if ($info['flags'] & (self::FLAG_CLIENT_REQUIRED | self::FLAG_AGENT_REQUIRED))
857                    unset($modes[$m]);
858            }
859        }
860        return $modes;
861    }
862
863    function setRequirementMode($mode) {
864        $modes = $this->getAllRequirementModes();
865        if (!isset($modes[$mode]))
866            return false;
867
868        $info = $modes[$mode];
869        $this->set('flags', $info['flags'] | self::FLAG_ENABLED);
870    }
871
872    function isRequiredForStaff() {
873        return $this->hasFlag(self::FLAG_AGENT_REQUIRED);
874    }
875    function isRequiredForUsers() {
876        return $this->hasFlag(self::FLAG_CLIENT_REQUIRED);
877    }
878    function isRequiredForClose() {
879        return $this->hasFlag(self::FLAG_CLOSE_REQUIRED);
880    }
881    function isEditableToStaff() {
882        return $this->isEnabled()
883            && $this->hasFlag(self::FLAG_AGENT_EDIT);
884    }
885    function isVisibleToStaff() {
886        return $this->isEnabled()
887            && $this->hasFlag(self::FLAG_AGENT_VIEW);
888    }
889    function isEditableToUsers() {
890        return $this->isEnabled()
891            && $this->hasFlag(self::FLAG_CLIENT_EDIT);
892    }
893    function isVisibleToUsers() {
894        return $this->isEnabled()
895            && $this->hasFlag(self::FLAG_CLIENT_VIEW);
896    }
897
898    function addToQuery($query, $name=false) {
899        return $query->values($name ?: $this->get('name'));
900    }
901
902    /**
903     * Used when updating the form via the admin panel. This represents
904     * validation on the form field template, not data entered into a form
905     * field of a custom form. The latter would be isValidEntry()
906     */
907    function isValid() {
908        if (count($this->errors()))
909            return false;
910        if (!$this->get('label'))
911            $this->addError(
912                __("Label is required for custom form fields"), "label");
913        if (($this->isRequiredForStaff() || $this->isRequiredForUsers())
914            && !$this->get('name')
915        ) {
916            $this->addError(
917                __("Variable name is required for required fields"
918                /* `required` is a visibility setting fields */
919                /* `variable` is used for automation. Internally it's called `name` */
920                ), "name");
921        }
922        if (preg_match('/[.{}\'"`; ]/u', $this->get('name')))
923            $this->addError(__(
924                'Invalid character in variable name. Please use letters and numbers only.'
925            ), 'name');
926        return count($this->errors()) == 0;
927    }
928
929    function delete() {
930        $values = $this->answers->count();
931
932        // Don't really delete form fields with data as that will screw up the data
933        // model. Instead, just drop the association with the form which
934        // will give the appearance of deletion. Not deleting means that
935        // the field will continue to exist on form entries it may already
936        // have answers on, but since it isn't associated with the form, it
937        // won't be available for new form submittals.
938        $this->set('form_id', 0);
939
940        $impl = $this->getImpl();
941
942        // Trigger db_clean so the field can do house cleaning
943        $impl->db_cleanup(true);
944
945        // Short-circuit deletion if the field has data.
946        if ($impl->hasData() && $values)
947            return $this->save();
948
949        // Delete the field for realz
950        parent::delete();
951
952    }
953
954    function save($refetch=false) {
955        if (count($this->dirty))
956            $this->set('updated', new SqlFunction('NOW'));
957        return parent::save($this->dirty || $refetch);
958    }
959
960    static function create($ht=false) {
961        $inst = new static($ht);
962        $inst->set('created', new SqlFunction('NOW'));
963        if (isset($ht['configuration']))
964            $inst->configuration = JsonDataEncoder::encode($ht['configuration']);
965        return $inst;
966    }
967}
968
969/**
970 * Represents an entry to a dynamic form. Used to render the completed form
971 * in reference to the attached ticket, etc. A form is used to represent the
972 * template of enterable data. This represents the data entered into an
973 * instance of that template.
974 *
975 * The data of the entry is called 'answers' in this model. This model
976 * represents an instance of a form entry. The data / answers to that entry
977 * are represented individually in the DynamicFormEntryAnswer model.
978 */
979class DynamicFormEntry extends VerySimpleModel {
980
981    static $meta = array(
982        'table' => FORM_ENTRY_TABLE,
983        'ordering' => array('sort'),
984        'pk' => array('id'),
985        'select_related' => array('form'),
986        'joins' => array(
987            'form' => array(
988                'null' => true,
989                'constraint' => array('form_id' => 'DynamicForm.id'),
990            ),
991            'answers' => array(
992                'reverse' => 'DynamicFormEntryAnswer.entry',
993            ),
994        ),
995    );
996
997    var $_fields;
998    var $_form;
999    var $_errors = false;
1000    var $_clean = false;
1001    var $_source = null;
1002
1003    function getId() {
1004        return $this->get('id');
1005    }
1006    function getFormId() {
1007        return $this->form_id;
1008    }
1009
1010    function getAnswers() {
1011        return $this->answers;
1012    }
1013
1014    function getAnswer($name) {
1015        foreach ($this->getAnswers() as $ans)
1016            if ($ans->getField()->get('name') == $name)
1017                return $ans;
1018        return null;
1019    }
1020
1021    function setAnswer($name, $value, $id=false) {
1022
1023        if ($ans=$this->getAnswer($name)) {
1024            $f = $ans->getField();
1025            if ($f->isStorable())
1026                $ans->setValue($value, $id);
1027        }
1028    }
1029
1030    function errors() {
1031        return $this->_errors;
1032    }
1033
1034    function getTitle() {
1035        return $this->form->getTitle();
1036    }
1037
1038    function getInstructions() {
1039        return $this->form->getInstructions();
1040    }
1041
1042    function getDynamicForm() {
1043        return $this->form;
1044    }
1045
1046    function getForm($source=false, $options=array()) {
1047        if (!isset($this->_form)) {
1048
1049            $fields = $this->getFields();
1050            if (isset($this->extra)) {
1051                $x = JsonDataParser::decode($this->extra) ?: array();
1052                foreach ($x['disable'] ?: array() as $id) {
1053                    unset($fields[$id]);
1054                }
1055            }
1056
1057            $source = $source ?: $this->getSource();
1058            $options += array(
1059                'title' => $this->getTitle(),
1060                'instructions' => $this->getInstructions(),
1061                'id' => $this->form_id,
1062                'type' => $this->getDynamicForm()->type ?: null,
1063                );
1064            $this->_form = new CustomForm($fields, $source, $options);
1065        }
1066
1067
1068        return $this->_form;
1069    }
1070
1071    function getDynamicFields() {
1072        return $this->form->fields;
1073    }
1074
1075    function getMedia() {
1076        return $this->getForm()->getMedia();
1077    }
1078
1079    function getFields() {
1080        if (!isset($this->_fields)) {
1081            $this->_fields = array();
1082            // Get all dynamic fields associated with the form
1083            // even when stored elsewhere -- important during validation
1084            foreach ($this->getDynamicFields() as $f) {
1085                $f = $f->getImpl($f);
1086                $this->_fields[$f->get('id')] = $f;
1087                $f->isnew = true;
1088            }
1089            // Include any other answers included in this entry, which may
1090            // be for fields which have since been deleted
1091            foreach ($this->getAnswers() as $a) {
1092                $f = $a->getField();
1093                $id = $f->get('id');
1094                if (!isset($this->_fields[$id])) {
1095                    // This field is not currently on the associated form
1096                    $a->deleted = true;
1097                }
1098                $this->_fields[$id] = $f;
1099                // This field has an answer, so it isn't new (to this entry)
1100                $f->isnew = false;
1101            }
1102        }
1103        return $this->_fields;
1104    }
1105
1106    function filterFields($filter) {
1107        $this->getFields();
1108        foreach ($this->_fields as $i=>$f) {
1109            if ($filter($f))
1110                unset($this->_fields[$i]);
1111        }
1112    }
1113
1114    function getSource() {
1115        return $this->_source ?: (isset($this->id) ? false : $_POST);
1116    }
1117    function setSource($source) {
1118        $this->_source = $source;
1119        // Ensure the field is connected to this data source
1120        foreach ($this->getFields() as $F)
1121            if (!$F->getForm())
1122                $F->setForm($this);
1123    }
1124
1125    function getField($name) {
1126        foreach ($this->getFields() as $field)
1127            if (!strcasecmp($field->get('name'), $name))
1128                return $field;
1129
1130        return null;
1131    }
1132
1133    /**
1134     * Validate the form and indicate if there no errors.
1135     *
1136     * Parameters:
1137     * $filter - (callback) function to receive each field and return
1138     *      boolean true if the field's errors are significant
1139     * $options - options to pass to form and fields.
1140     *
1141     */
1142    function isValid($filter=false, $options=array()) {
1143
1144        if (!is_array($this->_errors)) {
1145            $form = $this->getForm(false, $options);
1146            $form->isValid($filter);
1147            $this->_errors = $form->errors();
1148        }
1149
1150        return !$this->_errors;
1151    }
1152
1153    function isValidForClient($update=false) {
1154        $filter = function($f) use($update) {
1155            return $update ? $f->isEditableToUsers() :
1156                $f->isVisibleToUsers();
1157        };
1158        return $this->isValid($filter);
1159    }
1160
1161    function isValidForStaff($update=false) {
1162        $filter = function($f) use($update) {
1163            return $update ? $f->isEditableToStaff() :
1164                $f->isVisibleToStaff();
1165        };
1166        return $this->isValid($filter);
1167    }
1168
1169    function getClean() {
1170        return $this->getForm()->getClean();
1171    }
1172
1173    /**
1174     * Compile a list of data used by the filtering system to match dynamic
1175     * content in this entry. This returs an array of `field.<id>` =>
1176     * <value> pairs where the <id> is the field id and the <value> is the
1177     * toString() value for the entered data.
1178     *
1179     * If the field returns an array for its ::getFilterData() method, the
1180     * data will be added in the array with the keys prefixed with
1181     * `field.<id>`. This is useful for properties on custom lists, for
1182     * instance, which can contain properties usefule for matching and
1183     * filtering.
1184     */
1185    function getFilterData() {
1186        $vars = array();
1187        foreach ($this->getFields() as $f) {
1188            $tag = 'field.'.$f->get('id');
1189            if ($d = $f->getFilterData()) {
1190                if (is_array($d)) {
1191                    foreach ($d as $k=>$v) {
1192                        if (is_string($k))
1193                            $vars["$tag$k"] = $v;
1194                        else
1195                            $vars[$tag] = $v;
1196                    }
1197                }
1198                else {
1199                    $vars[$tag] = $d;
1200                }
1201            }
1202        }
1203        return $vars;
1204    }
1205
1206    function forTicket($ticket_id, $force=false) {
1207        static $entries = array();
1208        if (!isset($entries[$ticket_id]) || $force) {
1209            $stuff = DynamicFormEntry::objects()
1210                ->filter(array('object_id'=>$ticket_id, 'object_type'=>'T'));
1211            // If forced, don't cache the result
1212            if ($force)
1213                return $stuff;
1214            $entries[$ticket_id] = &$stuff;
1215        }
1216        return $entries[$ticket_id];
1217    }
1218
1219    function forTask($id, $force=false) {
1220        static $entries = array();
1221        if (!isset($entries[$id]) || $force) {
1222            $stuff = DynamicFormEntry::objects()->filter(array(
1223                        'object_id' => $id,
1224                        'object_type' => ObjectModel::OBJECT_TYPE_TASK
1225                        ));
1226            // If forced, don't cache the result
1227            if ($force)
1228                return $stuff;
1229
1230            $entries[$id] = &$stuff;
1231        }
1232        return $entries[$id];
1233    }
1234
1235    function setTicketId($ticket_id) {
1236        $this->object_type = 'T';
1237        $this->object_id = $ticket_id;
1238    }
1239
1240    function setClientId($user_id) {
1241        $this->object_type = 'U';
1242        $this->object_id = $user_id;
1243    }
1244
1245    function setObjectId($object_id) {
1246        $this->object_id = $object_id;
1247    }
1248
1249    function forObject($object_id, $object_type) {
1250        return DynamicFormEntry::objects()
1251            ->filter(array('object_id'=>$object_id, 'object_type'=>$object_type));
1252    }
1253
1254    function render($options=array()) {
1255        $options += array('staff' => true);
1256        return $this->getForm()->render($options);
1257    }
1258
1259    function getChanges() {
1260        $fields = array();
1261        foreach ($this->getAnswers() as $a) {
1262            $field = $a->getField();
1263            if (!$field->hasData() || $field->isPresentationOnly())
1264                continue;
1265            $changes = $field->getChanges();
1266            if (!$changes)
1267                continue;
1268            $fields[$field->get('id')] = $changes;
1269        }
1270        return $fields;
1271    }
1272
1273    /**
1274     * addMissingFields
1275     *
1276     * Adds fields that have been added to the linked form (field set) since
1277     * this entry was originally created. If fields are added to the form,
1278     * the method will automatically add the fields and null answers to the
1279     * entry.
1280     */
1281    function addMissingFields() {
1282        foreach ($this->getFields() as $field) {
1283            if ($field->isnew && $field->isEnabled()
1284                && !$field->isPresentationOnly()
1285                && $field->hasData()
1286                && $field->isStorable()
1287            ) {
1288                $a = new DynamicFormEntryAnswer(
1289                    array('field_id'=>$field->get('id'), 'entry'=>$this));
1290
1291                // Add to list of answers
1292                $this->answers->add($a);
1293
1294                // Omit fields without data and non-storable fields.
1295                if (!$field->hasData() || !$field->isStorable())
1296                    continue;
1297
1298                $a->save();
1299            }
1300        }
1301
1302        // Sort the form the way it is declared to be sorted
1303        if ($this->_fields) {
1304            uasort($this->_fields,
1305                function($a, $b) {
1306                    return $a->get('sort') - $b->get('sort');
1307            });
1308        }
1309    }
1310
1311    /**
1312     * Save the form entry and all associated answers.
1313     *
1314     */
1315
1316    function save($refetch=false) {
1317        return $this->saveAnswers(null, $refetch);
1318    }
1319
1320    /**
1321     * Save the form entry and all associated answers.
1322     *
1323     * Returns:
1324     * (mixed) FALSE if updated failed, otherwise the number of dirty answers
1325     * which were save is returned (which may be ZERO).
1326     */
1327
1328    function saveAnswers($isEditable=null, $refetch=false) {
1329        if (count($this->dirty))
1330            $this->set('updated', new SqlFunction('NOW'));
1331
1332        if (!parent::save($refetch || count($this->dirty)))
1333            return false;
1334
1335        $dirty = 0;
1336        foreach ($this->getAnswers() as $a) {
1337            $field = $a->getField();
1338            // Don't save answers for presentation-only fields or fields
1339            // which are stored elsewhere or those which are not editable
1340            if (!$field->hasData()
1341                    || !$field->isStorable()
1342                    || $field->isPresentationOnly()
1343                    || ($isEditable && !$isEditable($field))) {
1344                continue;
1345            }
1346            // Set the entry here so that $field->getClean() can use the
1347            // entry-id if necessary
1348            $a->entry = $this;
1349
1350            try {
1351                $field->setForm($this);
1352                //for form entry values of file upload fields, we want to save the value as
1353                //json with the file id(s) and file name(s) of each file stored in the field
1354                //so that they display correctly within tasks/tickets
1355                if (get_class($field) == 'FileUploadField') {
1356                    //use getChanges if getClean returns an empty array
1357                    $fieldClean = $field->getClean() ?: $field->getChanges();
1358                    if (is_array($fieldClean) && $fieldClean[0])
1359                        $fieldClean = json_decode($fieldClean[0], true);
1360                } else
1361                    $fieldClean = $field->getClean();
1362
1363                $val = $field->to_database($fieldClean);
1364            }
1365            catch (FieldUnchanged $e) {
1366                // Don't update the answer.
1367                continue;
1368            }
1369            if (is_array($val)) {
1370                $a->set('value', $val[0]);
1371                $a->set('value_id', $val[1]);
1372            }
1373            else {
1374                $a->set('value', $val);
1375            }
1376            if ($a->dirty)
1377                $dirty++;
1378            $a->save($refetch);
1379        }
1380        return $dirty;
1381    }
1382
1383    function delete() {
1384        if (!parent::delete())
1385            return false;
1386
1387        foreach ($this->getAnswers() as $a)
1388            $a->delete();
1389
1390        return true;
1391    }
1392
1393    static function create($ht=false, $data=null) {
1394        $inst = new static($ht);
1395        $inst->set('created', new SqlFunction('NOW'));
1396        if ($data)
1397            $inst->setSource($data);
1398        foreach ($inst->getDynamicFields() as $field) {
1399            if (!($impl = $field->getImpl($field)))
1400                continue;
1401            if (!$impl->hasData() || !$impl->isStorable())
1402                continue;
1403            $a = new DynamicFormEntryAnswer(
1404                array('field'=>$field, 'entry'=>$inst));
1405            $a->field->setAnswer($a);
1406            $inst->answers->add($a);
1407        }
1408        return $inst;
1409    }
1410}
1411
1412/**
1413 * Represents a single answer to a single field on a dynamic form. The
1414 * data / answer to the field is linked back to the form and field which was
1415 * originally used for the submission.
1416 */
1417class DynamicFormEntryAnswer extends VerySimpleModel {
1418
1419    static $meta = array(
1420        'table' => FORM_ANSWER_TABLE,
1421        'ordering' => array('field__sort'),
1422        'pk' => array('entry_id', 'field_id'),
1423        'select_related' => array('field'),
1424        'fields' => array('entry_id', 'field_id', 'value', 'value_id'),
1425        'joins' => array(
1426            'field' => array(
1427                'constraint' => array('field_id' => 'DynamicFormField.id'),
1428            ),
1429            'entry' => array(
1430                'constraint' => array('entry_id' => 'DynamicFormEntry.id'),
1431            ),
1432        ),
1433    );
1434
1435    var $_field;
1436    var $deleted = false;
1437    var $_value;
1438
1439    function getEntry() {
1440        return $this->entry;
1441    }
1442
1443    function getForm() {
1444        return $this->getEntry()->getForm();
1445    }
1446
1447    function getField() {
1448        if (!isset($this->_field)) {
1449            $this->_field = $this->field->getImpl($this->field);
1450            $this->_field->setAnswer($this);
1451        }
1452        return $this->_field;
1453    }
1454
1455    function getValue() {
1456
1457        if (!isset($this->_value)) {
1458            //XXX: We're settting the value here to avoid infinite loop
1459            $this->_value = false;
1460            if (isset($this->value))
1461                $this->_value = $this->getField()->to_php(
1462                        $this->get('value'), $this->get('value_id'));
1463        }
1464
1465        return $this->_value;
1466    }
1467
1468    function setValue($value, $id=false) {
1469        $this->getField()->reset();
1470        $this->_value = null;
1471        $this->set('value', $value);
1472        if ($id !== false)
1473            $this->set('value_id', $id);
1474    }
1475
1476    function getLocal($tag) {
1477        return $this->field->getLocal($tag);
1478    }
1479
1480    function getIdValue() {
1481        return $this->get('value_id');
1482    }
1483
1484    function isDeleted() {
1485        return $this->deleted;
1486    }
1487
1488    function toString() {
1489        return $this->getField()->toString($this->getValue());
1490    }
1491
1492    function display() {
1493        return $this->getField()->display($this->getValue());
1494    }
1495
1496    function getSearchable($include_label=false) {
1497        if ($include_label)
1498            $label = Format::searchable($this->getField()->getLabel()) . " ";
1499        return sprintf("%s%s", $label,
1500            $this->getField()->searchable($this->getValue())
1501        );
1502    }
1503
1504    function getSearchKeys() {
1505        return implode(',', (array) $this->getField()->getKeys($this->getValue()));
1506    }
1507
1508    function asVar() {
1509        return $this->getField()->asVar(
1510            $this->get('value'), $this->get('value_id')
1511        );
1512    }
1513
1514    function getVar($tag) {
1515        if (is_object($var = $this->asVar()) && method_exists($var, 'getVar'))
1516            return $var->getVar($tag);
1517    }
1518
1519    function __toString() {
1520        $v = $this->toString();
1521        return is_string($v) ? $v : (string) $this->getValue();
1522    }
1523
1524    function delete() {
1525        if (!parent::delete())
1526            return false;
1527
1528        // Allow the field to cleanup anything else in the database
1529        $this->getField()->db_cleanup();
1530        return true;
1531    }
1532
1533    function save($refetch=false) {
1534        if ($this->dirty)
1535            unset($this->_value);
1536        return parent::save($refetch);
1537    }
1538}
1539
1540class SelectionField extends FormField {
1541    static $widget = 'ChoicesWidget';
1542
1543    function getListId() {
1544        list(,$list_id) = explode('-', $this->get('type'));
1545        return $list_id ?: $this->get('list_id');
1546    }
1547
1548    function getList() {
1549        if (!$this->_list)
1550            $this->_list = DynamicList::lookup($this->getListId());
1551
1552        return $this->_list;
1553    }
1554
1555    function getWidget($widgetClass=false) {
1556        $config = $this->getConfiguration();
1557        if ($config['widget'] == 'typeahead' && $config['multiselect'] == false)
1558            $widgetClass = 'TypeaheadSelectionWidget';
1559        elseif ($config['widget'] == 'textbox')
1560            $widgetClass = 'TextboxSelectionWidget';
1561
1562        return parent::getWidget($widgetClass);
1563    }
1564
1565    function display($value) {
1566        global $thisstaff;
1567
1568        if (!is_array($value)
1569                || !$thisstaff // Only agents can preview for now
1570                || !($list=$this->getList()))
1571            return parent::display($value);
1572
1573        $display = array();
1574        foreach ($value as $k => $v) {
1575            if (is_numeric($k)
1576                    && ($i=$list->getItem((int) $k))
1577                    && $i->hasProperties())
1578                $display[] = $i->display();
1579            else // Perhaps deleted  entry
1580                $display[] = $v;
1581        }
1582
1583        return implode(',', $display);
1584
1585    }
1586
1587    function parse($value) {
1588
1589        if (!($list=$this->getList()))
1590            return null;
1591
1592        $config = $this->getConfiguration();
1593        $choices = $this->getChoices();
1594        $selection = array();
1595
1596        if ($value && !is_array($value))
1597            $value = array($value);
1598
1599        if ($value && is_array($value)) {
1600            foreach ($value as $k=>$v) {
1601                if ($k && ($i=$list->getItem((int) $k)))
1602                    $selection[$i->getId()] = $i->getValue();
1603                elseif (isset($choices[$k]))
1604                    $selection[$k] = $choices[$k];
1605                elseif (isset($choices[$v]))
1606                    $selection[$v] = $choices[$v];
1607                elseif (($i=$list->getItem($v, true)))
1608                    $selection[$i->getId()] = $i->getValue();
1609            }
1610        } elseif($value) {
1611            //Assume invalid textbox input to be validated
1612            $selection[] = $value;
1613        }
1614
1615        // Don't return an empty array
1616        return $selection ?: null;
1617    }
1618
1619    function to_database($value) {
1620        if (is_array($value)) {
1621            reset($value);
1622        }
1623        if ($value && is_array($value))
1624            $value = JsonDataEncoder::encode($value);
1625
1626        return $value;
1627    }
1628
1629    function to_php($value, $id=false) {
1630        if (is_string($value))
1631            $value = JsonDataParser::parse($value) ?: $value;
1632
1633        if (!is_array($value)) {
1634            $values = array();
1635            $choices = $this->getChoices();
1636            foreach (explode(',', $value) as $V) {
1637                if (isset($choices[$V]))
1638                    $values[$V] = $choices[$V];
1639            }
1640            if ($id && isset($choices[$id]))
1641                $values[$id] = $choices[$id];
1642
1643            if ($values)
1644                return $values;
1645            // else return $value unchanged
1646        }
1647        // Don't set the ID here as multiselect prevents using exactly one
1648        // ID value. Instead, stick with the JSON value only.
1649        return $value;
1650    }
1651
1652    function getKeys($value) {
1653        if (!is_array($value))
1654            $value = $this->getChoice($value);
1655        if (is_array($value))
1656            return implode(', ', array_keys($value));
1657        return (string) $value;
1658    }
1659
1660    // PHP 5.4 Move this to a trait
1661    function whatChanged($before, $after) {
1662        $before = (array) $before;
1663        $after = (array) $after;
1664        $added = array_diff($after, $before);
1665        $deleted = array_diff($before, $after);
1666        $added = array_map(array($this, 'display'), $added);
1667        $deleted = array_map(array($this, 'display'), $deleted);
1668
1669        if ($added && $deleted) {
1670            $desc = sprintf(
1671                __('added <strong>%1$s</strong> and removed <strong>%2$s</strong>'),
1672                implode(', ', $added), implode(', ', $deleted));
1673        }
1674        elseif ($added) {
1675            $desc = sprintf(
1676                __('added <strong>%1$s</strong>'),
1677                implode(', ', $added));
1678        }
1679        elseif ($deleted) {
1680            $desc = sprintf(
1681                __('removed <strong>%1$s</strong>'),
1682                implode(', ', $deleted));
1683        }
1684        else {
1685            $desc = sprintf(
1686                __('changed to <strong>%1$s</strong>'),
1687                $this->display($after));
1688        }
1689        return $desc;
1690    }
1691
1692    function asVar($value, $id=false) {
1693        $values = $this->to_php($value, $id);
1694        if (is_array($values)) {
1695            return new PlaceholderList($this->getList()->getAllItems()
1696                ->filter(array('id__in' => array_keys($values)))
1697            );
1698        }
1699    }
1700
1701    function hasSubFields() {
1702        return $this->getList()->getForm();
1703    }
1704    function getSubFields() {
1705        $fields = new ListObject(array(
1706            new TextboxField(array(
1707                // XXX: i18n: Change to a better word when the UI changes
1708                'label' => '['.__('Abbrev').']',
1709                'id' => 'abb',
1710            ))
1711        ));
1712        $form = $this->getList()->getForm();
1713        if ($form && ($F = $form->getFields()))
1714            $fields->extend($F);
1715        return $fields;
1716    }
1717
1718    function toString($items) {
1719        return is_array($items)
1720            ? implode(', ', $items) : (string) $items;
1721    }
1722
1723    function validateEntry($entry) {
1724        parent::validateEntry($entry);
1725        if (!$this->errors()) {
1726            $config = $this->getConfiguration();
1727            if ($config['widget'] == 'textbox') {
1728                if ($entry && (
1729                        !($k=key($entry))
1730                     || !($i=$this->getList()->getItem((int) $k))
1731                 )) {
1732                    $config = $this->getConfiguration();
1733                    $this->_errors[] = $this->getLocal('validator-error', $config['validator-error'])
1734                        ?: __('Unknown or invalid input');
1735                }
1736            } elseif ($config['typeahead']
1737                    && ($entered = $this->getWidget()->getEnteredValue())
1738                    && !in_array($entered, $entry)
1739                    && $entered != $entry) {
1740                $this->_errors[] = __('Select a value from the list');
1741           }
1742        }
1743    }
1744
1745    function getConfigurationOptions() {
1746        return array(
1747            'multiselect' => new BooleanField(array(
1748                'id'=>2,
1749                'label'=>__(/* Type of widget allowing multiple selections */ 'Multiselect'),
1750                'required'=>false, 'default'=>false,
1751                'configuration'=>array(
1752                    'desc'=>__('Allow multiple selections')),
1753            )),
1754            'widget' => new ChoiceField(array(
1755                'id'=>1,
1756                'label'=>__('Widget'),
1757                'required'=>false, 'default' => 'dropdown',
1758                'choices'=>array(
1759                    'dropdown' => __('Drop Down'),
1760                    'typeahead' => __('Typeahead'),
1761                    'textbox' => __('Text Input'),
1762                ),
1763                'configuration'=>array(
1764                    'multiselect' => false,
1765                ),
1766                'visibility' => new VisibilityConstraint(
1767                    new Q(array('multiselect__eq'=>false)),
1768                    VisibilityConstraint::HIDDEN
1769                ),
1770                'hint'=>__('Typeahead will work better for large lists')
1771            )),
1772            'validator-error' => new TextboxField(array(
1773                'id'=>5, 'label'=>__('Validation Error'), 'default'=>'',
1774                'configuration'=>array('size'=>40, 'length'=>80,
1775                    'translatable'=>$this->getTranslateTag('validator-error')
1776                ),
1777                'visibility' => new VisibilityConstraint(
1778                    new Q(array('widget__eq'=>'textbox')),
1779                    VisibilityConstraint::HIDDEN
1780                ),
1781                'hint'=>__('Message shown to user if the item entered is not in the list')
1782            )),
1783            'prompt' => new TextboxField(array(
1784                'id'=>3,
1785                'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
1786                'hint'=>__('Leading text shown before a value is selected'),
1787                'configuration'=>array('size'=>40, 'length'=>40,
1788                    'translatable'=>$this->getTranslateTag('prompt'),
1789                ),
1790            )),
1791            'default' => new SelectionField(array(
1792                'id'=>4, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
1793                'list_id'=>$this->getListId(),
1794                'configuration' => array('prompt'=>__('Select a Default')),
1795            )),
1796        );
1797    }
1798
1799    function getConfiguration() {
1800
1801        $config = parent::getConfiguration();
1802        if ($config['widget'])
1803            $config['typeahead'] = $config['widget'] == 'typeahead';
1804
1805        // Drop down list does not support multiple selections
1806        if ($config['typeahead'])
1807            $config['multiselect'] = false;
1808
1809        return $config;
1810    }
1811
1812    function getChoices($verbose=false, $options=array()) {
1813        if (!$this->_choices || $verbose) {
1814            $choices = array();
1815            foreach ($this->getList()->getItems() as $i)
1816                $choices[$i->getId()] = $i->getValue();
1817
1818            // Retired old selections
1819            $values = ($a=$this->getAnswer()) ? $a->getValue() : array();
1820            if ($values && is_array($values)) {
1821                foreach ($values as $k => $v) {
1822                    if (!isset($choices[$k])) {
1823                        if ($verbose) $v .= ' '.__('(retired)');
1824                        $choices[$k] = $v;
1825                    }
1826                }
1827            }
1828
1829            if ($verbose) // Don't cache
1830                return $choices;
1831
1832            $this->_choices = $choices;
1833        }
1834
1835        return $this->_choices;
1836    }
1837
1838    function getChoice($value) {
1839        $choices = $this->getChoices();
1840        if ($value && is_array($value)) {
1841            $selection = $value;
1842        } elseif (isset($choices[$value]))
1843            $selection[] = $choices[$value];
1844        elseif ($this->get('default'))
1845            $selection[] = $choices[$this->get('default')];
1846
1847        return $selection;
1848    }
1849
1850    function lookupChoice($value) {
1851
1852        // See if it's in the choices.
1853        $choices = $this->getChoices();
1854        if ($choices && ($i=array_search($value, $choices)))
1855            return array($i=>$choices[$i]);
1856
1857        // Query the store by value or extra (abbrv.)
1858        if (!($list=$this->getList()))
1859            return null;
1860
1861        if ($i = $list->getItem($value))
1862            return array($i->getId() => $i->getValue());
1863
1864        if ($i = $list->getItem($value, true))
1865            return array($i->getId() => $i->getValue());
1866
1867        return null;
1868    }
1869
1870
1871    function getFilterData() {
1872        // Start with the filter data for the list item as the [0] index
1873        $data = array(parent::getFilterData());
1874        if (($v = $this->getClean())) {
1875            // Add in the properties for all selected list items in sub
1876            // labeled by their field id
1877            foreach ($v as $id=>$L) {
1878                if (!($li = DynamicListItem::lookup($id))
1879                      || !$li->getListId())
1880                    continue;
1881                foreach ($li->getFilterData() as $prop=>$value) {
1882                    if (!isset($data[$prop]))
1883                        $data[$prop] = $value;
1884                    else
1885                        $data[$prop] .= " $value";
1886                }
1887            }
1888        }
1889        return $data;
1890    }
1891
1892    function getSearchMethods() {
1893        return array(
1894            'set' =>        __('has a value'),
1895            'nset' =>     __('does not have a value'),
1896            'includes' =>   __('includes'),
1897            '!includes' =>  __('does not include'),
1898        );
1899    }
1900
1901    function getSearchMethodWidgets() {
1902        return array(
1903            'set' => null,
1904            'nset' => null,
1905            'includes' => array('ChoiceField', array(
1906                'choices' => $this->getChoices(),
1907                'configuration' => array('multiselect' => true),
1908            )),
1909            '!includes' => array('ChoiceField', array(
1910                'choices' => $this->getChoices(),
1911                'configuration' => array('multiselect' => true),
1912            )),
1913        );
1914    }
1915
1916    function getSearchQ($method, $value, $name=false) {
1917        $name = $name ?: $this->get('name');
1918        $val = $value;
1919        if ($value && is_array($value))
1920            $val = '"?'.implode('("|,|$)|"?', array_keys($value)).'("|,|$)';
1921        switch ($method) {
1922        case '!includes':
1923            return Q::not(array("{$name}__regex" => $val));
1924        case 'includes':
1925            return new Q(array("{$name}__regex" => $val));
1926        default:
1927            return parent::getSearchQ($method, $value, $name);
1928        }
1929    }
1930}
1931
1932class TypeaheadSelectionWidget extends ChoicesWidget {
1933    function render($options=array()) {
1934
1935        if ($options['mode'] == 'search')
1936            return parent::render($options);
1937
1938        $name = $this->getEnteredValue();
1939        $config = $this->field->getConfiguration();
1940        if (is_array($this->value)) {
1941            $name = $name ?: current($this->value);
1942            $value = key($this->value);
1943        }
1944        else {
1945            // Pull configured default (if configured)
1946            $def_key = $this->field->get('default');
1947            if (!$def_key && $config['default'])
1948                $def_key = $config['default'];
1949            if (is_array($def_key))
1950                $name = current($def_key);
1951        }
1952
1953        $source = array();
1954        foreach ($this->field->getList()->getItems() as $i)
1955            $source[] = array(
1956                'value' => $i->getValue(), 'id' => $i->getId(),
1957                'info' => sprintf('%s%s',
1958                    $i->getValue(),
1959                    (($extra= $i->getAbbrev()) ? " — $extra" : '')),
1960            );
1961        ?>
1962        <span style="display:inline-block">
1963        <input type="text" size="30" name="<?php echo $this->name; ?>_name"
1964            id="<?php echo $this->name; ?>" value="<?php echo Format::htmlchars($name); ?>"
1965            placeholder="<?php echo $config['prompt'];
1966            ?>" autocomplete="off" />
1967        <input type="hidden" name="<?php echo $this->name;
1968            ?>_id" id="<?php echo $this->name;
1969            ?>_id" value="<?php echo Format::htmlchars($value); ?>"/>
1970        <script type="text/javascript">
1971        $(function() {
1972            $('input#<?php echo $this->name; ?>').typeahead({
1973                source: <?php echo JsonDataEncoder::encode($source); ?>,
1974                property: 'info',
1975                onselect: function(item) {
1976                    $('input#<?php echo $this->name; ?>_name').val(item['value'])
1977                    $('input#<?php echo $this->name; ?>_id')
1978                      .attr('name', '<?php echo $this->name; ?>[' + item['id'] + ']')
1979                      .val(item['value']);
1980                    return false;
1981                }
1982            });
1983        });
1984        </script>
1985        </span>
1986        <?php
1987    }
1988
1989    function parsedValue() {
1990        return array($this->getValue() => $this->getEnteredValue());
1991    }
1992
1993    function getValue() {
1994        $data = $this->field->getSource();
1995        $name = $this->field->get('name');
1996        if (isset($data["{$this->name}_id"]) && is_numeric($data["{$this->name}_id"])) {
1997            return array($data["{$this->name}_id"] => $data["{$this->name}_name"]);
1998        }
1999        elseif (isset($data[$name])) {
2000            return $data[$name];
2001        }
2002        // Attempt to lookup typed value (usually from a default)
2003        elseif ($val = $this->getEnteredValue()) {
2004            return $this->field->lookupChoice($val);
2005        }
2006
2007        return parent::getValue();
2008    }
2009
2010    function getEnteredValue() {
2011        // Used to verify typeahead fields
2012        $data = $this->field->getSource();
2013        if (isset($data[$this->name.'_name'])) {
2014            // Drop the extra part, if any
2015            $v = $data[$this->name.'_name'];
2016            $pos = strrpos($v, ' — ');
2017            if ($pos !== false)
2018                $v = substr($v, 0, $pos);
2019
2020            return trim($v);
2021        }
2022        return parent::getValue();
2023    }
2024}
2025?>
2026