AbstractBuildForm.php 65.6 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
 */
Carsten  Rose's avatar
Carsten Rose committed
8

9
10
11
namespace qfq;

use qfq;
12
13
use qfq\Store;
use qfq\OnArray;
14
use qfq\UserFormException;
15
16
17
18

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

25
26

/**
Carsten  Rose's avatar
Carsten Rose committed
27
28
 * Class AbstractBuildForm
 * @package qfq
29
 */
30
abstract class AbstractBuildForm {
31
32
33
    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
34
    protected $store = null;
Carsten  Rose's avatar
Carsten Rose committed
35
    protected $evaluate = null;
36
37
    protected $buildElementFunctionName = array();
    protected $pattern = array();
38
    protected $wrap = array();
39
    protected $symbol = array();
Carsten  Rose's avatar
Carsten Rose committed
40
    protected $showDebugInfo = false;
41
    protected $inputCheckPattern = array();
Carsten  Rose's avatar
Carsten Rose committed
42

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

45
46
    private $formId = null;

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

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

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

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

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

110
111
112
        $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>";
113

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

117
118
    abstract public function fillWrap();

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

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

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

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

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

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

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

167
        // </form>
168

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

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

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

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

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

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

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

        return $html;
193
194
    }

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

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

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

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

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

217
        return $url;
218
219
220
    }

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

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

        $attribute = $this->getFormTagAtrributes();

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

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

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

        return $attribute;
    }

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

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

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

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

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

    }
301

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

    abstract public function doSubrecords();

306
    abstract public function getProcessFilter();
307
308

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

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

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

338
339
            $debugStack = array();

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

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

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

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

354
355
356
            // 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);
357

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

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

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

            // 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
375
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe['dynamicUpdate'] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
376
377
378
                    $json[] = $jsonElement;
                }
            }
379
380

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

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

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

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

396
397
398
        return $html;
    }

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

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

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

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

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

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

        return $url;
    }

477
    abstract public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementId);
478

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

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

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

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

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

        return $html;
499
500
    }

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

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

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

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

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

            $this->adjustMaxLength($formElement);

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

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

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

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

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

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

558
559
    }

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

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

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

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

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

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

        return false;
629
630
631
632
    }

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

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

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

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

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

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

            case SANITIZE_ALLOW_ALL:
                break;

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

698
699
700
701
        return $attribute;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        }
872
873
874
    }

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

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

        if ($fieldTypeDefinition === false) {
888
            throw new UserFormException("Column '$column' unknown in table '" . $this->formSpec['tableName'] . "'", ERROR_DB_UNKNOWN_COLUMN);
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
915
        }

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

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

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

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

    }

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

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

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

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

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

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

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

984
985
986
        return $html;
    }

987
    /**
988
989
990
991
992
     * 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.
     *