AbstractBuildForm.php 77 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
require_once(__DIR__ . '/../qfq/report/Link.php');
25

26
27

/**
Carsten  Rose's avatar
Carsten Rose committed
28
29
 * Class AbstractBuildForm
 * @package qfq
30
 */
31
abstract class AbstractBuildForm {
32
33
34
35
36
    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
    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
46
47
48
49
50
51
    /**
     * @var Store
     */
    protected $store = null;
    /**
     * @var Evaluate
     */
    protected $evaluate = null;
52
53
54
    /**
     * @var string
     */
55
    private $formId = null;
56
57
58
59
60
    /**
     * @var Sip
     */
    private $sip = null;

61
62
63
64
65
66
67
    /**
     * AbstractBuildForm constructor.
     *
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
     */
68
69
70
71
72
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) {
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
73
        $this->db = new Database();
Carsten  Rose's avatar
Carsten Rose committed
74
        $this->evaluate = new Evaluate($this->store, $this->db);
Carsten  Rose's avatar
Carsten Rose committed
75
        $this->showDebugInfo = ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes');
76

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

79
        // render mode specific
80
        $this->fillWrap();
81
82
83

        $this->buildElementFunctionName = [
            'checkbox' => 'Checkbox',
84
85
            'date' => 'DateTime',
            'datetime' => 'DateTime',
86
87
            'dateJQW' => 'DateJQW',
            'datetimeJQW' => 'DateJQW',
88
89
            'email' => 'Input',
            'gridJQW' => 'GridJQW',
90
            FE_TYPE_EXTRA => 'Extra',
91
            'text' => 'Input',
92
            'time' => 'DateTime',
93
94
95
96
            'note' => 'Note',
            'password' => 'Input',
            'radio' => 'Radio',
            'select' => 'Select',
97
            'subrecord' => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
98
            'upload' => 'File',
99
100
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
101
102
        ];

103
104
        $this->buildRowName = [
            'checkbox' => 'Native',
105
106
            'date' => 'Native',
            'datetime' => 'Native',
107
108
109
110
            'dateJQW' => 'Native',
            'datetimeJQW' => 'Native',
            'email' => 'Native',
            'gridJQW' => 'Native',
111
            FE_TYPE_EXTRA => 'Native',
112
            'text' => 'Native',
113
            'time' => 'Native',
114
115
116
117
118
119
120
121
122
123
            'note' => 'Native',
            'password' => 'Native',
            'radio' => 'Native',
            'select' => 'Native',
            'subrecord' => 'Subrecord',
            'upload' => 'Native',
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
        ];

124
125
126
        $this->symbol[SYMBOL_EDIT] = "<span class='glyphicon " . GLYPH_ICON_EDIT . "'></span>";
        $this->symbol[SYMBOL_NEW] = "<span class='glyphicon " . GLYPH_ICON_NEW . "'></span>";
        $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon " . GLYPH_ICON_DELETE . "'></span>";
127

128
        $this->inputCheckPattern = Sanitize::inputCheckPatternArray();
129
130
    }

131
132
    abstract public function fillWrap();

133
    /**
134
     * Builds complete form. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
135
     *
136
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
Carsten  Rose's avatar
Carsten Rose committed
137
     * @return string|array   $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all formElement.dynamicUpdate-yes  values/states
138
139
     * @throws CodeException
     * @throws DbException
140
     * @throws \qfq\UserFormException
141
     */
142
    public function process($mode, $htmlElementNameIdZero = false) {
Carsten  Rose's avatar
Carsten Rose committed
143
144
145
146
147
        $htmlHead = '';
        $htmlTail = '';
        $htmlSubrecords = '';
        $htmlElements = '';
        $json = array();
148
149
150
151
152
153
154
155

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

157
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
158
159
160
161
162
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
            $htmlTail = $this->tail();
            $htmlSubrecords = $this->doSubrecords();
        }
163

164
        $filter = $this->getProcessFilter();
165

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

168
169
            $parentRecords = $this->db->sql($this->formSpec['multiSql']);
            foreach ($parentRecords as $row) {
170
                $this->store->setVarArray($row, STORE_PARENT_RECORD, true);
Carsten  Rose's avatar
Carsten Rose committed
171
                $jsonTmp = array();
172
                $htmlElements = $this->elements($row['_id'], $filter, 0, $jsonTmp, $modeCollectFe);
Carsten  Rose's avatar
Carsten Rose committed
173
                $json[] = $jsonTmp;
174
175
            }
        } else {
176
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
177
            $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode);
178
        }
179
180
181
        $htmlSip = $this->buildHiddenSip($json);

        return ($mode === FORM_LOAD) ? $htmlHead . $htmlElements . $htmlSip . $htmlTail . $htmlSubrecords : $json;
182
183
    }

184
    /**
185
     * Builds the head area of the form.
186
     *
187
     * @return string
188
     */
189
190
    public function head() {
        $html = '';
191

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

194
195
196
        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
        $formEditUrl = $this->createFormEditUrl();
197

198
        $html .= "<p><a " . Support::doAttribute('href', $formEditUrl) . ">Edit</a> <small>[$sipParamString]</small></p>";
199

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

202
203
204
        $html .= $this->getFormTag();

        return $html;
205
206
    }

207
    /**
208
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of the SIP.
209
     *
210
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:..., ...]</small>
211
     */
212
    public function createFormEditUrl() {
213

Carsten  Rose's avatar
Carsten Rose committed
214
        if (!$this->showDebugInfo) {
215
216
            return '';
        }
217

218
219
220
221
222
        $queryStringArray = [
            'id' => $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3),
            'form' => 'form',
            'r' => $this->formSpec['id']
        ];
223

224
        $queryString = Support::arrayToQueryString($queryStringArray);
225

226
227
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
228

229
        return $url;
230
231
232
    }

    /**
233
234
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
235
236
237
238
239
240
241
242
243
244
245
246
     * @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];
    }

    /**
247
     * Returns '<form ...>'-tag with various attributes.
248
249
250
251
252
253
254
255
256
257
258
259
260
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

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

    /**
     * Build an assoc array with standard form attributes.
     *
261
     * @return array
262
263
264
     */
    public function getFormTagAtrributes() {

265
        $attribute['id'] = $this->getFormId();
266
267
268
269
270
271
272
273
274
275
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

276
277
278
279
280
281
282
283
284
285
    /**
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
        return $this->formId;
    }

286
287
288
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
289
290
     * Notice: the SIP will be transferred as POST Parameter.
     *
291
292
293
294
295
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {

296
        return API_DIR . '/save.php';
297
298
299
300
301
302
303
304
305
306
307
308
    }

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

309
        $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"');
310
311
312
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
313

Carsten  Rose's avatar
Carsten Rose committed
314
315
316
317
    abstract public function tail();

    abstract public function doSubrecords();

318
    abstract public function getProcessFilter();
319
320

    /**
321
322
     * Process all FormElements: build corresponding HTML code. Collect and return all HTML code.
     *
323
     * @param $recordId
324
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
325
     * @param int $feIdContainer
326
327
328
329
     * @param array $json
     * @param string $modeCollectFe
     * @param bool $htmlElementNameIdZero
     * @param string $storeUse
330
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
331
     * @return string
332
333
     * @throws CodeException
     * @throws DbException
334
     * @throws \qfq\UserFormException
335
     */
336
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, array &$json,
337
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false, $storeUse = STORE_USE_DEFAULT, $mode = FORM_LOAD) {
338
        $html = '';
339
        $flagOutput = false;
340
341

        // get current data record
342
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
343
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec[F_TABLE_NAME] . " WHERE id = ?", ROW_EXPECT_1, array($recordId));
344
            $this->store->setVarArray($row, STORE_RECORD);
345
        }
346
347
348

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

350
351
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
Carsten  Rose's avatar
Carsten Rose committed
352
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe['dynamicUpdate'] === 'no')
353
354
355
356
            ) {
                continue; // skip this FE
            }

357
358
            $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA);

359
360
            $debugStack = array();

361
362
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
363
364

            // evaluate current FormElement
365
            $formElement = $this->evaluate->parseArray($fe, $debugStack);
366

367
368
369
            // Some Defaults
            $formElement = Support::setFeDefaults($formElement);

370
371
372
373
374
375
376
377
378
            if ($flagOutput === true) {
                Support::setIfNotSet($formElement, F_BS_LABEL_COLUMNS);
                Support::setIfNotSet($formElement, F_BS_INPUT_COLUMNS);
                Support::setIfNotSet($formElement, F_BS_NOTE_COLUMNS);
                $label = ($formElement[F_BS_LABEL_COLUMNS] == '') ? $this->formSpec[F_BS_LABEL_COLUMNS] : $formElement[F_BS_LABEL_COLUMNS];
                $input = ($formElement[F_BS_INPUT_COLUMNS] == '') ? $this->formSpec[F_BS_INPUT_COLUMNS] : $formElement[F_BS_INPUT_COLUMNS];
                $note = ($formElement[F_BS_NOTE_COLUMNS] == '') ? $this->formSpec[F_BS_NOTE_COLUMNS] : $formElement[F_BS_NOTE_COLUMNS];
                $this->fillWrapLabelInputNote($label, $input, $note);
            }
379

380
            // Get default value
381
            $value = ($formElement['value'] === '') ? $this->store->getVar($formElement['name'], $storeUse,
382
                $formElement['checkType']) : $formElement['value'];
Carsten  Rose's avatar
Carsten Rose committed
383

384
385
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
386
            $htmlFormElementId = HelperFormElement::buildFormElementName($formElement['name'], ($htmlElementNameIdZero) ? 0 : $recordId);
387

Carsten  Rose's avatar
Carsten Rose committed
388
            // Construct Marshaller Name: buildElement
389
            $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement[FE_TYPE]];
390

Carsten  Rose's avatar
Carsten Rose committed
391
            $jsonElement = array();
392
            // Render pure element
393
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
394

395
396
//            $fake0 = $fe['dynamicUpdate'];
//            $fake1 = $formElement['dynamicUpdate'];
Carsten  Rose's avatar
Carsten Rose committed
397
398
399
400
401
402
403
404

            // 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
405
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe['dynamicUpdate'] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
406
407
408
409
410
411
                    if (isset($jsonElement[0]) && is_array($jsonElement[0])) {
                        // Checkboxes are delivered as array of arrays: unnest them and append them to the existing json array.
                        $json = array_merge($json, $jsonElement);
                    } else {
                        $json[] = $jsonElement;
                    }
Carsten  Rose's avatar
Carsten Rose committed
412
413
                }
            }
414

415
416
417
418
419
            if ($flagOutput) {
                // debugStack as Tooltip
                if ($this->showDebugInfo && count($debugStack) > 0) {
                    $elementHtml = Support::appendTooltip($elementHtml, implode("\n", $debugStack));
                }
420

421
422
                // Construct Marshaller Name: buildRow
                $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
423

424
425
                $html .= $this->$buildRowName($formElement, $elementHtml, $htmlFormElementId);
            }
426
        }
427

428
429
430
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

431
432
433
        return $html;
    }

434
435
    abstract public function fillWrapLabelInputNote($label, $input, $note);

436
437
438
    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
439
     * @param array $json
440
441
442
443
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
444
    public function buildHiddenSip(array &$json) {
445

446
        $sipArray = $this->store->getStore(STORE_SIP);
447
448

        // do not include system vars
449
450
451
452
453
454
455
456
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

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

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

457
        $json[] = $this->getJsonElementUpdate(CLIENT_SIP, $sipValue, FE_MODE_SHOW);
458
459
460
461
462
463
464

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

    /**
     * @param $htmlFormElementId
     * @param string|array $value
465
     * @param string $feMode disabled|readonly|''
466
467
     * @return array
     */
468
    private function getJsonElementUpdate($htmlFormElementId, $value, $feMode) {
469

470
        $json = $this->getJsonFeMode($feMode);
471
472
473
474
475
476
477

        $json['form-element'] = $htmlFormElementId;
        $json['value'] = $value;

        return $json;
    }

478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
    /**
     * Set corresponding JSON attributes readonly/required/disabled, based on $formElement[FE_MODE].
     *
     * @param array $feMode
     * @return array
     * @throws UserFormException
     */
    private function getJsonFeMode($feMode) {

        $this->getFeMode($feMode, $hidden, $disabled, $required);

        return [API_JSON_HIDDEN => $hidden === 'yes', API_JSON_DISABLED => $disabled === 'yes', API_JSON_REQUIRED => $required === 'yes'];
    }

    /**
     * @param $feMode
     * @param $hidden
     * @param $disabled
     * @param $required
     * @throws \qfq\UserFormException
     */
    private function getFeMode($feMode, &$hidden, &$disabled, &$required) {
        $hidden = 'no';
        $disabled = 'no';
        $required = 'no';

        switch ($feMode) {
            case FE_MODE_SHOW:
                break;
            case FE_MODE_REQUIRED:
                $required = 'yes';
                break;
            case FE_MODE_READONLY:
511
                $disabled = 'yes';  // convert the UI status 'readonly' to the HTML/CSS status 'disabled'.
512
513
514
515
516
517
518
519
520
521
                break;
            case FE_MODE_HIDDEN:
                $hidden = 'yes';
                break;
            default:
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
                break;
        }
    }

522
523
524
525
526
527
528
529
530
531
532
    /**
     * 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
533
534
535
536
    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @throws CodeException
537
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
538
539
540
541
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
        $urlParam = $this->store->getStore(STORE_SIP);
        $urlParam[SIP_RECORD_ID] = 0;
542

Carsten  Rose's avatar
Carsten Rose committed
543
544
        unset($urlParam[SIP_SIP]);
        unset($urlParam[SIP_URLPARAM]);
545
546

        Support::appendTypo3ParameterToArray($urlParam);
Carsten  Rose's avatar
Carsten Rose committed
547
548
549
550
551

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

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

552
        if ($this->showDebugInfo) {
553
            //TODO: missing decoding of SIP
554
555
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }
Carsten  Rose's avatar
Carsten Rose committed
556
557
558
559

        return $url;
    }

560
    abstract public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementId);
561

562
    abstract public function buildRowPill(array $formElement, $elementHtml);
563

564
    abstract public function buildRowFieldset(array $formElement, $elementHtml);
565

566
    abstract public function buildRowSubrecord(array $formElement, $elementHtml);
567

568
    /**
569
570
     * Builds a label, typically for an html-'<input>'-element.
     *
571
572
     * @param string $htmlFormElementId
     * @param string $label
573
574
     * @return string
     */
575
    public function buildLabel($htmlFormElementId, $label) {
576
577
578
579
        $attributes = Support::doAttribute('for', $htmlFormElementId);
        $attributes .= Support::doAttribute('class', 'control-label');

        $html = Support::wrapTag("<label $attributes>", $label);
580
581

        return $html;
582
583
    }

584
585
586
587
    /**
     * 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"]
Carsten  Rose's avatar
Carsten Rose committed
588
     *           [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
589
590
591
592
593
     *
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
594
     * @param array $json
595
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
596
     * @return string
597
     * @throws \qfq\UserFormException
598
     */
599
    public function buildInput(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) {
600
        $textarea = '';
601

602
        $attribute = Support::doAttribute('name', $htmlFormElementId);
603
        $attribute .= Support::doAttribute('class', 'form-control');
604

605
        // Check for input type 'textarea'
606
        $colsRows = explode(',', $formElement['size'], 2);
607
        if (count($colsRows) === 2) {
608
            // <textarea>
609
610
            $htmlTag = '<textarea';

611
612
            $attribute .= Support::doAttribute('cols', $colsRows[0]);
            $attribute .= Support::doAttribute('rows', $colsRows[1]);
613
            $textarea = htmlentities($value) . '</textarea>';
614
615

        } else {
Carsten  Rose's avatar
Carsten Rose committed
616
617
618
619
            $htmlTag = '<input';

            $this->adjustMaxLength($formElement);

620
            if ($formElement['maxLength'] > 0 && $value !== '') {
Carsten  Rose's avatar
Carsten Rose committed
621
                // crop string only if it's not empty (substr returns false on empty strings)
622
                $value = substr($value, 0, $formElement['maxLength']);
623
            }
624
625
626
627

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

630
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
631
632
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
633
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
634

635
        $attribute .= $this->getAttributeFeMode($formElement[FE_MODE]);
636

637
        $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement[FE_MODE]);
Carsten  Rose's avatar
Carsten Rose committed
638

639
        return "$htmlTag $attribute>$textarea" . $this->getHelpBlock();
640

641
642
    }

Carsten  Rose's avatar
Carsten Rose committed
643
644
645
646
647
648
649
650
    /**
     * @param array $formElement
     */
    private function adjustMaxLength(array &$formElement) {

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

651
        switch ($formElement[FE_TYPE]) {
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
            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
672
        if ($maxLength !== false) {
673
            if (is_numeric($formElement['maxLength']) && $formElement['maxLength'] != 0) {
Carsten  Rose's avatar
Carsten Rose committed
674
675
676
677
678
679
680
681
682
                if ($formElement['maxLength'] > $maxLength) {
                    $formElement['maxLength'] = $maxLength;
                }
            } else {
                $formElement['maxLength'] = $maxLength;
            }
        }
    }

683
    /**
684
685
     * Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
     *
686
     * @param $column
687
     * @return bool|int  a) 'false' if there is no length definition, b) length definition, c) date|time|datetime|timestamp use hardcoded length
688
689
690
691
692
     */
    private function getColumnSize($column) {
        $matches = array();

        $typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);
693
694
695
696
697
698
699
700
701
        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:
702
703
704
                if (substr($typeSpec, 0, 4) === 'set(' || substr($typeSpec, 0, 5) === 'enum(') {
                    return $this->maxLengthSetEnum($typeSpec);
                }
705
706
                break;
        }
707

708
        // e.g.: string(64) >> 64, enum('yes','no') >> false
709
710
711
712
713
714
        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
715
716
    }

717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
    /**
     * Get the strlen of the longest element in enum('val1','val2',...,'valn') or set('val1','val2',...,'valn')
     *
     * @param string $typeSpec
     * @return int
     */
    private function maxLengthSetEnum($typeSpec) {
        $startPos = (substr($typeSpec, 0, 4) === 'set(') ? 4 : 5;
        $max = 0;

        $valueList = substr($typeSpec, $startPos, strlen($typeSpec) - $startPos - 1);
        $valueArr = explode(',', $valueList);
        foreach ($valueArr as $value) {
            $value = trim($value, "'");
            $len = strlen($value);
            if ($len > $max) {
                $max = $len;
            }
        }

        return $max;
    }

740
741
    /**
     * Builds a HTML attribute list, based on  $attributeList.
742
     *
743
     * E.g.: attributeList: [ 'type', 'autofocus' ]
744
     *       generates: 'type="$formElement[FE_TYPE]" autofocus="$formElement['autofocus']" '
745
746
747
748
749
     *
     * @param array $formElement
     * @param array $attributeList
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
750
    private function getAttributeList(array $formElement, array $attributeList) {
751
752
753
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
754
                $attribute .= Support::doAttribute(strtolower($item), $formElement[$item]);
755
756
757
758
759
760
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
761
     *
762
     *   type     data                      result
763
     *   -------  -----------------------   -------------------------------------------------------------------------------
764
765
     *   min|max  <min value>|<max value>   min="$attrData[0]"|max="$attrData[1]"
     *   pattern  <regexp>                  pattern="$data"
Carsten  Rose's avatar
Carsten Rose committed
766
     *   digit    -                         pattern="^[0-9]*$"
767
     *   email    -                         pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
768
     *   alnumx   -
769
     *
770
     * For 'min/max' and 'pattern' the 'data' will be injected in the attribute string via '%s'.
771
772
773
774
     *
     * @param $type
     * @param $data
     * @return string
775
     * @throws \qfq\UserFormException
776
     */
Carsten  Rose's avatar
Carsten Rose committed
777
    private function getInputCheckPattern($type, $data) {
778
779
        $attribute = '';

780
781
        if ($type === '') {
            return '';
782
        }
783

784
785
786
787
788
        switch ($type) {
            case SANITIZE_ALLOW_MIN_MAX:
            case SANITIZE_ALLOW_MIN_MAX_DATE:
                $arrData = explode("|", $data);
                if (count($arrData) != 2 || $arrData[0] == '' || $arrData[1] == '')
789
                    throw new UserFormException("Missing MIN|MAX values", ERROR_MISSING_MIN_MAX);
790

791
792
793
                $attribute = 'min="' . $arrData[0] . '" ';
                $attribute .= 'max="' . $arrData[1] . '" ';
                break;
794

795
796
797
798
799
800
801
802
803
804
            case SANITIZE_ALLOW_PATTERN:
                $attribute = 'pattern="' . $data . '" ';
                break;

            case SANITIZE_ALLOW_ALL:
                break;

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

807
808
809
810
        return $attribute;
    }

    /**
811
     * Set corresponding html attributes readonly/required/disabled, based on $formElement[FE_MODE].
812
     *
813
     * @param string $feMode
814
     * @return string
815
     * @throws UserFormException
816
     */
817
    private function getAttributeFeMode($feMode) {
818
819
        $attribute = '';

820
821
822
823
824
        $this->getFeMode($feMode, $hidden, $disabled, $required);

        switch ($feMode) {
            case FE_MODE_HIDDEN:
            case FE_MODE_SHOW:
825
                break;
826
827
828
            case FE_MODE_REQUIRED:
            case FE_MODE_READONLY:
                $attribute .= Support::doAttribute($feMode, $feMode);
829
830
                break;
            default:
831
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
832
833
                break;
        }
834

835
        // Attributes: data-...
836
        $attribute .= Support::doAttribute(DATA_HIDDEN, $hidden);
837
        $attribute .= Support::doAttribute(DATA_DISABLED, $disabled);
838
839
        $attribute .= Support::doAttribute(DATA_REQUIRED, $required);

840
841
842
        return $attribute;
    }

843
844
845
846
847
848
849
    /**
     * @return string
     */
    private function getHelpBlock() {
        return '<div class="help-block with-errors"></div>';
    }

850
851
852
    /**
     * Builds HTML 'checkbox' element.
     *
853
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transferred first.
854
855
856
     *
     * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
     *         <input name="$htmlFormElementId" type="checkbox" [autofocus="autofocus"]
Carsten  Rose's avatar
Carsten Rose committed
857
     *            [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
858
859
860
861
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
862
     * @param array $json
863
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE*
864
     * @return string
865
866
     * @throws CodeException
     * @throws \qfq\UserFormException
867
     */
868
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) {
869
870
871
872
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
873
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
874
875
876

        // Get fallback, if 'checkBoxMode' is not defined:
        if (!isset($formElement['checkBoxMode'])) {
877
            // This fallback is problematic if 'set' or 'enum' has 2 : defaults to single but maybe multi is meant.
878
879
880
881
            $formElement['checkBoxMode'] = (count($itemKey) > 2) ? 'multi' : 'single';
        }

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

888
        $attributeBase = $this->getAttributeFeMode($formElement[FE_MODE]);
889
        $attributeBase .= Support::doAttribute('type', $formElement[FE_TYPE]);
890
891
892

        switch ($formElement['checkBoxMode']) {
            case 'single':
893
                $html = $this->buildCheckboxSingle($formElement, $htmlFormElementId, $attributeBase, $value, $json);
894
895
                break;
            case 'multi';
896
                $html = $this->buildCheckboxMulti($formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue, $json);
897
898
                break;
            default:
899
                throw new UserFormException('checkBoxMode: \'' . $formElement['checkBoxMode'] . '\' is unknown.', ERROR_CHECKBOXMODE_UNKNOWN);
900
        }
901

902
903
904
        return $html;
    }

905
    /**
906
907
908
909
910
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
911
     * Copies the found keys to &$itemKey and the values to &$itemValue
912
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
913
914
915
916
     *
     * @param array $formElement
     * @param $itemKey
     * @param $itemValue
917
     * @throws CodeException
918
     * @throws \qfq\UserFormException
919
     */
920
    public function getKeyValueListFromSqlEnumSpec(array $formElement, &$itemKey, &$itemValue) {
921
922
923
924
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();

925
926
927
        if (count($formElement) < 20)
            throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT);

928
        // Call getItemsForEnumOrSet() only if there a corresponding column really exist.
929
        if (false !== $this->store->getVar($formElement['name'], STORE_TABLE_COLUMN_TYPES)) {
930
931
            $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);
        }
932
933

        if (is_array($formElement['sql1'])) {
934
935
936
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
937

938
939
940
941
                // 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]);
                }
942

943
944
945
946
947
948
                $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]);
                }
949
            }
950
        } elseif (isset($formElement['itemList']) && strlen($formElement['itemList']) > 0) {
951
            $arr = KeyValueStringParser::parse($formElement['itemList'], ':', ',', KVP_IF_VALUE_EMPTY_COPY_KEY);
952
953
            $itemValue = array_values($arr);
            $itemKey = array_keys($arr);
954
        } elseif ($fieldType === 'enum' || $fieldType === 'set') {
Carsten  Rose's avatar
Carsten Rose committed
955
            // already done at the beginning with '$this->getItemsForEnumOrSet($formElement['name'], $fieldType);'
956
        } else {
957
            throw new UserFormException("Missing definition (- nothing found in 'sql1', 'parameter:itemValues', 'enum-' or 'set-definition'", ERROR_MISSING_ITEM_VALUES);
958
959
960
961
962
        }

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }