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
343
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec['tableName'] . " WHERE id = ?", ROW_EXPECT_1, array($recordId));
            $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
            // Get default value
379
            $value = ($formElement['value'] === '') ? $this->store->getVar($formElement['name'], $storeUse,
380
                $formElement['checkType']) : $formElement['value'];
Carsten  Rose's avatar
Carsten Rose committed
381

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

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

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

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

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

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

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

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

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

429
430
431
        return $html;
    }

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

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

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

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

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

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

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

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

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

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

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

        return $json;
    }

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

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

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

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

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

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

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

        return $url;
    }

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

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

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

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

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

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

        return $html;
580
581
    }

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

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

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

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

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

            $this->adjustMaxLength($formElement);

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

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

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

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

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

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

641
642
    }

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

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

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

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

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

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

        return false;
715
716
    }

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

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

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

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

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

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

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

            case SANITIZE_ALLOW_ALL:
                break;

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

807
808
809
810
        return $attribute;
    }

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

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

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

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

840
841
842
        return $attribute;
    }

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

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

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

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

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

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

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

902
903
904
        return $html;
    }

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

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

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

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

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

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

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