AbstractBuildForm.php 56.3 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
 */
8
9
10
namespace qfq;

use qfq;
11
use qfq\Store;
12
use qfq\UserFormException;
13
use qfq\OnArray;
14
15
16
17

require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
18
require_once(__DIR__ . '/../qfq/exceptions/UserFormException.php');
19
require_once(__DIR__ . '/../qfq/Database.php');
20
require_once(__DIR__ . '/../qfq/helper/HelperFormElement.php');
21
require_once(__DIR__ . '/../qfq/helper/Support.php');
22
require_once(__DIR__ . '/../qfq/helper/OnArray.php');
23

24
25

/**
Carsten  Rose's avatar
Carsten Rose committed
26
27
 * Class AbstractBuildForm
 * @package qfq
28
 */
29
abstract class AbstractBuildForm {
30
31
32
    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
33
    protected $store = null;
Carsten  Rose's avatar
Carsten Rose committed
34
    protected $evaluate = null;
35
36
    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
    private $formId = null;

46
47
48
49
50
51
52
    /**
     * AbstractBuildForm constructor.
     *
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
     */
53
54
55
56
57
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) {
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
58
        $this->db = new Database();
Carsten  Rose's avatar
Carsten Rose committed
59
        $this->evaluate = new Evaluate($this->store, $this->db);
Carsten  Rose's avatar
Carsten Rose committed
60
        $this->showDebugInfo = ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes');
61

62
63
//        $sip = $this->store->getVar(CLIENT_SIP, STORE_CLIENT);

64
        // render mode specific
65
        $this->fillWrap();
66
67
68

        $this->buildElementFunctionName = [
            'checkbox' => 'Checkbox',
69
70
            'dateJQW' => 'DateJQW',
            'datetimeJQW' => 'DateJQW',
71
72
73
            'email' => 'Input',
            'gridJQW' => 'GridJQW',
            'hidden' => 'Hidden',
74
            'text' => 'Input',
75
76
77
78
            'note' => 'Note',
            'password' => 'Input',
            'radio' => 'Radio',
            'select' => 'Select',
79
            'subrecord' => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
80
            'upload' => 'File',
81
82
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
83
84
        ];

85
86
87
88
89
90
91
        $this->buildRowName = [
            'checkbox' => 'Native',
            'dateJQW' => 'Native',
            'datetimeJQW' => 'Native',
            'email' => 'Native',
            'gridJQW' => 'Native',
            'hidden' => 'Native',
92
            'text' => 'Native',
93
94
95
96
97
98
99
100
101
102
            'note' => 'Native',
            'password' => 'Native',
            'radio' => 'Native',
            'select' => 'Native',
            'subrecord' => 'Subrecord',
            'upload' => 'Native',
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
        ];

103
104
105
        $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>";
106

107
        $this->inputCheckPattern = Sanitize::inputCheckPatternArray();
108
109
    }

110
111
    abstract public function fillWrap();

112
    /**
113
     * Builds complete form. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
114
     *
Carsten  Rose's avatar
Carsten Rose committed
115
116
     * @param $mode
     * @return string|array   $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all formElement.dynamicUpdate-yes  values/states
117
118
     * @throws CodeException
     * @throws DbException
119
     * @throws \qfq\UserFormException
120
     */
121
    public function process($mode, $htmlElementNameIdZero = false) {
Carsten  Rose's avatar
Carsten Rose committed
122
123
124
125
126
        $htmlHead = '';
        $htmlTail = '';
        $htmlSubrecords = '';
        $htmlElements = '';
        $json = array();
127
        $modeCollectFe = ($mode === FORM_SAVE) ? FLAG_ALL : FLAG_DYNAMIC_UPDATE;
128

129
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
130
131
132
133
134
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
            $htmlTail = $this->tail();
            $htmlSubrecords = $this->doSubrecords();
        }
135

136
        $filter = $this->getProcessFilter();
137

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

140
141
            $parentRecords = $this->db->sql($this->formSpec['multiSql']);
            foreach ($parentRecords as $row) {
142
                $this->store->setVarArray($row, STORE_PARENT_RECORD, true);
Carsten  Rose's avatar
Carsten Rose committed
143
                $jsonTmp = array();
144
                $htmlElements = $this->elements($row['_id'], $filter, 0, $jsonTmp, $modeCollectFe);
Carsten  Rose's avatar
Carsten Rose committed
145
                $json[] = $jsonTmp;
146
147
            }
        } else {
148
            $htmlElements = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero);
149
150
        }

151
152
        $htmlSip = $this->buildHiddenSip($json);

153
        // </form>
154

155
        return ($mode === FORM_LOAD) ? $htmlHead . $htmlElements . $htmlSip . $htmlTail . $htmlSubrecords : $json;
156
157
    }

158
    /**
159
     * Builds the head area of the form.
160
     *
161
     * @return string
162
     */
163
164
    public function head() {
        $html = '';
165

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

168
169
170
        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
        $formEditUrl = $this->createFormEditUrl();
171

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

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

176
177
178
        $html .= $this->getFormTag();

        return $html;
179
180
    }

181
    /**
182
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of the SIP.
183
     *
184
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:..., ...]</small>
185
     */
186
    public function createFormEditUrl() {
187

Carsten  Rose's avatar
Carsten Rose committed
188
        if (!$this->showDebugInfo) {
189
190
            return '';
        }
191

192
193
194
195
196
        $queryStringArray = [
            'id' => $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3),
            'form' => 'form',
            'r' => $this->formSpec['id']
        ];
197

198
        $queryString = Support::arrayToQueryString($queryStringArray);
199

200
201
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
202

203
        return $url;
204
205
206
    }

    /**
207
208
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
209
210
211
212
213
214
215
216
217
218
219
220
     * @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];
    }

    /**
221
     * Returns '<form ...>'-tag with various attributes.
222
223
224
225
226
227
228
229
230
231
232
233
234
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

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

    /**
     * Build an assoc array with standard form attributes.
     *
235
     * @return array
236
237
238
     */
    public function getFormTagAtrributes() {

239
        $attribute['id'] = $this->getFormId();
240
241
242
243
244
245
246
247
248
249
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

250
251
252
253
254
255
256
257
258
259
    /**
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
        return $this->formId;
    }

260
261
262
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
263
264
     * Notice: the SIP will be transferred as POST Parameter.
     *
265
266
267
268
269
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {

270
        return API_DIR . '/save.php';
271
272
273
274
275
276
277
278
279
280
281
282
    }

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

283
        $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"');
284
285
286
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
287

Carsten  Rose's avatar
Carsten Rose committed
288
289
290
291
    abstract public function tail();

    abstract public function doSubrecords();

292
    abstract public function getProcessFilter();
293
294

    /**
295
296
     * Process all FormElements: build corresponding HTML code. Collect and return all HTML code.
     *
297
     * @param $recordId
298
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
299
     * @param int $feIdContainer
300
     * @return string
301
302
     * @throws CodeException
     * @throws DbException
303
     * @throws \qfq\UserFormException
304
     */
305
306
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, &$json,
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false) {
307
308
309
        $html = '';

        // get current data record
310
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
311
312
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_EXPECT_1, array($recordId));
            $this->store->setVarArray($row, STORE_RECORD);
313
        }
314
315
316

        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
Carsten  Rose's avatar
Carsten Rose committed
317
318
319
            if (($filter === FORM_ELEMENTS_NATIVE && $fe['type'] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe['type'] !== 'subrecord')
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe['dynamicUpdate'] === 'no')
320
321
322
323
            ) {
                continue; // skip this FE
            }

324
325
            $debugStack = array();

326
327
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
328
329

            // evaluate current FormElement
330
            $evaluate = new Evaluate($this->store, $this->db);
331
            $formElement = $evaluate->parseArray($fe, $debugStack);
332

333
            // Get default value
334
335
            $value = ($formElement['value'] === '') ? $this->store->getVar($formElement['name'], STORE_USE_DEFAULT,
                $formElement['checkType']) : $formElement['value'];
Carsten  Rose's avatar
Carsten Rose committed
336

337
338
339
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
            $htmlFormElementId = HelperFormElement::buildFormElementId($formElement['name'], ($htmlElementNameIdZero) ? 0 : $recordId);
340

Carsten  Rose's avatar
Carsten Rose committed
341
            // Construct Marshaller Name: buildElement
342
343
            $buildElementFunctionName = 'build' . $this->buildElementFunctionName[$formElement['type']];

Carsten  Rose's avatar
Carsten Rose committed
344
            $jsonElement = array();
345
            // Render pure element
Carsten  Rose's avatar
Carsten Rose committed
346
347
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $jsonElement);

348
349
//            $fake0 = $fe['dynamicUpdate'];
//            $fake1 = $formElement['dynamicUpdate'];
Carsten  Rose's avatar
Carsten Rose committed
350
351
352
353
354
355
356
357

            // 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
358
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe['dynamicUpdate'] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
359
360
361
                    $json[] = $jsonElement;
                }
            }
362
363

            // debugStack as Tooltip
Carsten  Rose's avatar
Carsten Rose committed
364
            if ($this->showDebugInfo && count($debugStack) > 0) {
365
366
//                $elementHtml = Support::appendTooltip($elementHtml, implode("\n", OnArray::htmlentitiesOnArray($debugStack)));
                $elementHtml = Support::appendTooltip($elementHtml, implode("\n", $debugStack));
367
368
            }

Carsten  Rose's avatar
Carsten Rose committed
369
            // Construct Marshaller Name: buildRow
370
371
372
            $buildRowName = 'buildRow' . $this->buildRowName[$formElement['type']];

            $html .= $this->$buildRowName($formElement, $elementHtml);
Carsten  Rose's avatar
Carsten Rose committed
373
//            break;
374
        }
375

376
377
378
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

379
380
381
        return $html;
    }

382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
     * @param $json
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
    public function buildHiddenSip(&$json) {
        $sipArray = $this->store->getStore(STORE_SIP);
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

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

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

        $json[] = $this->getJsonElementUpdate(CLIENT_SIP, $sipValue, '');

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

    /**
     * @param $htmlFormElementId
     * @param string|array $value
     * @param string $mode disabled|readonly|''
     * @return array
     */
    private function getJsonElementUpdate($htmlFormElementId, $value, $mode) {
        $json = array();

        $json['form-element'] = $htmlFormElementId;
        $json['value'] = $value;
        $json['disabled'] = ($mode === 'disabled');
        $json['readonly'] = ($mode === 'readonly');

        return $json;
    }

    /**
     * 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
433
434
435
436
    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @throws CodeException
437
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
438
439
440
441
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
        $urlParam = $this->store->getStore(STORE_SIP);
        $urlParam[SIP_RECORD_ID] = 0;
442

Carsten  Rose's avatar
Carsten Rose committed
443
444
        unset($urlParam[SIP_SIP]);
        unset($urlParam[SIP_URLPARAM]);
445
446

        Support::appendTypo3ParameterToArray($urlParam);
Carsten  Rose's avatar
Carsten Rose committed
447
448
449
450
451

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

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

452
        if ($this->showDebugInfo) {
453
            //TODO: missing decoding of SIP
454
455
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }
Carsten  Rose's avatar
Carsten Rose committed
456
457
458
459

        return $url;
    }

460
461
462
463
464
465
466
467
    abstract public function buildRowNative($formElement, $elementHtml);

    abstract public function buildRowPill($formElement, $elementHtml);

    abstract public function buildRowFieldset($formElement, $elementHtml);

    abstract public function buildRowSubrecord($formElement, $elementHtml);

468
    /**
469
470
     * Builds a label, typically for an html-'<input>'-element.
     *
471
472
     * @param array $htmlFormElementId
     * @param $label
473
474
     * @return string
     */
475
476
477
478
    public function buildLabel($htmlFormElementId, $label) {
        $html = '<label for="' . $htmlFormElementId . '">' . $label . '</label>';

        return $html;
479
480
    }

481
482
483
484
485
486
487
488
489
490
491
    /**
     * 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"]
     *           [pattern="$pattern"] [readonly="readonly"] [required="required"] [disabled="disabled"] value="$value">
     *
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
492
     * @throws UserFormException
493
     */
Carsten  Rose's avatar
Carsten Rose committed
494
    public function buildInput(array $formElement, $htmlFormElementId, $value, &$json) {
495
        $textarea = '';
496

497
        $attribute = Support::doAttribute('name', $htmlFormElementId);
498

499
        // Check for input type 'textarea'
500
        $colsRows = explode(',', $formElement['size'], 2);
501
        if (count($colsRows) === 2) {
502
            // <textarea>
503
504
            $htmlTag = '<textarea';

505
506
            $attribute .= Support::doAttribute('cols', $colsRows[0]);
            $attribute .= Support::doAttribute('rows', $colsRows[1]);
507
            $textarea = htmlentities($value) . '</textarea>';
508
509

        } else {
Carsten  Rose's avatar
Carsten Rose committed
510
511
512
513
            $htmlTag = '<input';

            $this->adjustMaxLength($formElement);

514
515
            // <input>
            if ($formElement['maxLength'] > 0) {
Carsten  Rose's avatar
Carsten Rose committed
516
517
518
                // crop string only if it's not empty (substr returns false on empty strings)
                if ($value !== '')
                    $value = substr($value, 0, $formElement['maxLength']);
519

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

526
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
527
528
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
529
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
530

Carsten  Rose's avatar
Carsten Rose committed
531
        $attribute .= $this->getAttributeMode($formElement);
532

Carsten  Rose's avatar
Carsten Rose committed
533
534
        $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']);

535
        return "$htmlTag $attribute>$textarea";
536

537
538
    }

Carsten  Rose's avatar
Carsten Rose committed
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
    /**
     * @param array $formElement
     */
    private function adjustMaxLength(array &$formElement) {

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

        if ($maxLength !== false) {
            if (is_numeric($formElement['maxLength'])) {
                if ($formElement['maxLength'] > $maxLength) {
                    $formElement['maxLength'] = $maxLength;
                }
            } else {
                $formElement['maxLength'] = $maxLength;
            }
        }
    }

558
    /**
559
560
     * Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
     *
561
     * @param $column
562
     * @return bool|int
563
564
565
566
567
568
     */
    private function getColumnSize($column) {
        $matches = array();

        $typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);

569
        // e.g.: string(64), enum('yes','no')
570
571
572
573
574
575
        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
576
577
578
579
    }

    /**
     * Builds a HTML attribute list, based on  $attributeList.
580
     *
581
582
583
584
585
586
587
     * E.g.: attributeList: [ 'type', 'autofocus' ]
     *       generates: 'type="$formElement['type']" autofocus="$formElement['autofocus']" '
     *
     * @param array $formElement
     * @param array $attributeList
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
588
    private function getAttributeList(array $formElement, array $attributeList) {
589
590
591
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
592
                $attribute .= Support::doAttribute(strtolower($item), $formElement[$item]);
593
594
595
596
597
598
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
599
     *
600
     *   type     data                      result
601
     *   -------  -----------------------   -------------------------------------------------------------------------------
602
603
     *   min|max  <min value>|<max value>   min="$attrData[0]"|max="$attrData[1]"
     *   pattern  <regexp>                  pattern="$data"
Carsten  Rose's avatar
Carsten Rose committed
604
     *   digit    -                         pattern="^[0-9]*$"
605
     *   email    -                         pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
606
     *   alnumx   -
607
     *
608
     * For 'min/max' and 'pattern' the 'data' will be injected in the attribute string via '%s'.
609
610
611
612
     *
     * @param $type
     * @param $data
     * @return string
613
     * @throws \qfq\UserFormException
614
     */
Carsten  Rose's avatar
Carsten Rose committed
615
    private function getInputCheckPattern($type, $data) {
616
617
        $attribute = '';

618
619
        if ($type === '') {
            return '';
620
        }
621

622
623
624
625
626
        switch ($type) {
            case SANITIZE_ALLOW_MIN_MAX:
            case SANITIZE_ALLOW_MIN_MAX_DATE:
                $arrData = explode("|", $data);
                if (count($arrData) != 2 || $arrData[0] == '' || $arrData[1] == '')
627
                    throw new UserFormException("Missing MIN|MAX values", ERROR_MISSING_MIN_MAX);
628

629
630
631
                $attribute = 'min="' . $arrData[0] . '" ';
                $attribute .= 'max="' . $arrData[1] . '" ';
                break;
632

633
634
635
636
637
638
639
640
641
642
            case SANITIZE_ALLOW_PATTERN:
                $attribute = 'pattern="' . $data . '" ';
                break;

            case SANITIZE_ALLOW_ALL:
                break;

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

645
646
647
648
        return $attribute;
    }

    /**
649
650
     * Set corresponding html attributes readonly/required/disabled, based on $formElement['mode'].
     *
651
652
     * @param array $formElement
     * @return string
653
     * @throws UserFormException
654
     */
Carsten  Rose's avatar
Carsten Rose committed
655
    private function getAttributeMode(array $formElement) {
656
657
658
659
660
661
        $attribute = '';

        switch ($formElement['mode']) {
            case 'show':
                break;
            case 'readonly':
662
                $attribute .= Support::doAttribute('readonly', 'readonly');
663
664
                break;
            case 'required':
665
                $attribute .= Support::doAttribute('required', 'required');
666
667
668
669
                break;
            case 'lock':
                break;
            case 'disabled':
670
                $attribute .= Support::doAttribute('disabled', 'disabled');
671
672
                break;
            default:
673
674
                // Preparation for Log, Debug
                $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);
675
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'mode', STORE_SYSTEM);
676
                throw new UserFormException("Unknown mode '" . $formElement['mode'] . "'", ERROR_UNKNOWN_MODE);
677
678
679
680
681
682
683
684
                break;
        }
        return $attribute;
    }

    /**
     * Builds HTML 'checkbox' element.
     *
685
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transferred first.
686
687
688
689
690
691
692
693
694
     *
     * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
     *         <input name="$htmlFormElementId" type="checkbox" [autofocus="autofocus"]
     *            [readonly="readonly"] [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
     * @return string
695
     * @throws UserFormException
696
     */
Carsten  Rose's avatar
Carsten Rose committed
697
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value, &$json) {
698
699
700
701
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
702
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
703
704
705

        // Get fallback, if 'checkBoxMode' is not defined:
        if (!isset($formElement['checkBoxMode'])) {
706
            // This fallback is problematic if 'set' or 'enum' has 2 : defaults to single but maybe multi is meant.
707
708
709
710
711
712
            $formElement['checkBoxMode'] = (count($itemKey) > 2) ? 'multi' : 'single';
        }

        if ($formElement['checkBoxMode'] === 'multi') {
            $htmlFormElementId .= '[]';
        } else {
713
            // Fill meaningfull defaults to parameter: checked|unchecked  (CHECKBOX_VALUE_CHECKED|CHECKBOX_VALUE_UNCHECKED)
714
715
716
            $this->prepareCheckboxCheckedUncheckedValue($itemKey, $formElement);
        }

Carsten  Rose's avatar
Carsten Rose committed
717
        $attributeBase = $this->getAttributeMode($formElement);
718
        $attributeBase .= Support::doAttribute('type', $formElement['type']);
719
720
721
722
723
724
725
726
727

        switch ($formElement['checkBoxMode']) {
            case 'single':
                $html = $this->buildCheckboxSingle($formElement, $htmlFormElementId, $attributeBase, $value);
                break;
            case 'multi';
                $html = $this->buildCheckboxMulti($formElement, $htmlFormElementId, $attributeBase, $value, $itemKey, $itemValue);
                break;
            default:
728
                throw new UserFormException('checkBoxMode: \'' . $formElement['checkBoxMode'] . '\' is unknown.', ERROR_CHECKBOXMODE_UNKNOWN);
729
        }
730

Carsten  Rose's avatar
Carsten Rose committed
731
        $json = $this->getJsonElementUpdate($htmlFormElementId, $value, $formElement['mode']);
732
//        return Support::wrapTag('<div class="checkbox">', $html, true);
733
734
735
        return $html;
    }

736
    /**
737
738
739
740
741
     * Look for key/value list (in this order, first match counts) in
     *  a) `sql1`
     *  b) `parameter:itemList`
     *  c) table.column definition
     *
742
     * Copies the found keys to &$itemKey and the values to &$itemValue
743
     * If there are no &$itemKey, copy &$itemValue to &$itemKey.
744
745
746
747
     *
     * @param array $formElement
     * @param $itemKey
     * @param $itemValue
748
     * @throws CodeException
749
     * @throws \qfq\UserFormException
750
     */
751
    public function getKeyValueListFromSqlEnumSpec(array $formElement, &$itemKey, &$itemValue) {
752
753
754
755
        $fieldType = '';
        $itemKey = array();
        $itemValue = array();

756
757
758
        if (count($formElement) < 20)
            throw new CodeException("Invalid (none or to small) Formelement", ERROR_MISSING_FORMELEMENT);

759
        // Call getItemsForEnumOrSet() only if there a corresponding column really exist.
760
        if (false !== $this->store->getVar($formElement['name'], STORE_TABLE_COLUMN_TYPES)) {
761
762
            $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);
        }
763
764

        if (is_array($formElement['sql1'])) {
765
766
767
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
768

769
770
771
772
                // 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]);
                }
773

774
775
776
777
778
779
                $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]);
                }
780
            }
781
        } elseif (isset($formElement['itemList']) && strlen($formElement['itemList']) > 0) {
782
            $arr = KeyValueStringParser::parse($formElement['itemList'], ':', ',', KVP_IF_VALUE_EMPTY_COPY_KEY);
783
784
            $itemValue = array_values($arr);
            $itemKey = array_keys($arr);
785
        } elseif ($fieldType === 'enum' || $fieldType === 'set') {
Carsten  Rose's avatar
Carsten Rose committed
786
            // already done at the beginning with '$this->getItemsForEnumOrSet($formElement['name'], $fieldType);'
787
        } else {
788
            throw new UserFormException("Missing definition (- nothing found in 'sql1', 'parameter:itemValues', 'enum-' or 'set-definition'", ERROR_MISSING_ITEM_VALUES);
789
790
791
792
793
        }

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
794
795
796
797
798
799
800
801
802
803

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

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

Carsten  Rose's avatar
Carsten Rose committed
805
806
        if (isset($formElement['emptyHide'])) {
            if (isset($itemValue['']))
807
                unset($itemValue['']);
Carsten  Rose's avatar
Carsten Rose committed
808
            if (isset($itemKey['']))
809
810
811
                unset($itemKey['']);

        }
812
813
814
    }

    /**
815
816
     * Get the attribute definition list of an enum or set column. For strings, get the default value. Return elements as an array.
     *
817
818
819
     * @param $column
     * @param $fieldType
     * @return array
820
     * @throws UserFormException
821
822
823
824
825
826
827
     */
    private function getItemsForEnumOrSet($column, &$fieldType) {

        // Get column definition
        $fieldTypeDefinition = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);

        if ($fieldTypeDefinition === false) {
828
            throw new UserFormException("Column '$column' unknown in table '" . $this->formSpec['tableName'] . "'", ERROR_DB_UNKNOWN_COLUMN);
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
        }

        $length = strlen($fieldTypeDefinition);

        // enum('...   set('
        switch (substr($fieldTypeDefinition, 0, 4)) {
            case 'enum':
                $startPosition = 5;
                break;
            case 'set(':
                $startPosition = 4;
                break;
            default:
                $fieldType = 'string';
                return array();
        }

        // enum('a','b','c', ...)   >> [ 'a', 'b', 'c', ... ]
        // set('a','b','c', ...)   >> [ 'a', 'b', 'c', ... ]
        $items = OnArray::trimArray(explode(',', substr($fieldTypeDefinition, $startPosition, $length - $startPosition - 1)), "'");
        $fieldType = substr($fieldTypeDefinition, 0, $startPosition - 1);

        return $items;
    }

    /**
     * For CheckBox's with only one checkbox: if no parameter:checked|unchecked is defined, take defaults:
856
     *
857
858
859
860
861
     *    checked: first Element in $itemKey
     *  unchecked: ''
     *
     * @param array $itemKey
     * @param array $formElement
862
     * @throws UserFormException
863
864
865
     */
    private function prepareCheckboxCheckedUncheckedValue(array $itemKey, array &$formElement) {

866
        if (!isset($formElement[CHECKBOX_VALUE_CHECKED])) {
867
868
            if (isset($itemKey[0])) {
                // First element in $itemKey list
869
                $formElement[CHECKBOX_VALUE_CHECKED] = $itemKey[0];
870
871
            } else {
                // Take column default value
872
                $formElement[CHECKBOX_VALUE_CHECKED] = $this->store->getVar($formElement['name'], STORE_TABLE_DEFAULT);
873
874
875
876
            }
        }

        // unchecked
877
        if (!isset($formElement[CHECKBOX_VALUE_UNCHECKED])) {
878
            if (isset($itemKey[1])) {
879
                $formElement[CHECKBOX_VALUE_UNCHECKED] = ($itemKey[0] === $formElement['checked']) ? $itemKey[1] : $itemKey[0];
880
            } else {
881
                $formElement[CHECKBOX_VALUE_UNCHECKED] = '';
882
883
884
            }
        }

885
        if ($formElement[CHECKBOX_VALUE_CHECKED] === $formElement[CHECKBOX_VALUE_UNCHECKED]) {
886
            throw new UserFormException('FormElement: type=checkbox - checked and unchecked can\'t be the same: ' . $formElement[CHECKBOX_VALUE_CHECKED], ERROR_CHECKBOX_EQUAL);
887
888
889
890
891
        }

    }

    /**
892
893
     * Build a Checkbox based on two values.
     *
894
895
896
897
898
899
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attribute
     * @param $value
     * @return string
     */
900
    public function buildCheckboxSingle(array $formElement, $htmlFormElementId, $attribute, $value) {
901
        $html = '';
902

903
904
        $attribute .= Support::doAttribute('name', $htmlFormElementId);
        $attribute .= Support::doAttribute('value', $formElement['checked'], false);
905
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
906
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
Carsten  Rose's avatar
Carsten Rose committed
907

908
        if ($formElement['checked'] === $value) {
909
            $attribute .= Support::doAttribute('checked', 'checked');
910
911
        }

Carsten  Rose's avatar
Carsten Rose committed
912
        $attribute .= $this->getAttributeList($formElement, ['autofocus']);
913

914
        $html = $this->buildNativeHidden($htmlFormElementId, $formElement['unchecked']);
915
916
917
918
919
920

        $html .= '<input ' . $attribute . '>';
        if (isset($formElement['label2'])) {
            $html .= $formElement['label2'];
        }

921
922
923
        $html = Support::wrapTag("<label>", $html, true);
        $html = Support::wrapTag("<div class='checkbox'>", $html, true);

924
925
926
        return $html;
    }

927
    /**
928
929
930
931
932
     * Build as many Checkboxes as items.
     *
     * Layout: The Bootstrap Layout needs very special setup, the checkboxes are wrapped differently with <div class=checkbox>
     *         depending of if they aligned horizontal or vertical.
     *
933
934
935
936
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attributeBase
     * @param $value
937
938
     * @param array $itemKey
     * @param array $itemValue
939
940
     * @return string
     */
941
    public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue) {
942
        // Defines which of the checkboxes will be checked.
943
        $values = explode(',', $value);
944

945
946
        $attributeBase .= Support::doAttribute('name', $htmlFormElementId);
        $attributeBase .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
947

948
        $html = $this->buildNativeHidden($htmlFormElementId, '');
949

Carsten  Rose's avatar
Carsten Rose committed
950
        $orientation = (isset($formElement[CHECKBOX_ORIENTATION]) && $formElement[CHECKBOX_ORIENTATION] === 'vertical') ? '' : 'checkbox-inline';
951

952
        $flagFirst = true;
953
954
        for ($ii = 0, $jj = 1; $ii < count($itemKey); $ii++, $jj++) {

955
            $attribute = $attributeBase;
956
957

            // Do this only the first round.
958
959
960
            if ($flagFirst) {
                $flagFirst = false;
                if (isset($formElement['autofocus']))
961
                    $attribute .= Support::doAttribute('autofocus', $formElement['autofocus']);
962
            }
963

964
            $attribute .= Support::doAttribute('value', $itemKey[$ii]);
965
966
967

            // Check if the given key is found in field.
            if (false !== array_search($itemKey[$ii], $values)) {
968
                $attribute .= Support::doAttribute('checked', 'checked');
969
            }
970

971
972
973
974
            $htmlCheckbox = '<input ' . $attribute . '>';
            $htmlCheckbox .= $itemValue[$ii];

            $htmlCheckbox = Support::wrapTag("<label class='$orientation'>", $htmlCheckbox, true);
Carsten  Rose's avatar
Carsten Rose committed
975
            if (isset($formElement[CHECKBOX_ORIENTATION]) && $formElement[CHECKBOX_ORIENTATION] === 'vertical')
Carsten  Rose's avatar