AbstractBuildForm.php 75.8 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 " . 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>";
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 " . Support::doAttribute('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
        $html = '';
338
        $flagOutput = false;
339
340

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

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

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

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

358
359
            $debugStack = array();

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

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

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

369
370
371
372
373
374
375
376
377
            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);
            }
378

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

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

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

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

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

            // 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
404
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe['dynamicUpdate'] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
405
406
407
408
409
410
                    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
411
412
                }
            }
413

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

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

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

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

430
431
432
        return $html;
    }

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

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

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

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

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

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

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

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

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

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

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

        return $json;
    }

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

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

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

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

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

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

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

        return $url;
    }

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

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

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

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

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

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

        return $html;
581
582
    }

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

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

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

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

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

            $this->adjustMaxLength($formElement);

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

            // '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
627
        }
628

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

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

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

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

640
641
    }

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

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

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

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

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

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

        return false;
714
715
    }

716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
    /**
     * 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;
    }

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

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

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

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

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

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

            case SANITIZE_ALLOW_ALL:
                break;

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

806
807
808
809
        return $attribute;
    }

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

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

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

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

839
840
841
        return $attribute;
    }

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

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

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

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

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

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

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

901
902
903
        return $html;
    }

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

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

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

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

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

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

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
962
963
964
965
966
967
968
969
970