AbstractBuildForm.php 63.8 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
12
use qfq\Store;
use qfq\OnArray;
13
use qfq\UserFormException;
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/UserFormException.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
    protected $symbol = array();
Carsten  Rose's avatar
Carsten Rose committed
39
    protected $showDebugInfo = false;
40
    protected $inputCheckPattern = array();
Carsten  Rose's avatar
Carsten Rose committed
41

42
//    protected $feDivClass = array(); // Wrap FormElements in <div class="$feDivClass[type]">
43

44
45
    private $formId = null;

46
47
48
49
50
51
52
    /**
     * AbstractBuildForm constructor.
     *
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
     */
53
54
55
56
57
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) {
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
58
        $this->db = new Database();
Carsten  Rose's avatar
Carsten Rose committed
59
        $this->evaluate = new Evaluate($this->store, $this->db);
Carsten  Rose's avatar
Carsten Rose committed
60
        $this->showDebugInfo = ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes');
61

62
63
//        $sip = $this->store->getVar(CLIENT_SIP, STORE_CLIENT);

64
        // render mode specific
65
        $this->fillWrap();
66
67
68

        $this->buildElementFunctionName = [
            'checkbox' => 'Checkbox',
69
70
            'date' => 'DateTime',
            'datetime' => 'DateTime',
71
72
            'dateJQW' => 'DateJQW',
            'datetimeJQW' => 'DateJQW',
73
74
75
            'email' => 'Input',
            'gridJQW' => 'GridJQW',
            'hidden' => 'Hidden',
76
            'text' => 'Input',
77
            'time' => 'DateTime',
78
79
80
81
            'note' => 'Note',
            'password' => 'Input',
            'radio' => 'Radio',
            'select' => 'Select',
82
            'subrecord' => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
83
            'upload' => 'File',
84
85
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
86
87
        ];

88
89
        $this->buildRowName = [
            'checkbox' => 'Native',
90
91
            'date' => 'Native',
            'datetime' => 'Native',
92
93
94
95
96
            'dateJQW' => 'Native',
            'datetimeJQW' => 'Native',
            'email' => 'Native',
            'gridJQW' => 'Native',
            'hidden' => 'Native',
97
            'text' => 'Native',
98
            'time' => 'Native',
99
100
101
102
103
104
105
106
107
108
            'note' => 'Native',
            'password' => 'Native',
            'radio' => 'Native',
            'select' => 'Native',
            'subrecord' => 'Subrecord',
            'upload' => 'Native',
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
        ];

109
110
111
        $this->symbol[SYMBOL_EDIT] = "<span class='glyphicon glyphicon-pencil'></span>";
        $this->symbol[SYMBOL_NEW] = "<span class='glyphicon glyphicon-plus'></span>";
        $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon glyphicon-trash'></span>";
112

113
        $this->inputCheckPattern = Sanitize::inputCheckPatternArray();
114
115
    }

116
117
    abstract public function fillWrap();

118
    /**
119
     * Builds complete form. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
120
     *
Carsten  Rose's avatar
Carsten Rose committed
121
122
     * @param $mode
     * @return string|array   $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all formElement.dynamicUpdate-yes  values/states
123
124
     * @throws CodeException
     * @throws DbException
125
     * @throws \qfq\UserFormException
126
     */
127
    public function process($mode, $htmlElementNameIdZero = false) {
Carsten  Rose's avatar
Carsten Rose committed
128
129
130
131
132
        $htmlHead = '';
        $htmlTail = '';
        $htmlSubrecords = '';
        $htmlElements = '';
        $json = array();
133
134
135
136
137
138
139
140

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

        if ($mode === FORM_SAVE) {
            $modeCollectFe = FLAG_ALL;
            $storeUse = STORE_RECORD . STORE_TABLE_DEFAULT;
        }
141

142
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
143
144
145
146
147
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
            $htmlTail = $this->tail();
            $htmlSubrecords = $this->doSubrecords();
        }
148

149
        $filter = $this->getProcessFilter();
150

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

153
154
            $parentRecords = $this->db->sql($this->formSpec['multiSql']);
            foreach ($parentRecords as $row) {
155
                $this->store->setVarArray($row, STORE_PARENT_RECORD, true);
Carsten  Rose's avatar
Carsten Rose committed
156
                $jsonTmp = array();
157
                $htmlElements = $this->elements($row['_id'], $filter, 0, $jsonTmp, $modeCollectFe);
Carsten  Rose's avatar
Carsten Rose committed
158
                $json[] = $jsonTmp;
159
160
            }
        } else {
161
            $htmlElements = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse);
162
163
        }

164
165
        $htmlSip = $this->buildHiddenSip($json);

166
        // </form>
167

168
        return ($mode === FORM_LOAD) ? $htmlHead . $htmlElements . $htmlSip . $htmlTail . $htmlSubrecords : $json;
169
170
    }

171
    /**
172
     * Builds the head area of the form.
173
     *
174
     * @return string
175
     */
176
177
    public function head() {
        $html = '';
178

179
        $html .= '<div ' . Support::doAttribute('class', $this->formSpec['class'], TRUE) . '>'; // main <div class=...> around everything
180

181
182
183
        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
        $formEditUrl = $this->createFormEditUrl();
184

185
        $html .= "<p><a href='$formEditUrl'>Edit</a> <small>[$sipParamString]</small></p>";
186

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

189
190
191
        $html .= $this->getFormTag();

        return $html;
192
193
    }

194
    /**
195
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of the SIP.
196
     *
197
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:..., ...]</small>
198
     */
199
    public function createFormEditUrl() {
200

Carsten  Rose's avatar
Carsten Rose committed
201
        if (!$this->showDebugInfo) {
202
203
            return '';
        }
204

205
206
207
208
209
        $queryStringArray = [
            'id' => $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3),
            'form' => 'form',
            'r' => $this->formSpec['id']
        ];
210

211
        $queryString = Support::arrayToQueryString($queryStringArray);
212

213
214
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
215

216
        return $url;
217
218
219
    }

    /**
220
221
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
222
223
224
225
226
227
228
229
230
231
232
233
     * @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];
    }

    /**
234
     * Returns '<form ...>'-tag with various attributes.
235
236
237
238
239
240
241
242
243
244
245
246
247
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

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

    /**
     * Build an assoc array with standard form attributes.
     *
248
     * @return array
249
250
251
     */
    public function getFormTagAtrributes() {

252
        $attribute['id'] = $this->getFormId();
253
254
255
256
257
258
259
260
261
262
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

263
264
265
266
267
268
269
270
271
272
    /**
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
        return $this->formId;
    }

273
274
275
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
276
277
     * Notice: the SIP will be transferred as POST Parameter.
     *
278
279
280
281
282
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {

283
        return API_DIR . '/save.php';
284
285
286
287
288
289
290
291
292
293
294
295
    }

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

296
        $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"');
297
298
299
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
300

Carsten  Rose's avatar
Carsten Rose committed
301
302
303
304
    abstract public function tail();

    abstract public function doSubrecords();

305
    abstract public function getProcessFilter();
306
307

    /**
308
309
     * Process all FormElements: build corresponding HTML code. Collect and return all HTML code.
     *
310
     * @param $recordId
311
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
312
     * @param int $feIdContainer
313
     * @return string
314
315
     * @throws CodeException
     * @throws DbException
316
     * @throws \qfq\UserFormException
317
     */
318
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, &$json,
319
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false, $storeUse = STORE_USE_DEFAULT) {
320
321
322
        $html = '';

        // get current data record
323
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
324
325
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_EXPECT_1, array($recordId));
            $this->store->setVarArray($row, STORE_RECORD);
326
        }
327
328
329

        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
Carsten  Rose's avatar
Carsten Rose committed
330
331
332
            if (($filter === FORM_ELEMENTS_NATIVE && $fe['type'] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe['type'] !== 'subrecord')
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe['dynamicUpdate'] === 'no')
333
334
335
336
            ) {
                continue; // skip this FE
            }

337
338
            $debugStack = array();

339
340
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
341
342

            // evaluate current FormElement
343
            $evaluate = new Evaluate($this->store, $this->db);
344
            $formElement = $evaluate->parseArray($fe, $debugStack);
345

346
347
348
            // Some Defaults
            $formElement = Support::setFeDefaults($formElement);

349
            // Get default value
350
            $value = ($formElement['value'] === '') ? $this->store->getVar($formElement['name'], $storeUse,
351
                $formElement['checkType']) : $formElement['value'];
Carsten  Rose's avatar
Carsten Rose committed
352

353
354
355
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
            $htmlFormElementId = HelperFormElement::buildFormElementId($formElement['name'], ($htmlElementNameIdZero) ? 0 : $recordId);
356

Carsten  Rose's avatar
Carsten Rose committed
357
            // Construct Marshaller Name: buildElement
358
359
            $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement['type']];

Carsten  Rose's avatar
Carsten Rose committed
360
            $jsonElement = array();
361
            // Render pure element
Carsten  Rose's avatar
Carsten Rose committed
362
363
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $jsonElement);

364
365
//            $fake0 = $fe['dynamicUpdate'];
//            $fake1 = $formElement['dynamicUpdate'];
Carsten  Rose's avatar
Carsten Rose committed
366
367
368
369
370
371
372
373

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
            if ($formElement['class'] == 'container') {
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
                // for non container elements: just add the current json status
374
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe['dynamicUpdate'] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
375
376
377
                    $json[] = $jsonElement;
                }
            }
378
379

            // debugStack as Tooltip
Carsten  Rose's avatar
Carsten Rose committed
380
            if ($this->showDebugInfo && count($debugStack) > 0) {
381
382
//                $elementHtml = Support::appendTooltip($elementHtml, implode("\n", OnArray::htmlentitiesOnArray($debugStack)));
                $elementHtml = Support::appendTooltip($elementHtml, implode("\n", $debugStack));
383
384
            }

Carsten  Rose's avatar
Carsten Rose committed
385
            // Construct Marshaller Name: buildRow
386
387
388
            $buildRowName = 'buildRow' . $this->buildRowName[$formElement['type']];

            $html .= $this->$buildRowName($formElement, $elementHtml);
Carsten  Rose's avatar
Carsten Rose committed
389
//            break;
390
        }
391

392
393
394
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

395
396
397
        return $html;
    }

398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
     * @param $json
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
    public function buildHiddenSip(&$json) {
        $sipArray = $this->store->getStore(STORE_SIP);
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

        $queryString = Support::arrayToQueryString($sipArray);
        $sip = $this->store->getSipInstance();

        $sipValue = $sip->queryStringToSip($queryString, RETURN_SIP);

        $json[] = $this->getJsonElementUpdate(CLIENT_SIP, $sipValue, '');

        return $this->buildNativeHidden(CLIENT_SIP, $sipValue);
    }

    /**
     * @param $htmlFormElementId
     * @param string|array $value
     * @param string $mode disabled|readonly|''
     * @return array
     */
    private function getJsonElementUpdate($htmlFormElementId, $value, $mode) {
        $json = array();

        $json['form-element'] = $htmlFormElementId;
        $json['value'] = $value;
        $json['disabled'] = ($mode === 'disabled');
        $json['readonly'] = ($mode === 'readonly');

        return $json;
    }

    /**
     * Builds a real HTML hidden form element. Useful 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) . '">';
    }

Carsten  Rose's avatar
Carsten Rose committed
449
450
451
452
    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @throws CodeException
453
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
454
455
456
457
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
        $urlParam = $this->store->getStore(STORE_SIP);
        $urlParam[SIP_RECORD_ID] = 0;
458

Carsten  Rose's avatar
Carsten Rose committed
459
460
        unset($urlParam[SIP_SIP]);
        unset($urlParam[SIP_URLPARAM]);
461
462

        Support::appendTypo3ParameterToArray($urlParam);
Carsten  Rose's avatar
Carsten Rose committed
463
464
465
466
467

        $sip = $this->store->getSipInstance();

        $url = $sip->queryStringToSip(OnArray::toString($urlParam));

468
        if ($this->showDebugInfo) {
469
            //TODO: missing decoding of SIP
470
471
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }
Carsten  Rose's avatar
Carsten Rose committed
472
473
474
475

        return $url;
    }

476
477
478
479
480
481
482
483
    abstract public function buildRowNative($formElement, $elementHtml);

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

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

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

484
    /**
485
486
     * Builds a label, typically for an html-'<input>'-element.
     *
487
488
     * @param array $htmlFormElementId
     * @param $label
489
490
     * @return string
     */
491
492
493
494
    public function buildLabel($htmlFormElementId, $label) {
        $html = '<label for="' . $htmlFormElementId . '">' . $label . '</label>';

        return $html;
495
496
    }

497
498
499
500
501
502
503
504
505
506
507
    /**
     * 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
508
     * @throws UserFormException
509
     */
Carsten  Rose's avatar
Carsten Rose committed
510
    public function buildInput(array $formElement, $htmlFormElementId, $value, &$json) {
511
        $textarea = '';
512

513
        $attribute = Support::doAttribute('name', $htmlFormElementId);
514

515
        // Check for input type 'textarea'
516
        $colsRows = explode(',', $formElement['size'], 2);
517
        if (count($colsRows) === 2) {
518
            // <textarea>
519
520
            $htmlTag = '<textarea';

521
522
            $attribute .= Support::doAttribute('cols', $colsRows[0]);
            $attribute .= Support::doAttribute('rows', $colsRows[1]);
523
            $textarea = htmlentities($value) . '</textarea>';
524
525

        } else {
Carsten  Rose's avatar
Carsten Rose committed
526
527
528
529
            $htmlTag = '<input';

            $this->adjustMaxLength($formElement);

530
531
            // <input>
            if ($formElement['maxLength'] > 0) {
Carsten  Rose's avatar
Carsten Rose committed
532
533
534
                // crop string only if it's not empty (substr returns false on empty strings)
                if ($value !== '')
                    $value = substr($value, 0, $formElement['maxLength']);
535

Carsten  Rose's avatar
Carsten Rose committed
536
                // 'maxLength' needs an upper 'L': naming convention for DB tables!
537
                $attribute .= $this->getAttributeList($formElement, ['type', 'size', 'maxLength']);
538
                $attribute .= Support::doAttribute('value', htmlentities($value), false);
539
            }
Carsten  Rose's avatar
Carsten Rose committed
540
        }
541

542
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
543
544
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
545
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
546

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

Carsten  Rose's avatar
Carsten Rose committed
549
550
        $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']);

551
        return "$htmlTag $attribute>$textarea";
552

553
554
    }

Carsten  Rose's avatar
Carsten Rose committed
555
556
557
558
559
560
561
562
    /**
     * @param array $formElement
     */
    private function adjustMaxLength(array &$formElement) {

        // MIN( $formElement['maxLength'], tabledefinition)
        $maxLength = $this->getColumnSize($formElement['name']);

563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
        switch ($formElement['type']) {
            case 'date':
                $feMaxLength = 10;
                break;
            case 'datetime':
                $feMaxLength = 19;
                break;
            case 'time':
                $feMaxLength = 8;
                break;
            default:
                $feMaxLength = false;
                break;
        }

        // In case the underlying tablecolumn is not of type date/time, the $maxLength might be to high: correct
        if ($feMaxLength !== false && $maxLength !== false && $feMaxLength < $maxLength) {
            $maxLength = $feMaxLength;
        }

        // date/datetime
Carsten  Rose's avatar
Carsten Rose committed
584
585
586
587
588
589
590
591
592
593
594
        if ($maxLength !== false) {
            if (is_numeric($formElement['maxLength'])) {
                if ($formElement['maxLength'] > $maxLength) {
                    $formElement['maxLength'] = $maxLength;
                }
            } else {
                $formElement['maxLength'] = $maxLength;
            }
        }
    }

595
    /**
596
597
     * Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
     *
598
     * @param $column
599
     * @return bool|int  a) 'false' if there is no length definition, b) length definition, c) date|time|datetime|timestamp use hardcoded length
600
601
602
603
604
     */
    private function getColumnSize($column) {
        $matches = array();

        $typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);
605
606
607
608
609
610
611
612
613
614
615
        switch ($typeSpec) {
            case 'date': // yyyy-mm-dd
                return 10;
            case 'datetime': // yyyy-mm-dd hh:mm:ss
            case 'timestamp': // yyyy-mm-dd hh:mm:ss
                return 19;
            case 'time': // hh:mm:ss
                return 8;
            default:
                break;
        }
616

617
        // e.g.: string(64) >> 64, enum('yes','no') >> false
618
619
620
621
622
623
        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
624
625
626
627
    }

    /**
     * Builds a HTML attribute list, based on  $attributeList.
628
     *
629
630
631
632
633
634
635
     * 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
636
    private function getAttributeList(array $formElement, array $attributeList) {
637
638
639
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
640
                $attribute .= Support::doAttribute(strtolower($item), $formElement[$item]);
641
642
643
644
645
646
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
647
     *
648
     *   type     data                      result
649
     *   -------  -----------------------   -------------------------------------------------------------------------------
650
651
     *   min|max  <min value>|<max value>   min="$attrData[0]"|max="$attrData[1]"
     *   pattern  <regexp>                  pattern="$data"
Carsten  Rose's avatar
Carsten Rose committed
652
     *   digit    -                         pattern="^[0-9]*$"
653
     *   email    -                         pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
654
     *   alnumx   -
655
     *
656
     * For 'min/max' and 'pattern' the 'data' will be injected in the attribute string via '%s'.
657
658
659
660
     *
     * @param $type
     * @param $data
     * @return string
661
     * @throws \qfq\UserFormException
662
     */
Carsten  Rose's avatar
Carsten Rose committed
663
    private function getInputCheckPattern($type, $data) {
664
665
        $attribute = '';

666
667
        if ($type === '') {
            return '';
668
        }
669

670
671
672
673
674
        switch ($type) {
            case SANITIZE_ALLOW_MIN_MAX:
            case SANITIZE_ALLOW_MIN_MAX_DATE:
                $arrData = explode("|", $data);
                if (count($arrData) != 2 || $arrData[0] == '' || $arrData[1] == '')
675
                    throw new UserFormException("Missing MIN|MAX values", ERROR_MISSING_MIN_MAX);
676

677
678
679
                $attribute = 'min="' . $arrData[0] . '" ';
                $attribute .= 'max="' . $arrData[1] . '" ';
                break;
680

681
682
683
684
685
686
687
688
689
690
            case SANITIZE_ALLOW_PATTERN:
                $attribute = 'pattern="' . $data . '" ';
                break;

            case SANITIZE_ALLOW_ALL:
                break;

            default:
                $attribute = 'pattern="' . $this->inputCheckPattern[$type] . '" ';
                break;
691
        }
692

693
694
695
696
        return $attribute;
    }

    /**
697
698
     * Set corresponding html attributes readonly/required/disabled, based on $formElement['mode'].
     *
699
700
     * @param array $formElement
     * @return string
701
     * @throws UserFormException
702
     */
Carsten  Rose's avatar
Carsten Rose committed
703
    private function getAttributeMode(array $formElement) {
704
705
706
707
708
709
        $attribute = '';

        switch ($formElement['mode']) {
            case 'show':
                break;
            case 'readonly':
710
                $attribute .= Support::doAttribute('readonly', 'readonly');
711
712
                break;
            case 'required':
713
                $attribute .= Support::doAttribute('required', 'required');
714
715
716
717
                break;
            case 'lock':
                break;
            case 'disabled':
718
                $attribute .= Support::doAttribute('disabled', 'disabled');
719
720
                break;
            default:
721
722
                // Preparation for Log, Debug
                $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);
723
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'mode', STORE_SYSTEM);
724
                throw new UserFormException("Unknown mode '" . $formElement['mode'] . "'", ERROR_UNKNOWN_MODE);
725
726
727
728
729
730
731
732
                break;
        }
        return $attribute;
    }

    /**
     * Builds HTML 'checkbox' element.
     *
733
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transferred first.
734
735
736
737
738
739
740
741
742
     *
     * 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
743
     * @throws UserFormException
744
     */
Carsten  Rose's avatar
Carsten Rose committed
745
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value, &$json) {
746
747
748
749
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
750
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
751
752
753

        // Get fallback, if 'checkBoxMode' is not defined:
        if (!isset($formElement['checkBoxMode'])) {
754
            // This fallback is problematic if 'set' or 'enum' has 2 : defaults to single but maybe multi is meant.
755
756
757
758
759
760
            $formElement['checkBoxMode'] = (count($itemKey) > 2) ? 'multi' : 'single';
        }

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

Carsten  Rose's avatar
Carsten Rose committed
765
        $attributeBase = $this->getAttributeMode($formElement);
766
        $attributeBase .= Support::doAttribute('type', $formElement['type']);
767
768
769
770
771
772
773
774
775

        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:
776
                throw new UserFormException('checkBoxMode: \'' . $formElement['checkBoxMode'] . '\' is unknown.', ERROR_CHECKBOXMODE_UNKNOWN);
777
        }
778

Carsten  Rose's avatar
Carsten Rose committed
779
        $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']);
780
//        return Support::wrapTag('<div class="checkbox">', $html, true);
781
782
783
        return $html;
    }

784
    /**
785
786
787
788
789
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
790
     * Copies the found keys to &$itemKey and the values to &$itemValue
791
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
792
793
794
795
     *
     * @param array $formElement
     * @param $itemKey
     * @param $itemValue
796
     * @throws CodeException
797
     * @throws \qfq\UserFormException
798
     */
799
    public function getKeyValueListFromSqlEnumSpec(array $formElement, &$itemKey, &$itemValue) {
800
801
802
803
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();

804
805
806
        if (count($formElement) < 20)
            throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT);

807
        // Call getItemsForEnumOrSet() only if there a corresponding column really exist.
808
        if (false !== $this->store->getVar($formElement['name'], STORE_TABLE_COLUMN_TYPES)) {
809
810
            $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);
        }
811
812

        if (is_array($formElement['sql1'])) {
813
814
815
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
816

817
818
819
820
                // 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]);
                }
821

822
823
824
825
826
827
                $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]);
                }
828
            }
829
        } elseif (isset($formElement['itemList']) && strlen($formElement['itemList']) > 0) {
830
            $arr = KeyValueStringParser::parse($formElement['itemList'], ':', ',', KVP_IF_VALUE_EMPTY_COPY_KEY);
831
832
            $itemValue = array_values($arr);
            $itemKey = array_keys($arr);
833
        } elseif ($fieldType === 'enum' || $fieldType === 'set') {
Carsten  Rose's avatar
Carsten Rose committed
834
            // already done at the beginning with '$this->getItemsForEnumOrSet($formElement['name'], $fieldType);'
835
        } else {
836
            throw new UserFormException("Missing definition (- nothing found in 'sql1', 'parameter:itemValues', 'enum-' or 'set-definition'", ERROR_MISSING_ITEM_VALUES);
837
838
839
840
841
        }

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
842
843
844
845
846
847
848
849
850
851

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

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

Carsten  Rose's avatar
Carsten Rose committed
853
854
        if (isset($formElement['emptyHide'])) {
            if (isset($itemValue['']))
855
                unset($itemValue['']);
Carsten  Rose's avatar
Carsten Rose committed
856
            if (isset($itemKey['']))
857
858
859
                unset($itemKey['']);

        }
860
861
862
    }

    /**
863
864
     * Get the attribute definition list of an enum or set column. For strings, get the default value. Return elements as an array.
     *
865
866
867
     * @param $column
     * @param $fieldType
     * @return array
868
     * @throws UserFormException
869
870
871
872
873
874
875
     */
    private function getItemsForEnumOrSet($column, &$fieldType) {

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

        if ($fieldTypeDefinition === false) {
876
            throw new UserFormException("Column '$column' unknown in table '" . $this->formSpec['tableName'] . "'", ERROR_DB_UNKNOWN_COLUMN);
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
        }

        $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:
904
     *
905
906
907
908
909
     *    checked: first Element in $itemKey
     *  unchecked: ''
     *
     * @param array $itemKey
     * @param array $formElement
910
     * @throws UserFormException
911
912
913
     */
    private function prepareCheckboxCheckedUncheckedValue(array $itemKey, array &$formElement) {

914
        if (!isset($formElement[CHECKBOX_VALUE_CHECKED])) {
915
916
            if (isset($itemKey[0])) {
                // First element in $itemKey list
917
                $formElement[CHECKBOX_VALUE_CHECKED] = $itemKey[0];
918
919
            } else {
                // Take column default value
920
                $formElement[CHECKBOX_VALUE_CHECKED] = $this->store->getVar($formElement['name'], STORE_TABLE_DEFAULT);
921
922
923
924
            }
        }

        // unchecked
925
        if (!isset($formElement[CHECKBOX_VALUE_UNCHECKED])) {
926
            if (isset($itemKey[1])) {
927
                $formElement[CHECKBOX_VALUE_UNCHECKED] = ($itemKey[0] === $formElement['checked']) ? $itemKey[1] : $itemKey[0];
928
            } else {
929
                $formElement[CHECKBOX_VALUE_UNCHECKED] = '';
930
931
932
            }
        }

933
        if ($formElement[CHECKBOX_VALUE_CHECKED] === $formElement[CHECKBOX_VALUE_UNCHECKED]) {
934
            throw new UserFormException('FormElement: type=checkbox - checked and unchecked can\'t be the same: ' . $formElement[CHECKBOX_VALUE_CHECKED], ERROR_CHECKBOX_EQUAL);
935
936
937
938
939
        }

    }

    /**
940
941
     * Build a Checkbox based on two values.
     *
942
943
944
945
946
947
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attribute
     * @param $value
     * @return string
     */
948
    public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value) {
949
        $html = '';
950

951
952
        $attribute .= Support::doAttribute('name', $htmlFormElementId);
        $attribute .= Support::doAttribute('value', $formElement['checked'], false);
953
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
954
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
Carsten  Rose's avatar
Carsten Rose committed
955

956
        if ($formElement['checked'] === $value) {
957
            $attribute .= Support::doAttribute('checked', 'checked');
958
959
        }

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

962
        $html = $this->buildNativeHidden($htmlFormElementId, $formElement['unchecked']);
963
964
965
966
967
968

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

969
970
971
        $html = Support::wrapTag("<label>", $html, true);
        $html = Support::wrapTag("<div class='checkbox'>", $html, true);

972
973
974
        return $html;
    }

975
    /**
976
977
978
979
980
     * Build as many Checkboxes as items.
     *
     * Layout: The Bootstrap Layout needs very special setup, the checkboxes are wrapped differently with <div class=checkbox>
     *         depending of if they aligned horizontal or vertical.
     *
981
982
983
984
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attributeBase
     * @param $value
985
986
     * @param array $itemKey
     * @param array $itemValue
987
988
     * @return string
     */
989
    public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue) {
990
        // Defines which of the checkboxes will be checked.
991
        $values = explode(',', $value);
992

993
994
        $attributeBase .= Support::doAttribute('name', $htmlFormElementId);
        $attributeBase .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
995

996
        $html = $this->buildNativeHidden($htmlFormElementId, '');
997

Carsten  Rose's avatar
Carsten Rose committed
998
        $orientation = (isset($formElement[CHECKBOX_ORIENTATION]) && $formElement[CHECKBOX_ORIENTATION] === 'vertical') ? '' : 'checkbox-inline';
999

1000
        $flagFirst = true;