AbstractBuildForm.php 64.5 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
            $buildRowName = 'buildRow' . $this->buildRowName[$formElement['type']];

388
            $html .= $this->$buildRowName($formElement, $elementHtml, $htmlFormElementId);
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
    abstract public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementId);
477

478
    abstract public function buildRowPill(array $formElement, $elementHtml);
479

480
    abstract public function buildRowFieldset(array $formElement, $elementHtml);
481

482
    abstract public function buildRowSubrecord(array $formElement, $elementHtml);
483

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

        $html = Support::wrapTag("<label $attributes>", $label);
496
497

        return $html;
498
499
    }

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

516
        $attribute = Support::doAttribute('name', $htmlFormElementId);
517
        $attribute .= Support::doAttribute('class', 'form-control');
518

519
        // Check for input type 'textarea'
520
        $colsRows = explode(',', $formElement['size'], 2);
521
        if (count($colsRows) === 2) {
522
            // <textarea>
523
524
            $htmlTag = '<textarea';

525
526
            $attribute .= Support::doAttribute('cols', $colsRows[0]);
            $attribute .= Support::doAttribute('rows', $colsRows[1]);
527
            $textarea = htmlentities($value) . '</textarea>';
528
529

        } else {
Carsten  Rose's avatar
Carsten Rose committed
530
531
532
533
            $htmlTag = '<input';

            $this->adjustMaxLength($formElement);

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

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

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

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

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

555
        return "$htmlTag $attribute>$textarea" . $this->getHelpBlock();
556

557
558
    }

Carsten  Rose's avatar
Carsten Rose committed
559
560
561
562
563
564
565
566
    /**
     * @param array $formElement
     */
    private function adjustMaxLength(array &$formElement) {

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

567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
        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
588
589
590
591
592
593
594
595
596
597
598
        if ($maxLength !== false) {
            if (is_numeric($formElement['maxLength'])) {
                if ($formElement['maxLength'] > $maxLength) {
                    $formElement['maxLength'] = $maxLength;
                }
            } else {
                $formElement['maxLength'] = $maxLength;
            }
        }
    }

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

        $typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);
609
610
611
612
613
614
615
616
617
618
619
        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;
        }
620

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

        return false;
628
629
630
631
    }

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

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

670
671
        if ($type === '') {
            return '';
672
        }
673

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

681
682
683
                $attribute = 'min="' . $arrData[0] . '" ';
                $attribute .= 'max="' . $arrData[1] . '" ';
                break;
684

685
686
687
688
689
690
691
692
693
694
            case SANITIZE_ALLOW_PATTERN:
                $attribute = 'pattern="' . $data . '" ';
                break;

            case SANITIZE_ALLOW_ALL:
                break;

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

697
698
699
700
        return $attribute;
    }

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

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

734
735
736
737
738
739
740
    /**
     * @return string
     */
    private function getHelpBlock() {
        return '<div class="help-block with-errors"></div>';
    }

741
742
743
    /**
     * Builds HTML 'checkbox' element.
     *
744
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transferred first.
745
746
747
748
749
750
751
752
753
     *
     * 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
754
     * @throws UserFormException
755
     */
Carsten  Rose's avatar
Carsten Rose committed
756
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value, &$json) {
757
758
759
760
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
761
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
762
763
764

        // Get fallback, if 'checkBoxMode' is not defined:
        if (!isset($formElement['checkBoxMode'])) {
765
            // This fallback is problematic if 'set' or 'enum' has 2 : defaults to single but maybe multi is meant.
766
767
768
769
770
771
            $formElement['checkBoxMode'] = (count($itemKey) > 2) ? 'multi' : 'single';
        }

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

Carsten  Rose's avatar
Carsten Rose committed
776
        $attributeBase = $this->getAttributeMode($formElement);
777
        $attributeBase .= Support::doAttribute('type', $formElement['type']);
778
779
780
781
782
783
784
785
786

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

Carsten  Rose's avatar
Carsten Rose committed
790
        $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']);
791
//        return Support::wrapTag('<div class="checkbox">', $html, true);
792
793
794
        return $html;
    }

795
    /**
796
797
798
799
800
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
801
     * Copies the found keys to &$itemKey and the values to &$itemValue
802
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
803
804
805
806
     *
     * @param array $formElement
     * @param $itemKey
     * @param $itemValue
807
     * @throws CodeException
808
     * @throws \qfq\UserFormException
809
     */
810
    public function getKeyValueListFromSqlEnumSpec(array $formElement, &$itemKey, &$itemValue) {
811
812
813
814
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();

815
816
817
        if (count($formElement) < 20)
            throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT);

818
        // Call getItemsForEnumOrSet() only if there a corresponding column really exist.
819
        if (false !== $this->store->getVar($formElement['name'], STORE_TABLE_COLUMN_TYPES)) {
820
821
            $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);
        }
822
823

        if (is_array($formElement['sql1'])) {
824
825
826
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
827

828
829
830
831
                // 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]);
                }
832

833
834
835
836
837
838
                $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]);
                }
839
            }
840
        } elseif (isset($formElement['itemList']) && strlen($formElement['itemList']) > 0) {
841
            $arr = KeyValueStringParser::parse($formElement['itemList'], ':', ',', KVP_IF_VALUE_EMPTY_COPY_KEY);
842
843
            $itemValue = array_values($arr);
            $itemKey = array_keys($arr);
844
        } elseif ($fieldType === 'enum' || $fieldType === 'set') {
Carsten  Rose's avatar
Carsten Rose committed
845
            // already done at the beginning with '$this->getItemsForEnumOrSet($formElement['name'], $fieldType);'
846
        } else {
847
            throw new UserFormException("Missing definition (- nothing found in 'sql1', 'parameter:itemValues', 'enum-' or 'set-definition'", ERROR_MISSING_ITEM_VALUES);
848
849
850
851
852
        }

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
853
854
855
856
857
858
859
860
861
862

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

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

Carsten  Rose's avatar
Carsten Rose committed
864
865
        if (isset($formElement['emptyHide'])) {
            if (isset($itemValue['']))
866
                unset($itemValue['']);
Carsten  Rose's avatar
Carsten Rose committed
867
            if (isset($itemKey['']))
868
869
870
                unset($itemKey['']);

        }
871
872
873
    }

    /**
874
875
     * Get the attribute definition list of an enum or set column. For strings, get the default value. Return elements as an array.
     *
876
877
878
     * @param $column
     * @param $fieldType
     * @return array
879
     * @throws UserFormException
880
881
882
883
884
885
886
     */
    private function getItemsForEnumOrSet($column, &$fieldType) {

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

        if ($fieldTypeDefinition === false) {
887
            throw new UserFormException("Column '$column' unknown in table '" . $this->formSpec['tableName'] . "'", ERROR_DB_UNKNOWN_COLUMN);
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
        }

        $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:
915
     *
916
917
918
919
920
     *    checked: first Element in $itemKey
     *  unchecked: ''
     *
     * @param array $itemKey
     * @param array $formElement
921
     * @throws UserFormException
922
923
924
     */
    private function prepareCheckboxCheckedUncheckedValue(array $itemKey, array &$formElement) {

925
        if (!isset($formElement[CHECKBOX_VALUE_CHECKED])) {
926
927
            if (isset($itemKey[0])) {
                // First element in $itemKey list
928
                $formElement[CHECKBOX_VALUE_CHECKED] = $itemKey[0];
929
930
            } else {
                // Take column default value
931
                $formElement[CHECKBOX_VALUE_CHECKED] = $this->store->getVar($formElement['name'], STORE_TABLE_DEFAULT);
932
933
934
935
            }
        }

        // unchecked
936
        if (!isset($formElement[CHECKBOX_VALUE_UNCHECKED])) {
937
            if (isset($itemKey[1])) {
938
                $formElement[CHECKBOX_VALUE_UNCHECKED] = ($itemKey[0] === $formElement['checked']) ? $itemKey[1] : $itemKey[0];
939
            } else {
940
                $formElement[CHECKBOX_VALUE_UNCHECKED] = '';
941
942
943
            }
        }

944
        if ($formElement[CHECKBOX_VALUE_CHECKED] === $formElement[CHECKBOX_VALUE_UNCHECKED]) {
945
            throw new UserFormException('FormElement: type=checkbox - checked and unchecked can\'t be the same: ' . $formElement[CHECKBOX_VALUE_CHECKED], ERROR_CHECKBOX_EQUAL);
946
947
948
949
950
        }

    }

    /**
951
952
     * Build a Checkbox based on two values.
     *
953
954
955
956
957
958
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attribute
     * @param $value
     * @return string
     */
959
    public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value) {
960
        $html = '';
961

962
963
        $attribute .= Support::doAttribute('name', $htmlFormElementId);
        $attribute .= Support::doAttribute('value', $formElement['checked'], false);
964
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
965
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
Carsten  Rose's avatar
Carsten Rose committed
966

967
        if ($formElement['checked'] === $value) {
968
            $attribute .= Support::doAttribute('checked', 'checked');
969
970
        }

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

973
        $html = $this->buildNativeHidden($htmlFormElementId, $formElement['unchecked']);
974
975
976
977
978
979

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

980
981
982
        $html = Support::wrapTag("<label>", $html, true);
        $html = Support::wrapTag("<div class='checkbox'>", $html, true);

983
984
985
        return $html;
    }

986
    /**
987
988
989
990
991
     * 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.
     *
992
993
994
995
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attributeBase
     * @param $value
996
997
     * @param array $itemKey
     * @param array $itemValue