AbstractBuildForm.php 55.7 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
328
329
            // Log / Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, $fe['name'] . ' / ' . $fe['id'], STORE_SYSTEM);

            // 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
673
674
                break;
            default:
                $this->store->setVar(SYSTEM_FORM_ELEMENT, $formElement['name'] . ' / ' . $formElement['id'], STORE_SYSTEM);
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'mode', STORE_SYSTEM);
675
                throw new UserFormException("Unknown mode '" . $formElement['mode'] . "'", ERROR_UNKNOWN_MODE);
676
677
678
679
680
681
682
683
                break;
        }
        return $attribute;
    }

    /**
     * Builds HTML 'checkbox' element.
     *
684
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transferred first.
685
686
687
688
689
690
691
692
693
     *
     * 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
694
     * @throws UserFormException
695
     */
Carsten  Rose's avatar
Carsten Rose committed
696
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value, &$json) {
697
698
699
700
        $itemKey = array();
        $itemValue = array();

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

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

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

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

        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:
727
                throw new UserFormException('checkBoxMode: \'' . $formElement['checkBoxMode'] . '\' is unknown.', ERROR_CHECKBOXMODE_UNKNOWN);
728
        }
729

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

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

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

758
759
760
        $itemValue = $this->getItemsForEnumOrSet($formElement['name'], $fieldType);

        if (is_array($formElement['sql1'])) {
761
762
763
            if (count($formElement['sql1']) > 0) {
                $keys = array_keys($formElement['sql1'][0]);
                $itemKey = array_column($formElement['sql1'], 'id');
764

765
766
767
768
                // 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]);
                }
769

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

        if (count($itemKey) === 0) {
            $itemKey = $itemValue;
        }
790
791
792
793
794
795
796
797
798
799

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

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

Carsten  Rose's avatar
Carsten Rose committed
801
802
        if (isset($formElement['emptyHide'])) {
            if (isset($itemValue['']))
803
                unset($itemValue['']);
Carsten  Rose's avatar
Carsten Rose committed
804
            if (isset($itemKey['']))
805
806
807
                unset($itemKey['']);

        }
808
809
810
    }

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

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

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

        $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:
852
     *
853
854
855
856
857
     *    checked: first Element in $itemKey
     *  unchecked: ''
     *
     * @param array $itemKey
     * @param array $formElement
858
     * @throws UserFormException
859
860
861
     */
    private function prepareCheckboxCheckedUncheckedValue(array $itemKey, array &$formElement) {

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

        // unchecked
873
        if (!isset($formElement[CHECKBOX_VALUE_UNCHECKED])) {
874
            if (isset($itemKey[1])) {
875
                $formElement[CHECKBOX_VALUE_UNCHECKED] = ($itemKey[0] === $formElement['checked']) ? $itemKey[1] : $itemKey[0];
876
            } else {
877
                $formElement[CHECKBOX_VALUE_UNCHECKED] = '';
878
879
880
            }
        }

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

    }

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

899
900
901
        $attribute .= Support::doAttribute('name', $htmlFormElementId);
        $attribute .= Support::doAttribute('value', $formElement['checked'], false);
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
Carsten  Rose's avatar
Carsten Rose committed
902

903
        if ($formElement['checked'] === $value) {
904
            $attribute .= Support::doAttribute('checked', 'checked');
905
906
        }

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

909
        $html = $this->buildNativeHidden($htmlFormElementId, $formElement['unchecked']);
910
911
912
913
914
915

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

916
917
918
        $html = Support::wrapTag("<label>", $html, true);
        $html = Support::wrapTag("<div class='checkbox'>", $html, true);

919
920
921
        return $html;
    }

922
    /**
923
924
925
926
927
     * 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.
     *
928
929
930
931
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $attributeBase
     * @param $value
932
933
     * @param array $itemKey
     * @param array $itemValue
934
935
     * @return string
     */
936
    public function buildCheckboxMulti(array $formElement, $htmlFormElementId, $attributeBase, $value, array $itemKey, array $itemValue) {
937
        // Defines which of the checkboxes will be checked.
938
        $values = explode(',', $value);
939

940
941
        $attributeBase .= Support::doAttribute('name', $htmlFormElementId);
        $attributeBase .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
942

943
        $html = $this->buildNativeHidden($htmlFormElementId, '');
944

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

947
        $flagFirst = true;
948
949
        for ($ii = 0, $jj = 1; $ii < count($itemKey); $ii++, $jj++) {

950
            $attribute = $attributeBase;
951
952

            // Do this only the first round.
953
954
955
            if ($flagFirst) {
                $flagFirst = false;
                if (isset($formElement['autofocus']))
956
                    $attribute .= Support::doAttribute('autofocus', $formElement['autofocus']);
957
            }
958

959
            $attribute .= Support::doAttribute('value', $itemKey[$ii]);
960
961
962

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

966
967
968
969
            $htmlCheckbox = '<input ' . $attribute . '>';
            $htmlCheckbox .= $itemValue[$ii];

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

            $html .= $htmlCheckbox;
974
        }
975

Carsten  Rose's avatar
Carsten Rose committed
976
        if (isset($formElement[CHECKBOX_ORIENTATION]) && $formElement[CHECKBOX_ORIENTATION] !== 'vertical')
977
978
            $html = Support::wrapTag("<div class='checkbox'>", $html, true);

979
980
981
        return $html;
    }