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 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 " . 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
620
            // <input>
            if ($formElement['maxLength'] > 0) {
Carsten  Rose's avatar
Carsten Rose committed
621
622
623
                // crop string only if it's not empty (substr returns false on empty strings)
                if ($value !== '')
                    $value = substr($value, 0, $formElement['maxLength']);
624

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

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

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

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

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

642
643
    }

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

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

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

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

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

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

        return false;
716
717
    }

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

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

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

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

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

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

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

            case SANITIZE_ALLOW_ALL:
                break;

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

808
809
810
811
        return $attribute;
    }

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

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

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

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

841
842
843
        return $attribute;
    }

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

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

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

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

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

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

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

903
904
905
        return $html;
    }

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

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

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

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

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

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

        if (count($itemKey) === 0) {
            $itemKey