AbstractBuildForm.php 83.1 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',
Carsten  Rose's avatar
Carsten Rose committed
92
            'editor' => 'Editor',
93
            'time' => 'DateTime',
94
95
96
97
            'note' => 'Note',
            'password' => 'Input',
            'radio' => 'Radio',
            'select' => 'Select',
98
            'subrecord' => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
99
            'upload' => 'File',
100
101
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
102
103
        ];

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

126
127
128
        $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>";
129

130
        $this->inputCheckPattern = Sanitize::inputCheckPatternArray();
131
132
    }

133
134
    abstract public function fillWrap();

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

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

159
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
160
161
162
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
        }
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
182
183
184
185

        // <form>
        if ($mode === FORM_LOAD) {
            $htmlTail = $this->tail();
            $htmlSubrecords = $this->doSubrecords();
        }

186
187
188
        $htmlSip = $this->buildHiddenSip($json);

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

191
    /**
192
     * Builds the head area of the form.
193
     *
194
     * @return string
195
     */
196
197
    public function head() {
        $html = '';
198

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

201
202
203
        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
        $formEditUrl = $this->createFormEditUrl();
204

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

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

209
210
211
        $html .= $this->getFormTag();

        return $html;
212
213
    }

214
    /**
215
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of the SIP.
216
     *
217
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:..., ...]</small>
218
     */
219
    public function createFormEditUrl() {
220

Carsten  Rose's avatar
Carsten Rose committed
221
        if (!$this->showDebugInfo) {
222
223
            return '';
        }
224

225
226
227
228
229
        $queryStringArray = [
            'id' => $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3),
            'form' => 'form',
            'r' => $this->formSpec['id']
        ];
230

231
        $queryString = Support::arrayToQueryString($queryStringArray);
232

233
234
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
235

236
        return $url;
237
238
239
    }

    /**
240
241
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
242
243
244
245
246
247
248
249
250
251
252
253
     * @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];
    }

    /**
254
     * Returns '<form ...>'-tag with various attributes.
255
256
257
258
259
260
261
262
263
264
265
266
267
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

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

    /**
     * Build an assoc array with standard form attributes.
     *
268
     * @return array
269
270
271
     */
    public function getFormTagAtrributes() {

272
        $attribute['id'] = $this->getFormId();
273
274
275
276
277
278
279
280
281
282
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

283
    /**
Carsten  Rose's avatar
Carsten Rose committed
284
285
     * Return a uniq form id
     *
286
287
288
289
290
291
292
293
294
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
        return $this->formId;
    }

295
296
297
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
298
299
     * Notice: the SIP will be transferred as POST Parameter.
     *
300
301
302
303
304
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {

305
        return API_DIR . '/save.php';
306
307
308
309
310
311
312
313
314
315
316
317
    }

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

318
        $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"');
319
320
321
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
322

323
    abstract public function getProcessFilter();
324
325

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

        // get current data record
347
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
348
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec[F_TABLE_NAME] . " WHERE id = ?", ROW_EXPECT_1, array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$recordId' from table '" . $this->formSpec[F_TABLE_NAME] . "'.");
349
            $this->store->setVarArray($row, STORE_RECORD);
350
        }
351
352
353

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

355
356
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
Carsten  Rose's avatar
Carsten Rose committed
357
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe['dynamicUpdate'] === 'no')
358
359
360
361
            ) {
                continue; // skip this FE
            }

362
363
            $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA);

364
365
            $debugStack = array();

366
367
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
368
369

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

372
373
374
            // Some Defaults
            $formElement = Support::setFeDefaults($formElement);

375
376
377
378
379
380
381
382
383
            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);
            }
384

385
            // Get default value
386
387
            $value = ($formElement[FE_VALUE] === '') ? $this->store->getVar($formElement['name'], $storeUse,
                $formElement['checkType']) : $formElement[FE_VALUE];
Carsten  Rose's avatar
Carsten Rose committed
388

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

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

Carsten  Rose's avatar
Carsten Rose committed
396
            $jsonElement = array();
397
            // Render pure element
398
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
399

400
401
//            $fake0 = $fe['dynamicUpdate'];
//            $fake1 = $formElement['dynamicUpdate'];
Carsten  Rose's avatar
Carsten Rose committed
402
403
404
405
406
407
408
409

            // 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
410
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe['dynamicUpdate'] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
411
412
413
414
415
416
                    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
417
418
                }
            }
419

420
421
422
423
424
            if ($flagOutput) {
                // debugStack as Tooltip
                if ($this->showDebugInfo && count($debugStack) > 0) {
                    $elementHtml = Support::appendTooltip($elementHtml, implode("\n", $debugStack));
                }
425

426
427
                // Construct Marshaller Name: buildRow
                $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
428

429
430
                $html .= $this->$buildRowName($formElement, $elementHtml, $htmlFormElementId);
            }
431
        }
432

433
434
435
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

436
437
438
        return $html;
    }

439
440
    abstract public function fillWrapLabelInputNote($label, $input, $note);

441
442
443
444
    abstract public function tail();

    abstract public function doSubrecords();

445
446
447
    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
448
     * @param array $json
449
450
451
452
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
453
    public function buildHiddenSip(array &$json) {
454

455
        $sipArray = $this->store->getStore(STORE_SIP);
456
457

        // do not include system vars
458
459
460
461
462
463
464
465
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

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

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

466
        $json[] = $this->getJsonElementUpdate(CLIENT_SIP, $sipValue, FE_MODE_SHOW);
467
468
469
470
471

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

    /**
Carsten  Rose's avatar
Carsten Rose committed
472
473
     * Create an array with standard elements and add 'form-element', 'value'.
     *
474
475
     * @param $htmlFormElementId
     * @param string|array $value
476
     * @param string $feMode disabled|readonly|''
477
478
     * @return array
     */
479
    private function getJsonElementUpdate($htmlFormElementId, $value, $feMode) {
480

481
        $json = $this->getJsonFeMode($feMode);
482
483
484
485
486
487
488

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

        return $json;
    }

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'];
    }

    /**
Carsten  Rose's avatar
Carsten Rose committed
504
505
     * Depending of $feMode set variables $hidden, $disabled, $required to 'yes' or 'no'.
     *
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
     * @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:
524
                $disabled = 'yes';  // convert the UI status 'readonly' to the HTML/CSS status 'disabled'.
525
526
527
528
529
530
531
532
533
534
                break;
            case FE_MODE_HIDDEN:
                $hidden = 'yes';
                break;
            default:
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
                break;
        }
    }

535
536
537
538
539
540
541
542
543
544
545
    /**
     * 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
546
547
548
549
    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @throws CodeException
550
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
551
552
553
554
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
        $urlParam = $this->store->getStore(STORE_SIP);
        $urlParam[SIP_RECORD_ID] = 0;
555

Carsten  Rose's avatar
Carsten Rose committed
556
557
        unset($urlParam[SIP_SIP]);
        unset($urlParam[SIP_URLPARAM]);
558
559

        Support::appendTypo3ParameterToArray($urlParam);
Carsten  Rose's avatar
Carsten Rose committed
560
561
562
563
564

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

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

565
        if ($this->showDebugInfo) {
566
            //TODO: missing decoding of SIP
567
568
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }
Carsten  Rose's avatar
Carsten Rose committed
569
570
571
572

        return $url;
    }

573
    abstract public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementId);
574

575
    abstract public function buildRowPill(array $formElement, $elementHtml);
576

577
    abstract public function buildRowFieldset(array $formElement, $elementHtml);
578

579
    abstract public function buildRowSubrecord(array $formElement, $elementHtml);
580

581
    /**
582
583
     * Builds a label, typically for an html-'<input>'-element.
     *
584
585
     * @param string $htmlFormElementId
     * @param string $label
586
587
     * @return string
     */
588
    public function buildLabel($htmlFormElementId, $label) {
589
590
591
592
        $attributes = Support::doAttribute('for', $htmlFormElementId);
        $attributes .= Support::doAttribute('class', 'control-label');

        $html = Support::wrapTag("<label $attributes>", $label);
593
594

        return $html;
595
596
    }

597
598
599
600
    /**
     * 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
601
     *           [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
602
603
604
605
606
     *
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
607
     * @param array $json
608
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
609
     * @return string
610
     * @throws \qfq\UserFormException
611
     */
612
    public function buildInput(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) {
613
        $textarea = '';
614

615
        $attribute = Support::doAttribute('name', $htmlFormElementId);
616
        $attribute .= Support::doAttribute('class', 'form-control');
617

618
        // Check for input type 'textarea'
619
        $colsRows = explode(',', $formElement['size'], 2);
620
        if (count($colsRows) === 2) {
621
            // <textarea>
622
623
            $htmlTag = '<textarea';

624
625
            $attribute .= Support::doAttribute('cols', $colsRows[0]);
            $attribute .= Support::doAttribute('rows', $colsRows[1]);
626
            $textarea = htmlentities($value) . '</textarea>';
627
628

        } else {
Carsten  Rose's avatar
Carsten Rose committed
629
630
631
632
            $htmlTag = '<input';

            $this->adjustMaxLength($formElement);

633
            if ($formElement['maxLength'] > 0 && $value !== '') {
Carsten  Rose's avatar
Carsten Rose committed
634
                // crop string only if it's not empty (substr returns false on empty strings)
635
                $value = substr($value, 0, $formElement['maxLength']);
636
            }
637
638
639
640

            // '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
641
        }
642

643
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
644
645
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
646
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
647

648
        $attribute .= $this->getAttributeFeMode($formElement[FE_MODE]);
649

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

652
        return "$htmlTag $attribute>$textarea" . $this->getHelpBlock();
653

654
655
    }

Carsten  Rose's avatar
Carsten Rose committed
656
    /**
Carsten  Rose's avatar
Carsten Rose committed
657
658
     * Calculates the maxlength of an input field, based on formElement type, formElement user definition and table.field definition.
     *
Carsten  Rose's avatar
Carsten Rose committed
659
660
661
662
663
664
665
     * @param array $formElement
     */
    private function adjustMaxLength(array &$formElement) {

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

666
        switch ($formElement[FE_TYPE]) {
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
            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
687
        if ($maxLength !== false) {
688
            if (is_numeric($formElement['maxLength']) && $formElement['maxLength'] != 0) {
Carsten  Rose's avatar
Carsten Rose committed
689
690
691
692
693
694
695
696
697
                if ($formElement['maxLength'] > $maxLength) {
                    $formElement['maxLength'] = $maxLength;
                }
            } else {
                $formElement['maxLength'] = $maxLength;
            }
        }
    }

698
    /**
699
700
     * Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
     *
701
     * @param $column
702
     * @return bool|int  a) 'false' if there is no length definition, b) length definition, c) date|time|datetime|timestamp use hardcoded length
703
704
705
706
707
     */
    private function getColumnSize($column) {
        $matches = array();

        $typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);
708
709
710
711
712
713
714
715
716
        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:
717
718
719
                if (substr($typeSpec, 0, 4) === 'set(' || substr($typeSpec, 0, 5) === 'enum(') {
                    return $this->maxLengthSetEnum($typeSpec);
                }
720
721
                break;
        }
722

723
        // e.g.: string(64) >> 64, enum('yes','no') >> false
724
725
726
727
728
729
        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
730
731
    }

732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
    /**
     * 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;
    }

755
756
    /**
     * Builds a HTML attribute list, based on  $attributeList.
757
     *
758
     * E.g.: attributeList: [ 'type', 'autofocus' ]
759
     *       generates: 'type="$formElement[FE_TYPE]" autofocus="$formElement['autofocus']" '
760
761
762
763
764
     *
     * @param array $formElement
     * @param array $attributeList
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
765
    private function getAttributeList(array $formElement, array $attributeList) {
766
767
768
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
769
                $attribute .= Support::doAttribute(strtolower($item), $formElement[$item]);
770
771
772
773
774
775
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
776
     *
777
     *   type     data                      result
778
     *   -------  -----------------------   -------------------------------------------------------------------------------
779
780
     *   min|max  <min value>|<max value>   min="$attrData[0]"|max="$attrData[1]"
     *   pattern  <regexp>                  pattern="$data"
Carsten  Rose's avatar
Carsten Rose committed
781
     *   digit    -                         pattern="^[0-9]*$"
782
     *   email    -                         pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
783
     *   alnumx   -
784
     *
785
     * For 'min/max' and 'pattern' the 'data' will be injected in the attribute string via '%s'.
786
787
788
789
     *
     * @param $type
     * @param $data
     * @return string
790
     * @throws \qfq\UserFormException
791
     */
Carsten  Rose's avatar
Carsten Rose committed
792
    private function getInputCheckPattern($type, $data) {
793
794
        $attribute = '';

795
796
        if ($type === '') {
            return '';
797
        }
798

799
800
801
802
803
        switch ($type) {
            case SANITIZE_ALLOW_MIN_MAX:
            case SANITIZE_ALLOW_MIN_MAX_DATE:
                $arrData = explode("|", $data);
                if (count($arrData) != 2 || $arrData[0] == '' || $arrData[1] == '')
804
                    throw new UserFormException("Missing MIN|MAX values", ERROR_MISSING_MIN_MAX);
805

806
807
808
                $attribute = 'min="' . $arrData[0] . '" ';
                $attribute .= 'max="' . $arrData[1] . '" ';
                break;
809

810
811
812
813
814
815
816
817
818
819
            case SANITIZE_ALLOW_PATTERN:
                $attribute = 'pattern="' . $data . '" ';
                break;

            case SANITIZE_ALLOW_ALL:
                break;

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

822
823
824
825
        return $attribute;
    }

    /**
826
     * Set corresponding html attributes readonly/required/disabled, based on $formElement[FE_MODE].
827
     *
828
     * @param string $feMode
829
     * @return string
830
     * @throws UserFormException
831
     */
832
    private function getAttributeFeMode($feMode) {
833
834
        $attribute = '';

835
836
837
838
839
        $this->getFeMode($feMode, $hidden, $disabled, $required);

        switch ($feMode) {
            case FE_MODE_HIDDEN:
            case FE_MODE_SHOW:
840
                break;
841
842
843
            case FE_MODE_REQUIRED:
            case FE_MODE_READONLY:
                $attribute .= Support::doAttribute($feMode, $feMode);
844
845
                break;
            default:
846
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
847
848
                break;
        }
849

850
        // Attributes: data-...
851
        $attribute .= Support::doAttribute(DATA_HIDDEN, $hidden);
852
        $attribute .= Support::doAttribute(DATA_DISABLED, $disabled);
853
854
        $attribute .= Support::doAttribute(DATA_REQUIRED, $required);

855
856
857
        return $attribute;
    }

858
    /**
Carsten  Rose's avatar
Carsten Rose committed
859
860
     * Build HelpBlock
     *
861
862
863
864
865
866
     * @return string
     */
    private function getHelpBlock() {
        return '<div class="help-block with-errors"></div>';
    }

867
868
869
    /**
     * Builds HTML 'checkbox' element.
     *
870
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transferred first.
871
872
873
     *
     * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
     *         <input name="$htmlFormElementId" type="checkbox" [autofocus="autofocus"]
Carsten  Rose's avatar
Carsten Rose committed
874
     *            [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
875
876
877
878
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
879
     * @param array $json
880
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE*
881
     * @return string
882
883
     * @throws CodeException
     * @throws \qfq\UserFormException
884
     */
885
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) {
886
887
888
889
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
890
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
891
892
893

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

        if ($formElement['checkBoxMode'] === 'multi') {
899
//            $htmlFormElementId .= '[]';
900
        } else {
901
            // Fill meaningfull defaults to parameter: checked|unchecked  (CHECKBOX_VALUE_CHECKED|CHECKBOX_VALUE_UNCHECKED)
902
903
904
            $this->prepareCheckboxCheckedUncheckedValue($itemKey, $formElement);
        }

905
        $attributeBase = $this->getAttributeFeMode($formElement[FE_MODE]);
906
        $attributeBase .= Support::doAttribute('type', $formElement[FE_TYPE]);
907
908
909

        switch ($formElement['checkBoxMode']) {
            case 'single':
910
                $html = $this->buildCheckboxSingle($formElement, $htmlFormElementId, $attributeBase, $value, $json);
911
912
                break;
            case 'multi';
913
                $html = $this->buildCheckboxMulti($formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue, $json);
914
915
                break;
            default:
916
                throw new UserFormException('checkBoxMode: \'' . $formElement['checkBoxMode'] . '\' is unknown.', ERROR_CHECKBOXMODE_UNKNOWN);
917
        }
918

919
920
921
        return $html;
    }

922
    /**
923
924
925
926
927
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
928
     * Copies the found keys to &$itemKey and the values to &$itemValue
929
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
930
931
932
933
     *
     * @param array $formElement
     * @param $itemKey
     * @param $itemValue
934
     * @throws CodeException
935
     * @throws \qfq\UserFormException
936
     */
937
    public function getKeyValueListFromSqlEnumSpec(array $formElement, &$itemKey, &$itemValue) {
938
939
940
941
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();

942
943
944
        if (count($formElement) < 20)
            throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT);

945
        // Call getItemsForEnumOrSet() only if there a corresponding column really exist.
946
        if (false !== $this->store->getVar($formElement['name'], STORE_TABLE_COLUMN_TYPES)) {
947
948
            $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);
        }
949
950

        if (is_array($formElement['sql1'])) {
951
952
953
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
954

955
956
957
958
                // 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]);
                }
959

960
961
962