AbstractBuildForm.php 42.3 KB
Newer Older
1
<?php
Carsten  Rose's avatar
Carsten Rose committed
2
3
4
5
6
7
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/6/16
 * Time: 8:02 PM
 */
8
9
10
namespace qfq;

use qfq;
11
use qfq\Store;
12
use qfq\UserException;
13
use qfq\OnArray;
14
15
16
17

require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
18
require_once(__DIR__ . '/../qfq/exceptions/UserException.php');
19
require_once(__DIR__ . '/../qfq/Database.php');
20
require_once(__DIR__ . '/../qfq/helper/HelperFormElement.php');
21
require_once(__DIR__ . '/../qfq/helper/Support.php');
22
require_once(__DIR__ . '/../qfq/helper/OnArray.php');
23

24
25

/**
Carsten  Rose's avatar
Carsten Rose committed
26
27
 * Class AbstractBuildForm
 * @package qfq
28
 */
29
abstract class AbstractBuildForm {
30
31
32
    protected $formSpec = array();  // copy of the loaded form
    protected $feSpecAction = array(); // copy of all formElement.class='action' of the loaded form
    protected $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form
33
    protected $store = null;
Carsten  Rose's avatar
Carsten Rose committed
34
    protected $evaluate = null;
35
36
    protected $buildElementFunctionName = array();
    protected $pattern = array();
37
    protected $wrap = array();
38

39
40
    private $formId = null;

41
42
43
44
45
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) {
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
46
        $this->db = new Database();
Carsten  Rose's avatar
Carsten Rose committed
47
        $this->evaluate = new Evaluate($this->store, $this->db);
48

49
50
//        $sip = $this->store->getVar(CLIENT_SIP, STORE_CLIENT);

51
        // render mode specific
52
        $this->fillWrap();
53
54
55

        $this->buildElementFunctionName = [
            'checkbox' => 'Checkbox',
56
57
            'dateJQW' => 'DateJQW',
            'datetimeJQW' => 'DateJQW',
58
59
60
            'email' => 'Input',
            'gridJQW' => 'GridJQW',
            'hidden' => 'Hidden',
61
            'text' => 'Input',
62
63
64
65
            'note' => 'Note',
            'password' => 'Input',
            'radio' => 'Radio',
            'select' => 'Select',
66
            'subrecord' => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
67
            'upload' => 'File',
68
69
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
70
71
        ];

72
73
74
75
76
77
78
        $this->buildRowName = [
            'checkbox' => 'Native',
            'dateJQW' => 'Native',
            'datetimeJQW' => 'Native',
            'email' => 'Native',
            'gridJQW' => 'Native',
            'hidden' => 'Native',
79
            'text' => 'Native',
80
81
82
83
84
85
86
87
88
89
            'note' => 'Native',
            'password' => 'Native',
            'radio' => 'Native',
            'select' => 'Native',
            'subrecord' => 'Subrecord',
            'upload' => 'Native',
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
        ];

90
        $this->inputCheckPattern = OnArray::inputCheckPatternArray();
Carsten  Rose's avatar
Carsten Rose committed
91

92
93
    }

94
95
    abstract public function fillWrap();

96
    /**
97
98
     * Builds complete form. Depending of Formspecification, the layout will plain / table / bootstrap.
     *
99
     * @return string The whole form as HTML
100
101
     * @throws CodeException
     * @throws DbException
102
103
104
105
106
     */
    public function process() {

        $html = $this->head();

107
        $filter = $this->getProcessFilter();
108

109
        if ($this->formSpec['multiMode'] !== 'none') {
110

111
112
            $parentRecords = $this->db->sql($this->formSpec['multiSql']);
            foreach ($parentRecords as $row) {
113
                $this->store->setVarArray($row, STORE_PARENT_RECORD, true);
114
                $html .= $this->elements($row['_id'], $filter);
115
116
            }
        } else {
117
            $html .= $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), $filter);
118
119
        }

120
        // close the form
121
122
        $html .= $this->tail();

123

124
        $html .= $this->doSubrecords();
125

126
127
128
        return $html;
    }

129
130
131
132
133
    /**
     * Builds the head area of the form.
     *
     * @return string
     */
134
135
136
137
138
139
140
141
142
143
    public function head() {
        $html = '';

        $html .= $this->wrapItem(WRAP_SETUP_TITLE, $this->formSpec['title'], true);
        $html .= $this->getFormTag();

        return $html;
    }

    /**
144
145
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
146
147
148
149
150
151
152
153
154
155
156
157
     * @param $item
     * @param $value
     * @param bool|false $flagOmitEmpty
     * @return string
     */
    public function wrapItem($item, $value, $flagOmitEmpty = false) {
        if ($flagOmitEmpty && $value === "")
            return '';
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
158
     * Rreturns complete '<form ...>'-tag
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

        return '<form ' . OnArray::toString($attribute, '=', ' ', "'") . '>';
    }

    /**
     * Build an assoc array with standard form attributes.
     *
     * @return mixed
     */
    public function getFormTagAtrributes() {

Carsten  Rose's avatar
Carsten Rose committed
176
177
        //TODO: ttconetn id eintragen
//        $attribute['id'] = $this->store->getVar(STORE_TYPO3,'1234');
178
        $attribute['id'] = $this->getFormId();
179
180
181
182
183
184
185
186
187
188
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

189
190
191
192
193
194
195
196
197
198
    /**
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
        return $this->formId;
    }

199
200
201
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
202
203
     * Notice: the SIP will be transferred as POST Parameter.
     *
204
205
206
207
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {
208
209
        return 'typo3conf/ext/qfq/qfq/api/save.php';

210
        $queryStringArray = array();
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226

        Support::appendTypo3ParameterToArray($queryStringArray);

        return basename($_SERVER['SCRIPT_NAME']) . "?" . Support::arrayToQueryString($queryStringArray);
    }

    /**
     * Determines the enctype.
     *
     * See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes
     *
     * @return string
     * @throws DbException
     */
    public function getEncType() {

227
        $result = $this->db->sql("SELECT id FROM FormElement AS fe WHERE fe.formId=? AND fe.type='upload' LIMIT 1", ROW_REGULAR, [$this->formSpec['id']], 'Look for Formelement.type="upload"');
228
229
230
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
231

232
    abstract public function getProcessFilter();
233
234
235

    /**
     * @param $recordId
236
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
237
     * @param int $feIdContainer
238
     * @return string
239
240
     * @throws CodeException
     * @throws DbException
241
     * @throws \qfq\UserException
242
     */
243
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0) {
244
245
246
        $html = '';

        // get current data record
247
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
248
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_REGULAR, array($recordId));
249
250
            $this->store->setVarArray($row[0], STORE_RECORD);
        }
251
252
253

        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
254

255
256
            if (($filter === FORM_ELEMENTS_NATIVE && $fe['type'] === 'subrecord') ||
                ($filter === FORM_ELEMENTS_SUBRECORD && $fe['type'] !== 'subrecord')
257
258
259
260
            ) {
                continue; // skip this FE
            }

261
262
263
264
            // Log / Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, $fe['name'] . ' / ' . $fe['id'], STORE_SYSTEM);

            // evaluate current FormElement
265
266
267
268
269
            $evaluate = new Evaluate($this->store, $this->db);
            $formElement = $evaluate->parseArray($fe, $fe['debug'] === 'yes');
            if ($fe['debug'] === 'yes') {
                throw new UserException($evaluate->getDebug(), ERROR_DEBUG);
            }
270

271
            // Get default value
272
            $value = $formElement['value'] === '' ? $this->store->getVar($formElement['name']) : $value = $formElement['value'];
Carsten  Rose's avatar
Carsten Rose committed
273

274
            $htmlFormElementId = HelperFormElement::buildFormElementId($formElement['name'], $recordId);
275

276
277
278
279
280
281
282
283
284
285
            // Construct Marshaller Name
            $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement['type']];

            // Render pure element
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value);

            // Construct Marshaller Name
            $buildRowName = 'buildRow' . $this->buildRowName[$formElement['type']];

            $html .= $this->$buildRowName($formElement, $elementHtml);
286
        }
287

288
289
290
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

291
292
293
        return $html;
    }

294
295
296
    abstract public function tail();

    abstract public function doSubrecords();
297

298
299
300
301
302
303
304
305
    abstract public function buildRowNative($formElement, $elementHtml);

    abstract public function buildRowPill($formElement, $elementHtml);

    abstract public function buildRowFieldset($formElement, $elementHtml);

    abstract public function buildRowSubrecord($formElement, $elementHtml);

306
    /**
307
308
     * Extract Tag from $tag (might contain further attributes) and wrap it around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
     * @param $tag
     * @param $value
     * @param bool|false $flagOmitEmpty
     * @return string
     */
    public function wrapTag($tag, $value, $flagOmitEmpty = false) {
        if ($flagOmitEmpty && $value === "")
            return '';

        // <div class="container-fluid"> >> </div>
        $arr = explode(' ', $tag);
        $closing = '</' . substr($arr[0], 1) . '>';

        return $tag . $value . $closing;
    }

325
    /**
326
327
     * Builds a label, typically for an html-'<input>'-element.
     *
328
329
     * @param array $htmlFormElementId
     * @param $label
330
331
     * @return string
     */
332
333
334
335
    public function buildLabel($htmlFormElementId, $label) {
        $html = '<label for="' . $htmlFormElementId . '">' . $label . '</label>';

        return $html;
336
337
    }

338
339
340
341
342
343
344
345
346
347
348
349
350
    /**
     * Builds HTML 'input' element.
     * Format: <input name="$htmlFormElementId" <type="email|input|password|url" [autocomplete="autocomplete"] [autofocus="autofocus"]
     *           [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"]
     *           [pattern="$pattern"] [readonly="readonly"] [required="required"] [disabled="disabled"] value="$value">
     *
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
351
    public function buildInput(array $formElement, $htmlFormElementId, $value) {
352
        $textarea = '';
353

Carsten  Rose's avatar
Carsten Rose committed
354
        $attribute = $this->getAttribute('name', $htmlFormElementId);
355
356
        $htmlTag = '<input';

357
358
359
360
361
362
363
364
365
366
367
368
        // MIN( $formElement['maxLength'], tabledefinition)
        $maxLength = $this->getColumnSize($formElement['name']);
        if ($maxLength !== false) {
            if (is_numeric($formElement['maxLength'])) {
                if ($formElement['maxLength'] > $maxLength) {
                    $formElement['maxLength'] = $maxLength;
                }
            } else {
                $formElement['maxLength'] = $maxLength;
            }
        }

369
        // Check for input type 'textarea'
370
        $colsRows = explode(',', $formElement['size'], 2);
371
        if (count($colsRows) === 2) {
372
            // <textarea>
373
374
            $htmlTag = '<textarea';

Carsten  Rose's avatar
Carsten Rose committed
375
376
            $attribute .= $this->getAttribute('cols', $colsRows[0]);
            $attribute .= $this->getAttribute('rows', $colsRows[1]);
377
            $textarea = htmlentities($value) . '</textarea>';
378
379

        } else {
380
381
382
            // <input>
            if ($formElement['maxLength'] > 0) {
                $value = substr($value, 0, $formElement['maxLength']);
383
384
385

                $attribute .= $this->getAttributeList($formElement, ['type', 'size', 'maxLength']);
                $attribute .= $this->getAttribute('value', htmlentities($value), false);
386
            }
Carsten  Rose's avatar
Carsten Rose committed
387
        }
388

389
        // 'maxLength' needs an upper 'L': naming convention for DB tables!
390
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
391
        $attribute .= $this->getAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
392
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
393

Carsten  Rose's avatar
Carsten Rose committed
394
        $attribute .= $this->getAttributeMode($formElement);
395

396
        return "$htmlTag $attribute>$textarea";
397

398
399
400
    }

    /**
401
402
     * Format's an attribute: $type=$value. If $flagOmitEmpty==true && $value=='': return ''.
     *
403
404
405
406
407
     * @param $type
     * @param $value
     * @param bool|false $flagOmitEmpty
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
408
    private function getAttribute($type, $value, $flagOmitEmpty = true) {
409
410
        if ($flagOmitEmpty && $value === "")
            return '';
Carsten  Rose's avatar
Carsten Rose committed
411

412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
        return $type . '="' . trim($value) . '" ';
    }

    /**
     * @param $column
     * @return bool|int|string
     */
    private function getColumnSize($column) {
        $matches = array();

        $typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);

        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
430
431
432
433
    }

    /**
     * Builds a HTML attribute list, based on  $attributeList.
434
     *
435
436
437
438
439
440
441
     * E.g.: attributeList: [ 'type', 'autofocus' ]
     *       generates: 'type="$formElement['type']" autofocus="$formElement['autofocus']" '
     *
     * @param array $formElement
     * @param array $attributeList
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
442
    private function getAttributeList(array $formElement, array $attributeList) {
443
444
445
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
Carsten  Rose's avatar
Carsten Rose committed
446
                $attribute .= $this->getAttribute(strtolower($item), $formElement[$item]);
447
448
449
450
451
452
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
453
     *
454
455
456
457
458
459
460
461
462
463
464
465
     *   type     data                      predefined
     *   -------  -----------------------   -------------------------------------------------------------------------------
     *   min|max  <min value>|<max value>   min="%s"|max="%s"
     *   pattern  <regexp>                  pattern="%s"
     *   number   -                         pattern="^[0-9]*$"
     *   email    -                         pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
     *
     * For 'min|max' and 'pattern' the 'data' will be injected in the attribute string vai '%s'.
     *
     * @param $type
     * @param $data
     * @return string
466
     * @throws \qfq\UserException
467
     */
Carsten  Rose's avatar
Carsten Rose committed
468
    private function getInputCheckPattern($type, $data) {
469
470
        if ($type === '') {
            return '';
471
        }
472
473
474

        $attribute = '';

475
        $arrAttr = explode("|", $this->inputCheckPattern[$type]);
476
477
478
        $arrData = explode("|", $data);

        for ($ii = 0; $ii < count($arrAttr); $ii++) {
479
480
481
482
483
            if ($arrAttr[$ii]) {
                if (!isset($arrData[$ii]))
                    throw new UserException("Missing MIN|MAX values", ERROR_MISSING_MIN_MAX);
                $attribute .= str_replace('%s', trim($arrData[$ii]), $arrAttr[$ii]) . ' ';
            }
484
485
486
487
        }
        return $attribute;
    }

488

489
    /**
490
491
     * Set corresponding html attributes readonly/required/disabled, based on $formElement['mode'].
     *
492
493
494
495
     * @param array $formElement
     * @return string
     * @throws UserException
     */
Carsten  Rose's avatar
Carsten Rose committed
496
    private function getAttributeMode(array $formElement) {
497
498
499
500
501
502
        $attribute = '';

        switch ($formElement['mode']) {
            case 'show':
                break;
            case 'readonly':
Carsten  Rose's avatar
Carsten Rose committed
503
                $attribute .= $this->getAttribute('readonly', 'readonly');
504
505
                break;
            case 'required':
Carsten  Rose's avatar
Carsten Rose committed
506
                $attribute .= $this->getAttribute('required', 'required');
507
508
509
510
                break;
            case 'lock':
                break;
            case 'disabled':
Carsten  Rose's avatar
Carsten Rose committed
511
                $attribute .= $this->getAttribute('disabled', 'disabled');
512
513
514
515
                break;
            default:
                $this->store->setVar(SYSTEM_FORM_ELEMENT, $formElement['name'] . ' / ' . $formElement['id'], STORE_SYSTEM);
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'mode', STORE_SYSTEM);
516
                throw new UserException("Unknown mode '" . $formElement['mode'] . "'", ERROR_UNKNOWN_MODE);
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
                break;
        }
        return $attribute;
    }

    /**
     * Builds HTML 'checkbox' element.
     *
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transfered first.
     *
     * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
     *         <input name="$htmlFormElementId" type="checkbox" [autofocus="autofocus"]
     *            [readonly="readonly"] [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
537
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value) {
538
539
540
541
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
542
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
543
544
545

        // Get fallback, if 'checkBoxMode' is not defined:
        if (!isset($formElement['checkBoxMode'])) {
546
            // This fallback is problematic if 'set' or 'enum' has 2 : defaults to single but maybe multi is meant.
547
548
549
550
551
552
553
554
555
556
            $formElement['checkBoxMode'] = (count($itemKey) > 2) ? 'multi' : 'single';
        }

        if ($formElement['checkBoxMode'] === 'multi') {
            $htmlFormElementId .= '[]';
        } else {
            // Fill meaningfull defaults to parameter:checked|unchecked
            $this->prepareCheckboxCheckedUncheckedValue($itemKey, $formElement);
        }

Carsten  Rose's avatar
Carsten Rose committed
557
558
        $attributeBase = $this->getAttributeMode($formElement);
        $attributeBase .= $this->getAttribute('type', $formElement['type']);
559
560
561
562
563
564
565
566
567
568
569
570

        switch ($formElement['checkBoxMode']) {
            case 'single':
                $html = $this->buildCheckboxSingle($formElement, $htmlFormElementId, $attributeBase, $value);
                break;
            case 'multi';
                $html = $this->buildCheckboxMulti($formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue);
                break;
            default:
                throw new UserException('checkBoxMode: \'' . $formElement['checkBoxMode'] . '\' is unknown.', ERROR_CHECKBOXMODE_UNKNOWN);
        }

571
572
573
        return $html;
    }

574
    /**
575
576
577
578
579
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
580
     * Copies the found keys to &$itemKey and the values to &$itemValue
581
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
582
583
584
585
     *
     * @param array $formElement
     * @param $itemKey
     * @param $itemValue
586
587
     * @throws CodeException
     * @throws \qfq\UserException
588
     */
589
    public function getKeyValueListFromSqlEnumSpec(array $formElement, &$itemKey, &$itemValue) {
590
591
592
593
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();

594
595
596
        if (count($formElement) < 20)
            throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT);

597
598
599
        $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);

        if (is_array($formElement['sql1'])) {
600
601
602
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
603

604
605
606
607
                // If there is no column 'id' and at least two columns in total
                if (count($itemKey) === 0 && count($keys) >= 2) {
                    $itemKey = array_column($formElement['sql1'], $keys[0]);
                }
608

609
610
611
612
613
614
                $itemValue = array_column($formElement['sql1'], 'label');
                // If there is no column 'label' (e.g.: SHOW tables)
                if (count($itemValue) === 0) {
                    $idx = count($keys) == 1 ? 0 : 1;
                    $itemValue = array_column($formElement['sql1'], $keys[$idx]);
                }
615
            }
616
        } elseif (isset($formElement['itemList']) && strlen($formElement['itemList']) > 0) {
617
            $arr = KeyValueStringParser::parse($formElement['itemList'], ':', ',', KVP_IF_VALUE_EMPTY_COPY_KEY);
618
619
            $itemValue = array_values($arr);
            $itemKey = array_keys($arr);
620
        } elseif ($fieldType === 'enum' || $fieldType === 'set') {
Carsten  Rose's avatar
Carsten Rose committed
621
            // already done at the beginning with '$this->getItemsForEnumOrSet($formElement['name'], $fieldType);'
622
        } else {
623
            throw new UserException("Missing definition (- nothing found in 'sql1', 'parameter:itemValues', 'enum-' or 'set-definition'", ERROR_MISSING_ITEM_VALUES);
624
625
626
627
628
        }

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
629
630
631
632
633
634
635
636
637
638

        if (isset($formElement['emptyItemAtStart'])) {
            array_unshift($itemKey, '');
            array_unshift($itemValue, '');
        }

        if (isset($formElement['emptyItemAtEnd'])) {
            $itemValue[] = '';
            $itemKey[] = '';
        }
639
640
641
    }

    /**
642
643
     * Get the attribute definition list of an enum or set column. For strings, get the default value. Return elements as an array.
     *
644
645
646
647
648
649
650
651
652
653
654
     * @param $column
     * @param $fieldType
     * @return array
     * @throws UserException
     */
    private function getItemsForEnumOrSet($column, &$fieldType) {

        // Get column definition
        $fieldTypeDefinition = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);

        if ($fieldTypeDefinition === false) {
655
            throw new UserException("Column '$column' unknown in table '" . $this->formSpec['tableName'] . "'", ERROR_DB_UNKNOWN_COLUMN);
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
        }

        $length = strlen($fieldTypeDefinition);

        // enum('...   set('
        switch (substr($fieldTypeDefinition, 0, 4)) {
            case 'enum':
                $startPosition = 5;
                break;
            case 'set(':
                $startPosition = 4;
                break;
            default:
                $fieldType = 'string';
                return array();
        }

        // enum('a','b','c', ...)   >> [ 'a', 'b', 'c', ... ]
        // set('a','b','c', ...)   >> [ 'a', 'b', 'c', ... ]
        $items = OnArray::trimArray(explode(',', substr($fieldTypeDefinition, $startPosition, $length - $startPosition - 1)), "'");
        $fieldType = substr($fieldTypeDefinition, 0, $startPosition - 1);

        return $items;
    }

    /**
     * For CheckBox's with only one checkbox: if no parameter:checked|unchecked is defined, take defaults:
683
     *
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
     *    checked: first Element in $itemKey
     *  unchecked: ''
     *
     * @param array $itemKey
     * @param array $formElement
     * @throws UserException
     */
    private function prepareCheckboxCheckedUncheckedValue(array $itemKey, array &$formElement) {

        if (!isset($formElement['checked'])) {
            if (isset($itemKey[0])) {
                // First element in $itemKey list
                $formElement['checked'] = $itemKey[0];
            } else {
                // Take column default value
                $formElement['checked'] = $this->store->getVar($formElement['name'], STORE_TABLE_DEFAULT);
            }
        }

        // unchecked
        if (!isset($formElement['unchecked'])) {
            if (isset($itemKey[1])) {
                $formElement['unchecked'] = ($itemKey[0] === $formElement['checked']) ? $itemKey[1] : $itemKey[0];
            } else {
                $formElement['unchecked'] = '';
            }
        }

        if ($formElement['checked'] === $formElement['unchecked']) {
            throw new UserException('FormElement: type=checkbox - checked and unchecked can\'t be the same: ' . $formElement['checked'], ERROR_CHECKBOX_EQUAL);
        }

    }

    /**
719
720
     * Build a Checkbox based on two values.
     *
721
722
723
724
725
726
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attribute
     * @param $value
     * @return string
     */
727
    public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value) {
728
        $html = '';
729

Carsten  Rose's avatar
Carsten Rose committed
730
731
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
        $attribute .= $this->getAttribute('value', $formElement['checked'], false);
732
        if ($formElement['checked'] === $value) {
Carsten  Rose's avatar
Carsten Rose committed
733
            $attribute .= $this->getAttribute('checked', 'checked');
734
735
        }

Carsten  Rose's avatar
Carsten Rose committed
736
        $attribute .= $this->getAttributeList($formElement, ['autofocus']);
737

738
        $html = $this->buildNativeHidden($htmlFormElementId, $formElement['unchecked']);
739
740
741
742
743
744
745
746
747
748

        $html .= '<input ' . $attribute . '>';
        if (isset($formElement['label2'])) {
            $html .= $formElement['label2'];
        }

        return $html;
    }

    /**
749
750
751
752
753
754
755
756
757
758
759
760
     * Builds a real HTML hidden form element. Usefull for Checkboxes, Multiple-Select and Radios.
     *
     * @param $htmlFormElementId
     * @param $value
     * @return string
     */
    public function buildNativeHidden($htmlFormElementId, $value) {
        return '<input type="hidden" name="' . $htmlFormElementId . '" value="' . htmlentities($value) . '">';
    }

    /**
     * Build as many Checkboxes as items
761
762
763
764
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attributeBase
     * @param $value
765
766
     * @param array $itemKey
     * @param array $itemValue
767
768
     * @return string
     */
769
    public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue) {
770
771
772
        // Defines which of the checkboxes will be checked.
        $values = explode($value, ',');

Carsten  Rose's avatar
Carsten Rose committed
773
        $attributeBase .= $this->getAttribute('name', $htmlFormElementId);
774

775
        $html = $this->buildNativeHidden($htmlFormElementId, $value);
776
777
778
779
780
781
782
783
784
785
786

        $flagFirst = true;
        $ii = 0;
        $jj = 0;
        foreach ($itemKey as $item) {
            $ii++;
            $jj++;
            $attribute = $attributeBase;
            if ($flagFirst) {
                $flagFirst = false;
                if (isset($formElement['autofocus']))
Carsten  Rose's avatar
Carsten Rose committed
787
                    $attribute .= $this->getAttribute('autofocus', $formElement['autofocus']);
788
            }
Carsten  Rose's avatar
Carsten Rose committed
789
            $attribute .= $this->getAttribute('value', $item);
790
            if ($item === $values[$jj]) {
Carsten  Rose's avatar
Carsten Rose committed
791
                $attribute .= $this->getAttribute('checked', 'checked');
792
793
794
795
796
797
798
799
800
801
802
            }
            $html .= '<input ' . $attribute . '>';
            $html .= $itemValue[$ii];
            if ($ii === $formElement['maxLength']) {
                $ii = 0;
                $html .= '<br>';
            }
        }
        return $html;
    }

803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
    /**
     * Submit hidden values by SIP.
     *
     * Sometimes, it's usefull to precalculate values during formload and to submit them as hidden fields.
     * To avoid any manipulation on those fields, the values will be transferred by SIP.
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     */
    public function buildHidden(array $formElement, $htmlFormElementId, $value) {

        $this->store->setVar($htmlFormElementId, $value, STORE_SIP, false);
    }

819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
    /**
     * Build HTML 'radio' element.
     *
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transfered first.
     *
     * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
     *         <input name="$htmlFormElementId" type="radio" [autofocus="autofocus"]
     *            [readonly="readonly"] [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
834
    public function buildRadio(array $formElement, $htmlFormElementId, $value) {
835
836
837
838
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
839
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
840

Carsten  Rose's avatar
Carsten Rose committed
841
842
843
        $attributeBase = $this->getAttributeMode($formElement);
        $attributeBase .= $this->getAttribute('name', $htmlFormElementId);
        $attributeBase .= $this->getAttribute('type', $formElement['type']);
844
845
846
847

        $jj = 0;
        $flagFirst = true;

848
        $html = $this->buildNativeHidden($htmlFormElementId, $value);
849
850
851
852
853
854
        for ($ii = 0; $ii < count($itemValue); $ii++) {
            $jj++;
            $attribute = $attributeBase;
            if ($flagFirst) {
                $flagFirst = false;
                if (isset($formElement['autofocus']))
Carsten  Rose's avatar
Carsten Rose committed
855
                    $attribute .= $this->getAttribute('autofocus', $formElement['autofocus']);
856
857
            }

Carsten  Rose's avatar
Carsten Rose committed
858
            $attribute .= $this->getAttribute('value', $itemKey[$ii]);
859
            if ($itemKey[$ii] === $value) {
Carsten  Rose's avatar
Carsten Rose committed
860
                $attribute .= $this->getAttribute('checked', 'checked');
861
862
863
864
865
866
867
868
869
870
871
            }
            $html .= '<input ' . $attribute . '>';
            $html .= $itemValue[$ii];
            if ($jj === $formElement['maxLength']) {
                $jj = 0;
                $html .= '<br>';
            }
        }
        return $html;
    }

Carsten  Rose's avatar
Carsten Rose committed
872
    /**
873
874
     * Builds a Selct (Dropdown) Box.
     *
Carsten  Rose's avatar
Carsten Rose committed
875
876
877
878
879
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return mixed
     */
880
    public function buildSelect(array $formElement, $htmlFormElementId, $value) {
Carsten  Rose's avatar
Carsten Rose committed
881
882
883
884
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
885
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
Carsten  Rose's avatar
Carsten Rose committed
886
887
888

        $attribute = $this->getAttributeMode($formElement);
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
889
        $attribute .= $this->getAttributeList($formElement, ['autofocus']);
Carsten  Rose's avatar
Carsten Rose committed
890
891
892
893
894
895
896

        if (isset($formElement['size']) && $formElement['size'] > 1) {
            $attribute .= $this->getAttribute('size', $formElement['size']);
            $attribute .= $this->getAttribute('multiple', 'multiple');
        }

        $option = '';
897
        $selected = 'selected';
Carsten  Rose's avatar
Carsten Rose committed
898
899
900
901
902
        for ($ii = 0; $ii < count($itemValue); $ii++) {
            $option .= '<option ';

            $option .= $this->getAttribute('value', $itemKey[$ii]);
            if ($itemKey[$ii] === $value) {
903
904
                $option .= $selected;
                $selected = '';
Carsten  Rose's avatar
Carsten Rose committed
905
906
907
908
909
910
            }

            $option .= '>' . $itemValue[$ii] . '</option>';
        }

        return '<select ' . $attribute . '>' . $option . '</select>';
911
912
    }

Carsten  Rose's avatar
Carsten Rose committed
913
914
915
916
917
918
919
920
921
922
    /**
     * Constuct a HTML table of the subrecord data.
     * Column syntax definition: https://wikiit.math.uzh.ch/it/projekt/qfq/qfq-jqwidgets/Documentation#Type:_subrecord
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
923
    public function buildSubrecord(array $formElement, $htmlFormElementId, $value) {
924
925
        $html = '';

926
927
928
929
930
931
        $primaryRecord = $this->store->getStore(STORE_RECORD);

        if (!isset($primaryRecord['id'])) {
            return 'Please save main record fist.';
        }

Carsten  Rose's avatar
Carsten Rose committed
932
933
934
935
        if (!is_array($formElement['sql1'])) {
            throw new UserException('Missing \'sql1\' Query', ERROR_MISSING_SQL1);
        }

936
937
938
939
940
941
        // No records?
        if (count($formElement['sql1']) == 0) {
            return '';
        }

        if (!isset($formElement['sql1'][0]['id'])) {
942
            throw new UserException('Missing column \'id\' in  \'sql1\' Query', ERROR_DB_MISSING_COLUMN_ID);
943
944
        }

Carsten  Rose's avatar
Carsten Rose committed
945
946
        // construct column attributes
        $control = $this->getSubrecordColumnControl(array_keys($formElement['sql1'][0]));
947

948
//        $html .= '<b>' . $formElement['label'] . '</b>';
949
950
//        $html .= '<table border="1">';
        $html .= '<table class="table">';
951
        $html .= '<tr><th></th><th>' . implode('</th><th>', $control['title']) . '</th></tr>';
Carsten  Rose's avatar
Carsten Rose committed
952
        foreach ($formElement['sql1'] as $row) {
953

Carsten  Rose's avatar
Carsten Rose committed
954
            $html .= '<tr>';
955
            $html .= '<td>' . $this->editLink($formElement, $row['id'], $primaryRecord) . '</td>';
956

957
            foreach ($row as $columnName => $value) {
958
                $html .= '<td>' . $this->renderCell($control, $columnName, $value) . '</td>';
Carsten  Rose's avatar
Carsten Rose committed
959
960
961
962
963
            }
            $html .= '</tr>';
        }
        $html .= '</table>';

964
        return $html;
965
966
    }

Carsten  Rose's avatar
Carsten Rose committed
967
    /**
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
     * Get various column format information based on the 'raw' column title. The attributes are separated by '|' and specified as 'key' or 'key=value'.
     *
     * - Return all parsed values as an assoc array.
     * - For regular columns: If there is no 'width' specified, take the default 'SUBRECORD_COLUMN_WIDTH'
     * - For 'icon /  url / mailto': no width limit.
     *
     * Returned assoc array:
     *  title      Only key. Element is non numeric, which is not a keyword 'width/nostrip/icon/url/mailto'
     *  width      Key/Value Pair. Not provided for 'icon/url/mailto'.
     *  nostrip    Only key. Do not strip HTML Tags from the content.
     *  icon       Only key. Value will rendered (later) as an image.
     *  url        Only key. Value will rendered (later) as a 'href'
     *  mailto     Only key. Value will rendered (later) as a 'href mailto'
     *
     *
Carsten  Rose's avatar
Carsten Rose committed
983
984
985
986
     * @param $titleRaw
     * @return array
     * @throws UserException
     */
987
    private function getSubrecordColumnControl(array $titleRaw) {
Carsten  Rose's avatar
Carsten Rose committed
988
989
        $control = array();

990
991
992
        foreach ($titleRaw AS $columnName) {
            $flagWidthLimit = true;
            $control['width'][$columnName] = SUBRECORD_COLUMN_WIDTH;
Carsten  Rose's avatar
Carsten Rose committed
993
994

            // a) 'City@width=40', b) 'Status@icon', c) 'Mailto@width=80@nostrip'
995
            $arr = KeyValueStringParser::parse($columnName, '=', '|', KVP_IF_VALUE_EMPTY_COPY_KEY);
996
997
            foreach ($arr as $attribute => $value) {
                switch ($attribute) {
Carsten  Rose's avatar
Carsten Rose committed
998
999
                    case 'width':
                    case 'nostrip':
1000
                    case 'title':
1001
                        break;
Carsten  Rose's avatar
Carsten Rose committed
1002
                    case 'icon':
1003
1004
1005
                    case 'url':
                    case 'mailto':
                        $flagWidthLimit = false;
Carsten  Rose's avatar
Carsten Rose committed
1006
1007
                        break;
                    default:
1008
                        $attribute = is_numeric($value) ? 'width' : 'title';
Carsten  Rose's avatar
Carsten Rose committed
1009
1010
                        break;
                }
1011
                $control[$attribute][$columnName] = $value;
Carsten  Rose's avatar
Carsten Rose committed
1012
            }
1013

1014
1015
1016
            if (!isset($control['title'][$columnName]))
                $control['title'][$columnName] = ''; // Fallback:  Might be wrong, but better than nothing.

1017
            // Limit title length
1018
1019
1020
1021
1022
1023
            $control['title'][$columnName] = substr($control['title'][$columnName], 0, $control['width'][$columnName]);

            if (!$flagWidthLimit) {
                $control['width'][$columnName] = false;
            }

Carsten  Rose's avatar
Carsten Rose committed
1024
1025
        }
        return $control;
1026
1027
    }

1028
    /**
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
     * Renders an Link with a symbol (edit) and register a new SIP to grant permission to the link..
     *
     * Returns <a href="<Link>"><span ...></span></a>
     *
     * Link: <page>?s=<SIP>&<standard typo3 params>
     * SIP: form = $formElement['form'] (provided via formElement['parameter'])
     *      r = $targetRecordId
     *      Parse  $formElement['detail'] with possible key/value pairs. E.g.: detail=id:gr_id,#{{a}}:p_id,#12:x_id
     *        gr_id = <<primarytable.id>>
     *        p_id = <<variable defined in SIP or Client>>
     *        x_id = 12 (constant)
     *
     *
1042
1043
1044
1045
1046
1047
     * @param $formElement
     * @param $targetRecordId
     * @param $record
     * @return string
     * @throws UserException
     */
1048
    private function editLink($formElement, $targetRecordId, $record) {
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059

        $queryStringArray = [
            'form' => $formElement['form'],
            'r' => $targetRecordId,

        ];

        // Add custom query parameter
        if (isset($formElement['detail'])) {
            $detailParam = KeyValueStringParser::parse($formElement['detail']);
            foreach ($detailParam as $src => $dest) {
1060
                // Constants
1061
1062
1063
1064
                if ($src[0] == '#') {
                    $queryStringArray[$dest] = substr($src, 1);
                    continue;
                }
1065
                // Form record values or parameter
1066
1067
1068
1069
1070
1071
1072
                if (isset($record[$src])) {
                    $queryStringArray[$dest] = $record[$src];
                }
            }
        }

        Support::appendTypo3ParameterToArray($queryStringArray);
1073
1074
1075
1076
        // If there is a specific targetpage defined, take it.
        if (isset($formElement['page']) && $formElement['page'] !== '') {
            $queryStringArray['id'] = $formElement['page'];
        }
1077
1078
1079
1080

        //-----------------
        $queryString = Support::arrayToQueryString($queryStringArray);

1081
        $sip = $this->store->getSipInstance();
1082
        $url = $sip->queryStringToSip($queryString);
1083
1084
1085
1086
1087

        return "<a href='$url'><span class='glyphicon glyphicon-pencil'></span></a>";

    }

1088
    /**
1089
1090
1091
1092
1093
1094
1095
1096
1097
     * Renders $value as specified in array $control
     *
     * nostrip: by default, HTML tags are removed. With this attribute, the value will be delivered as it is.
     * width: if there is a size limit - apply it.
     * icon: The cell will be rendered as an image. $value should contain the name of an image in 'fileadmin/icons/'
     * mailto: The cell will be rendered as an <a> tag with the 'mailto' attribute.
     * url:  The cell will be rendered as an <a> tag. The value will be exploded by '|'. $value[0] = href, value[1] = text.
     *  E.g. $value = 'www.math.uzh.ch/?id=45&v=234|Show details for Modul 123' >> <a href="www.math.uzh.ch/?id=45&v=234">Show details for Modul 123</a>
     *
1098
1099
1100
1101
1102
     * @param array $control
     * @param $columnName
     * @param $value
     * @return string
     */
1103
1104
1105
1106
1107
    private function renderCell(array $control, $columnName, $value) {

        $arr = explode('|', $value);
        if (count($arr) == 1)
            $arr[1] = $arr[0];
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118

        $cell = isset($control['nostrip'][$columnName]) ? $value : strip_tags($value);

        if ($control['width'][$columnName] !== false)
            $cell = substr($cell, 0, $control['width'][$columnName]);

        if (isset($control['icon'][$columnName])) {
            $cell = ($cell === '') ? '' : "<image src='fileadmin/icons/$cell'>";
        }

        if (isset($control['mailto'][$columnName])) {
1119
            $cell = "<a href='mailto:$arr[0]'>$arr[1]</a>";
1120
1121
1122
        }

        if (isset($control['url'][$columnName])) {
1123
            $cell = "<a href='$arr[0]'>$arr[1]</a>";
1124
1125
1126
1127
1128
        }

        return $cell;
    }

Carsten  Rose's avatar
Carsten Rose committed
1129
    /**
1130
1131
     * Builts an Upload (File) Button.
     *
Carsten  Rose's avatar
Carsten Rose committed
1132
1133
1134
1135
1136
1137
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
     * @throws UserException
     */
1138
    public function buildFile(array $formElement, $htmlFormElementId, $value) {
Carsten  Rose's avatar
Carsten Rose committed
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153

        $attribute = $this->getAttributeMode($formElement);
        $attribute .= $this->getAttribute('type', 'file');
        $attribute .= $this->getAttribute('name', $htmlFormElementId);
        $attribute .= $this->getAttributeList($formElement, ['autofocus', 'accept']);

        return '<input ' . $attribute . '>';
    }

    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @throws UserException
     */
1154
    public function buildDateJQW(array $formElement, $htmlFormElementId, $value) {
1155
        // TODO: implement
1156
        throw new UserException("Not implemented yet: buildDateJQW()", ERROR_NOT_IMPLEMENTED);
1157
1158
    }

Carsten  Rose's avatar
Carsten Rose committed
1159
1160
1161
1162
1163
1164
    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @throws UserException
     */
1165
    public function buildGridJQW(array $formElement, $htmlFormElementId, $value) {
1166
        // TODO: implement
1167
        throw new UserException("Not implemented yet: buildGridJQW()", ERROR_NOT_IMPLEMENTED);
1168
    }
Carsten  Rose's avatar
Carsten Rose committed
1169
1170
1171
1172
1173
1174
1175

    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return mixed
     */
1176
    public function buildNote(array $formElement, $htmlFormElementId, $value) {
Carsten  Rose's avatar
Carsten Rose committed
1177
1178
1179
        return $value;
    }

1180
1181
1182
1183
1184
1185
1186
    /**
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return mixed
     */
    public function buildPill(array $formElement, $htmlFormElementId, $value) {
1187
        return $value;