AbstractBuildForm.php 75.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
 */
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
34
35
    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();
36
    protected $wrap = array();
37
    protected $symbol = array();
Carsten  Rose's avatar
Carsten Rose committed
38
    protected $showDebugInfo = false;
39
    protected $inputCheckPattern = array();
Carsten  Rose's avatar
Carsten Rose committed
40

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

43
44
45
46
47
48
49
50
    /**
     * @var Store
     */
    protected $store = null;
    /**
     * @var Evaluate
     */
    protected $evaluate = null;
51
52
53
    /**
     * @var string
     */
54
    private $formId = null;
55
56
57
58
59
    /**
     * @var Sip
     */
    private $sip = null;

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

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

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

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

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

123
124
125
        $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>";
126

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

130
131
    abstract public function fillWrap();

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

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

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

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

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

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

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

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

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

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

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

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

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

        return $html;
204
205
    }

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

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

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

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

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

228
        return $url;
229
230
231
    }

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

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

        $attribute = $this->getFormTagAtrributes();

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

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

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

        return $attribute;
    }

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

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

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

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

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

    }
312

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

    abstract public function doSubrecords();

317
    abstract public function getProcessFilter();
318
319

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

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

        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
347
348
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
Carsten  Rose's avatar
Carsten Rose committed
349
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe['dynamicUpdate'] === 'no')
350
351
352
353
            ) {
                continue; // skip this FE
            }

354
355
            $debugStack = array();

356
357
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
358
359

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

362
363
364
            // Some Defaults
            $formElement = Support::setFeDefaults($formElement);

365
366
367
            Support::setIfNotSet($formElement, F_BS_LABEL_COLUMNS);
            Support::setIfNotSet($formElement, F_BS_INPUT_COLUMNS);
            Support::setIfNotSet($formElement, F_BS_NOTE_COLUMNS);
368
369
370
371
372
            $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);

373
            // Get default value
374
            $value = ($formElement['value'] === '') ? $this->store->getVar($formElement['name'], $storeUse,
375
                $formElement['checkType']) : $formElement['value'];
Carsten  Rose's avatar
Carsten Rose committed
376

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

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

Carsten  Rose's avatar
Carsten Rose committed
384
            $jsonElement = array();
385
            // Render pure element
386
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
387

388
389
//            $fake0 = $fe['dynamicUpdate'];
//            $fake1 = $formElement['dynamicUpdate'];
Carsten  Rose's avatar
Carsten Rose committed
390
391
392
393
394
395
396
397

            // 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
398
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe['dynamicUpdate'] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
399
400
401
402
403
404
                    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
405
406
                }
            }
407
408

            // debugStack as Tooltip
Carsten  Rose's avatar
Carsten Rose committed
409
            if ($this->showDebugInfo && count($debugStack) > 0) {
410
411
//                $elementHtml = Support::appendTooltip($elementHtml, implode("\n", OnArray::htmlentitiesOnArray($debugStack)));
                $elementHtml = Support::appendTooltip($elementHtml, implode("\n", $debugStack));
412
413
            }

Carsten  Rose's avatar
Carsten Rose committed
414
            // Construct Marshaller Name: buildRow
415
            $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
416

417
            $html .= $this->$buildRowName($formElement, $elementHtml, $htmlFormElementId);
Carsten  Rose's avatar
Carsten Rose committed
418
//            break;
419
        }
420

421
422
423
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

424
425
426
        return $html;
    }

427
428
    abstract public function fillWrapLabelInputNote($label, $input, $note);

429
430
431
    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
432
     * @param array $json
433
434
435
436
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
437
    public function buildHiddenSip(array &$json) {
438

439
        $sipArray = $this->store->getStore(STORE_SIP);
440
441

        // do not include system vars
442
443
444
445
446
447
448
449
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

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

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

450
        $json[] = $this->getJsonElementUpdate(CLIENT_SIP, $sipValue, FE_MODE_SHOW);
451
452
453
454
455
456
457

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

    /**
     * @param $htmlFormElementId
     * @param string|array $value
458
     * @param string $feMode disabled|readonly|''
459
460
     * @return array
     */
461
    private function getJsonElementUpdate($htmlFormElementId, $value, $feMode) {
462

463
        $json = $this->getJsonFeMode($feMode);
464
465
466
467
468
469
470

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

        return $json;
    }

471
472
473
474
475
476
477
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
    /**
     * 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:
504
                $disabled = 'yes';  // convert the UI status 'readonly' to the HTML/CSS status 'disabled'.
505
506
507
508
509
510
511
512
513
514
                break;
            case FE_MODE_HIDDEN:
                $hidden = 'yes';
                break;
            default:
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
                break;
        }
    }

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

Carsten  Rose's avatar
Carsten Rose committed
536
537
        unset($urlParam[SIP_SIP]);
        unset($urlParam[SIP_URLPARAM]);
538
539

        Support::appendTypo3ParameterToArray($urlParam);
Carsten  Rose's avatar
Carsten Rose committed
540
541
542
543
544

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

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

545
        if ($this->showDebugInfo) {
546
            //TODO: missing decoding of SIP
547
548
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }
Carsten  Rose's avatar
Carsten Rose committed
549
550
551
552

        return $url;
    }

553
    abstract public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementId);
554

555
    abstract public function buildRowPill(array $formElement, $elementHtml);
556

557
    abstract public function buildRowFieldset(array $formElement, $elementHtml);
558

559
    abstract public function buildRowSubrecord(array $formElement, $elementHtml);
560

561
    /**
562
563
     * Builds a label, typically for an html-'<input>'-element.
     *
564
565
     * @param string $htmlFormElementId
     * @param string $label
566
567
     * @return string
     */
568
    public function buildLabel($htmlFormElementId, $label) {
569
570
571
572
        $attributes = Support::doAttribute('for', $htmlFormElementId);
        $attributes .= Support::doAttribute('class', 'control-label');

        $html = Support::wrapTag("<label $attributes>", $label);
573
574

        return $html;
575
576
    }

577
578
579
580
    /**
     * 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
581
     *           [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
582
583
584
585
586
     *
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
587
     * @param array $json
588
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
589
     * @return string
590
     * @throws \qfq\UserFormException
591
     */
592
    public function buildInput(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) {
593
        $textarea = '';
594

595
        $attribute = Support::doAttribute('name', $htmlFormElementId);
596
        $attribute .= Support::doAttribute('class', 'form-control');
597

598
        // Check for input type 'textarea'
599
        $colsRows = explode(',', $formElement['size'], 2);
600
        if (count($colsRows) === 2) {
601
            // <textarea>
602
603
            $htmlTag = '<textarea';

604
605
            $attribute .= Support::doAttribute('cols', $colsRows[0]);
            $attribute .= Support::doAttribute('rows', $colsRows[1]);
606
            $textarea = htmlentities($value) . '</textarea>';
607
608

        } else {
Carsten  Rose's avatar
Carsten Rose committed
609
610
611
612
            $htmlTag = '<input';

            $this->adjustMaxLength($formElement);

613
614
            // <input>
            if ($formElement['maxLength'] > 0) {
Carsten  Rose's avatar
Carsten Rose committed
615
616
617
                // crop string only if it's not empty (substr returns false on empty strings)
                if ($value !== '')
                    $value = substr($value, 0, $formElement['maxLength']);
618

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

625
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
626
627
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
628
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
629

630
        $attribute .= $this->getAttributeFeMode($formElement[FE_MODE]);
631

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

634
        return "$htmlTag $attribute>$textarea" . $this->getHelpBlock();
635

636
637
    }

Carsten  Rose's avatar
Carsten Rose committed
638
639
640
641
642
643
644
645
    /**
     * @param array $formElement
     */
    private function adjustMaxLength(array &$formElement) {

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

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

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

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

703
        // e.g.: string(64) >> 64, enum('yes','no') >> false
704
705
706
707
708
709
        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
710
711
    }

712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
    /**
     * 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;
    }

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

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

775
776
        if ($type === '') {
            return '';
777
        }
778

779
780
781
782
783
        switch ($type) {
            case SANITIZE_ALLOW_MIN_MAX:
            case SANITIZE_ALLOW_MIN_MAX_DATE:
                $arrData = explode("|", $data);
                if (count($arrData) != 2 || $arrData[0] == '' || $arrData[1] == '')
784
                    throw new UserFormException("Missing MIN|MAX values", ERROR_MISSING_MIN_MAX);
785

786
787
788
                $attribute = 'min="' . $arrData[0] . '" ';
                $attribute .= 'max="' . $arrData[1] . '" ';
                break;
789

790
791
792
793
794
795
796
797
798
799
            case SANITIZE_ALLOW_PATTERN:
                $attribute = 'pattern="' . $data . '" ';
                break;

            case SANITIZE_ALLOW_ALL:
                break;

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

802
803
804
805
        return $attribute;
    }

    /**
806
     * Set corresponding html attributes readonly/required/disabled, based on $formElement[FE_MODE].
807
     *
808
     * @param string $feMode
809
     * @return string
810
     * @throws UserFormException
811
     */
812
    private function getAttributeFeMode($feMode) {
813
814
        $attribute = '';

815
816
817
818
819
        $this->getFeMode($feMode, $hidden, $disabled, $required);

        switch ($feMode) {
            case FE_MODE_HIDDEN:
            case FE_MODE_SHOW:
820
                break;
821
822
823
            case FE_MODE_REQUIRED:
            case FE_MODE_READONLY:
                $attribute .= Support::doAttribute($feMode, $feMode);
824
825
                break;
            default:
826
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
827
828
                break;
        }
829

830
        // Attributes: data-...
831
        $attribute .= Support::doAttribute(DATA_HIDDEN, $hidden);
832
        $attribute .= Support::doAttribute(DATA_DISABLED, $disabled);
833
834
        $attribute .= Support::doAttribute(DATA_REQUIRED, $required);

835
836
837
        return $attribute;
    }

838
839
840
841
842
843
844
    /**
     * @return string
     */
    private function getHelpBlock() {
        return '<div class="help-block with-errors"></div>';
    }

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

        // Fill $itemKey & $itemValue
868
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
869
870
871

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

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

883
        $attributeBase = $this->getAttributeFeMode($formElement[FE_MODE]);
884
        $attributeBase .= Support::doAttribute('type', $formElement[FE_TYPE]);
885
886
887

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

897
898
899
        return $html;
    }

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

920
921
922
        if (count($formElement) < 20)
            throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT);

923
        // Call getItemsForEnumOrSet() only if there a corresponding column really exist.
924
        if (false !== $this->store->getVar($formElement['name'], STORE_TABLE_COLUMN_TYPES)) {
925
926
            $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);
        }
927
928

        if (is_array($formElement['sql1'])) {
929
930
931
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
932

933
934
935
936
                // 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]);
                }
937

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

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
958
959
960
961
962
963
964
965
966
967

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

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