AbstractBuildForm.php 89.1 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
require_once(__DIR__ . '/../qfq/report/Link.php');
25

26
27

/**
Carsten  Rose's avatar
Carsten Rose committed
28
29
 * Class AbstractBuildForm
 * @package qfq
30
 */
31
abstract class AbstractBuildForm {
32
33
34
35
36
    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();
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
46
47
48
49
50
51
    /**
     * @var Store
     */
    protected $store = null;
    /**
     * @var Evaluate
     */
    protected $evaluate = null;
52
53
54
    /**
     * @var string
     */
55
    private $formId = null;
56
57
58
59
60
    /**
     * @var Sip
     */
    private $sip = null;

61
62
63
64
65
66
67
    /**
     * AbstractBuildForm constructor.
     *
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
     */
68
69
70
71
72
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) {
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
73
        $this->db = new Database();
Carsten  Rose's avatar
Carsten Rose committed
74
        $this->evaluate = new Evaluate($this->store, $this->db);
Carsten  Rose's avatar
Carsten Rose committed
75
        $this->showDebugInfo = ($this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM) === 'yes');
76

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

79
        // render mode specific
80
        $this->fillWrap();
81
82
83

        $this->buildElementFunctionName = [
            'checkbox' => 'Checkbox',
84
85
            'date' => 'DateTime',
            'datetime' => 'DateTime',
86
87
            'dateJQW' => 'DateJQW',
            'datetimeJQW' => 'DateJQW',
88
89
            'email' => 'Input',
            'gridJQW' => 'GridJQW',
90
            FE_TYPE_EXTRA => 'Extra',
91
            'text' => 'Input',
Carsten  Rose's avatar
Carsten Rose committed
92
            'editor' => 'Editor',
93
            'time' => 'DateTime',
94
95
96
97
            'note' => 'Note',
            'password' => 'Input',
            'radio' => 'Radio',
            'select' => 'Select',
98
            'subrecord' => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
99
            'upload' => 'File',
100
101
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
102
103
        ];

104
105
        $this->buildRowName = [
            'checkbox' => 'Native',
106
107
            'date' => 'Native',
            'datetime' => 'Native',
108
109
110
111
            'dateJQW' => 'Native',
            'datetimeJQW' => 'Native',
            'email' => 'Native',
            'gridJQW' => 'Native',
112
            FE_TYPE_EXTRA => 'Native',
113
            'text' => 'Native',
Carsten  Rose's avatar
Carsten Rose committed
114
            'editor' => 'Native',
115
            'time' => 'Native',
116
117
118
119
120
121
122
123
124
125
            'note' => 'Native',
            'password' => 'Native',
            'radio' => 'Native',
            'select' => 'Native',
            'subrecord' => 'Subrecord',
            'upload' => 'Native',
            'fieldset' => 'Fieldset',
            'pill' => 'Pill'
        ];

126
127
128
        $this->symbol[SYMBOL_EDIT] = "<span class='glyphicon " . GLYPH_ICON_EDIT . "'></span>";
        $this->symbol[SYMBOL_NEW] = "<span class='glyphicon " . GLYPH_ICON_NEW . "'></span>";
        $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon " . GLYPH_ICON_DELETE . "'></span>";
129

130
        $this->inputCheckPattern = Sanitize::inputCheckPatternArray();
131
132
    }

133
134
    abstract public function fillWrap();

135
    /**
136
     * Builds complete form. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
137
     *
138
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
Carsten  Rose's avatar
Carsten Rose committed
139
     * @return string|array   $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all formElement.dynamicUpdate-yes  values/states
140
141
     * @throws CodeException
     * @throws DbException
142
     * @throws \qfq\UserFormException
143
     */
144
    public function process($mode, $htmlElementNameIdZero = false) {
Carsten  Rose's avatar
Carsten Rose committed
145
146
147
148
149
        $htmlHead = '';
        $htmlTail = '';
        $htmlSubrecords = '';
        $htmlElements = '';
        $json = array();
150
151
152
153
154
155
156
157

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

        if ($mode === FORM_SAVE) {
            $modeCollectFe = FLAG_ALL;
            $storeUse = STORE_RECORD . STORE_TABLE_DEFAULT;
        }
158

159
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
160
161
162
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
        }
163

164
        $filter = $this->getProcessFilter();
165

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

168
169
            $parentRecords = $this->db->sql($this->formSpec['multiSql']);
            foreach ($parentRecords as $row) {
170
                $this->store->setVarArray($row, STORE_PARENT_RECORD, true);
Carsten  Rose's avatar
Carsten Rose committed
171
                $jsonTmp = array();
172
                $htmlElements = $this->elements($row['_id'], $filter, 0, $jsonTmp, $modeCollectFe);
Carsten  Rose's avatar
Carsten Rose committed
173
                $json[] = $jsonTmp;
174
175
            }
        } else {
176
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
177
            $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode);
178
        }
179
180
181
182
183
184
185

        // <form>
        if ($mode === FORM_LOAD) {
            $htmlTail = $this->tail();
            $htmlSubrecords = $this->doSubrecords();
        }

186
187
188
        $htmlSip = $this->buildHiddenSip($json);

        return ($mode === FORM_LOAD) ? $htmlHead . $htmlElements . $htmlSip . $htmlTail . $htmlSubrecords : $json;
189
190
    }

191
    /**
192
     * Builds the head area of the form.
193
     *
194
     * @return string
195
     */
196
197
    public function head() {
        $html = '';
198

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

201
202
203
        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
        $formEditUrl = $this->createFormEditUrl();
204

205
        $html .= "<p><a " . Support::doAttribute('href', $formEditUrl) . ">Edit</a> <small>[$sipParamString]</small></p>";
206

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

209
210
211
        $html .= $this->getFormTag();

        return $html;
212
213
    }

214
    /**
215
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of the SIP.
216
     *
217
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:..., ...]</small>
218
     */
219
    public function createFormEditUrl() {
220

Carsten  Rose's avatar
Carsten Rose committed
221
        if (!$this->showDebugInfo) {
222
223
            return '';
        }
224

225
226
227
228
229
        $queryStringArray = [
            'id' => $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3),
            'form' => 'form',
            'r' => $this->formSpec['id']
        ];
230

231
        $queryString = Support::arrayToQueryString($queryStringArray);
232

233
234
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
235

236
        return $url;
237
238
239
    }

    /**
240
241
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
242
243
244
245
246
247
     * @param $item
     * @param $value
     * @param bool|false $flagOmitEmpty
     * @return string
     */
    public function wrapItem($item, $value, $flagOmitEmpty = false) {
248
249

        if ($flagOmitEmpty && $value === "") {
250
            return '';
251
252
        }

253
254
255
256
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
257
     * Returns '<form ...>'-tag with various attributes.
258
259
260
261
262
263
264
265
266
267
268
269
270
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

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

    /**
     * Build an assoc array with standard form attributes.
     *
271
     * @return array
272
273
274
     */
    public function getFormTagAtrributes() {

275
        $attribute['id'] = $this->getFormId();
276
277
278
279
280
281
282
283
284
285
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

286
    /**
Carsten  Rose's avatar
Carsten Rose committed
287
288
     * Return a uniq form id
     *
289
290
291
292
293
294
295
296
297
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
        return $this->formId;
    }

298
299
300
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
301
302
     * Notice: the SIP will be transferred as POST Parameter.
     *
303
304
305
306
307
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {

308
        return API_DIR . '/save.php';
309
310
311
312
313
314
315
316
317
318
319
320
    }

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

321
        $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"');
322
323
324
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
325

326
    abstract public function getProcessFilter();
327
328

    /**
329
330
     * Process all FormElements: build corresponding HTML code. Collect and return all HTML code.
     *
331
     * @param $recordId
332
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
333
     * @param int $feIdContainer
334
335
336
337
     * @param array $json
     * @param string $modeCollectFe
     * @param bool $htmlElementNameIdZero
     * @param string $storeUse
338
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
339
     * @return string
340
341
     * @throws CodeException
     * @throws DbException
342
     * @throws \qfq\UserFormException
343
     */
344
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, array &$json,
345
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false, $storeUse = STORE_USE_DEFAULT, $mode = FORM_LOAD) {
346
        $html = '';
347
        $flagOutput = false;
348
349
350
        // The following 'FormElement.parameter' will never be used during load (fe.type='upload').
        $skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, F_FE_PARAMETER];

351
352

        // get current data record
353
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
354
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec[F_TABLE_NAME] . " WHERE id = ?", ROW_EXPECT_1, array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$recordId' from table '" . $this->formSpec[F_TABLE_NAME] . "'.");
355
            $this->store->setVarArray($row, STORE_RECORD);
356
        }
357

358
359
        $this->checkAutoFocus();

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

363
364
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
Carsten  Rose's avatar
Carsten Rose committed
365
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe['dynamicUpdate'] === 'no')
366
367
368
369
            ) {
                continue; // skip this FE
            }

370
371
            $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA);

372
373
            $debugStack = array();

374
375
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
376

377
378
            // for Upload FormElements, it's necessary to precalculate an optional given 'slaveId'.
            if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) {
379
                Support::setIfNotSet($fe, FE_SLAVE_ID);
380
381
                $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
382
383
            }

384
            // evaluate current FormElement
385
            $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack);
386

387
388
389
            // Some Defaults
            $formElement = Support::setFeDefaults($formElement);

390
391
392
393
394
395
396
397
398
            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);
            }
399

400
401
            //In case the current element is a 'RETYPE' element: take the element name of the source FormElement. Needed in the next row to retrieve the default value.
            $name = (isset($formElement[FE_RETYPE_SOURCE_NAME])) ? $formElement[FE_RETYPE_SOURCE_NAME] : $formElement[FE_NAME];
402

403
            // Get default value
404
            $value = ($formElement[FE_VALUE] === '') ? $this->store->getVar($name, $storeUse,
405
                $formElement['checkType']) : $formElement[FE_VALUE];
Carsten  Rose's avatar
Carsten Rose committed
406

407
408
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
409
            $htmlFormElementId = HelperFormElement::buildFormElementName($formElement[FE_NAME], ($htmlElementNameIdZero) ? 0 : $recordId);
410

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

Carsten  Rose's avatar
Carsten Rose committed
414
            $jsonElement = array();
415
            // Render pure element
416
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementId, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
417

418
419
//            $fake0 = $fe['dynamicUpdate'];
//            $fake1 = $formElement['dynamicUpdate'];
Carsten  Rose's avatar
Carsten Rose committed
420
421
422
423
424
425
426
427

            // 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
428
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe['dynamicUpdate'] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
429
430
431
432
433
434
                    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
435
436
                }
            }
437

438
439
440
441
442
            if ($flagOutput) {
                // debugStack as Tooltip
                if ($this->showDebugInfo && count($debugStack) > 0) {
                    $elementHtml = Support::appendTooltip($elementHtml, implode("\n", $debugStack));
                }
443

444
445
                // Construct Marshaller Name: buildRow
                $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
446

447
448
                $html .= $this->$buildRowName($formElement, $elementHtml, $htmlFormElementId);
            }
449
        }
450

451
452
453
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

454
455
456
        return $html;
    }

457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
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
    /**
     * Check if there is an explicit 'autofocus' definition in at least one FE.
     * Found: do nothing, it will be rendered at the correct position.
     * Not found: set 'autofocus' on the first FE.
     *
     * Accepted misbehaviour on forms with pills: if there is at least one editable element on the first pill,
     *   the other pills are not checked - independent if there was a definition on the first pill or not.
     *   Reason: checks happens per pill - if there is no explizit definition on the first pill, take the first editable
     *           element of that pill.
     */
    private function checkAutoFocus() {
        static $found = false;
        $idx = false;

        if ($found) {
            return;
        }

        // Search if there is an explicit autofocus definition.
        for ($i = 0; $i < count($this->feSpecNative); ++$i) {
            // Only check native elements which will be shown
            if ($this->feSpecNative[$i][FE_CLASS] == FE_CLASS_NATIVE &&
                ($this->feSpecNative[$i][FE_MODE] == FE_MODE_SHOW || $this->feSpecNative[$i][FE_MODE] == FE_MODE_REQUIRED)
            ) {
                // Check if there is an explicit definition.
                if (isset($this->feSpecNative[$i][FE_AUTOFOCUS])) {
                    if ($this->feSpecNative[$i][FE_AUTOFOCUS] == '' || $this->feSpecNative[$i][FE_AUTOFOCUS] == '1') {
                        $this->feSpecNative[$i][FE_AUTOFOCUS] = '1'; // fix to '=1'
                    } else {
                        unset($this->feSpecNative[$i][FE_AUTOFOCUS]);
                    }
                    $found = true;
                    return;
                }

                if ($idx === false) {
                    $idx = $i;
                }
            }
        }

        // No explicit definition found: take the first found editable element.
        if ($idx !== false) {
            $found = true;
            // No explicit definition found: set autofocus.
            $this->feSpecNative[$idx][FE_AUTOFOCUS] = '1';
        }
    }

506
507
    abstract public function fillWrapLabelInputNote($label, $input, $note);

508
509
510
511
    abstract public function tail();

    abstract public function doSubrecords();

512
513
514
    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
515
     * @param array $json
516
517
518
519
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
520
    public function buildHiddenSip(array &$json) {
521

522
        $sipArray = $this->store->getStore(STORE_SIP);
523
524

        // do not include system vars
525
526
527
528
529
530
531
532
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

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

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

533
        $json[] = $this->getJsonElementUpdate(CLIENT_SIP, $sipValue, FE_MODE_SHOW);
534
535
536
537
538

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

    /**
Carsten  Rose's avatar
Carsten Rose committed
539
540
     * Create an array with standard elements and add 'form-element', 'value'.
     *
541
542
     * @param $htmlFormElementId
     * @param string|array $value
543
     * @param string $feMode disabled|readonly|''
544
545
     * @return array
     */
546
    private function getJsonElementUpdate($htmlFormElementId, $value, $feMode) {
547

548
        $json = $this->getJsonFeMode($feMode);
549
550
551
552
553
554
555

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

        return $json;
    }

556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
    /**
     * 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'];
    }

    /**
Carsten  Rose's avatar
Carsten Rose committed
571
572
     * Depending of $feMode set variables $hidden, $disabled, $required to 'yes' or 'no'.
     *
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
     * @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:
591
                $disabled = 'yes';  // convert the UI status 'readonly' to the HTML/CSS status 'disabled'.
592
593
594
595
596
597
598
599
600
601
                break;
            case FE_MODE_HIDDEN:
                $hidden = 'yes';
                break;
            default:
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
                break;
        }
    }

602
603
604
605
606
607
608
609
610
611
612
    /**
     * 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
613
614
615
616
    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @throws CodeException
617
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
618
619
620
621
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
        $urlParam = $this->store->getStore(STORE_SIP);
        $urlParam[SIP_RECORD_ID] = 0;
622

Carsten  Rose's avatar
Carsten Rose committed
623
624
        unset($urlParam[SIP_SIP]);
        unset($urlParam[SIP_URLPARAM]);
625
626

        Support::appendTypo3ParameterToArray($urlParam);
Carsten  Rose's avatar
Carsten Rose committed
627
628
629
630
631

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

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

632
        if ($this->showDebugInfo) {
633
            //TODO: missing decoding of SIP
634
635
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }
Carsten  Rose's avatar
Carsten Rose committed
636
637
638
639

        return $url;
    }

640
    abstract public function buildRowNative(array $formElement, $htmlElement, $htmlFormElementId);
641

642
    abstract public function buildRowPill(array $formElement, $elementHtml);
643

644
    abstract public function buildRowFieldset(array $formElement, $elementHtml);
645

646
    abstract public function buildRowSubrecord(array $formElement, $elementHtml);
647

648
    /**
649
650
     * Builds a label, typically for an html-'<input>'-element.
     *
651
652
     * @param string $htmlFormElementId
     * @param string $label
653
654
     * @return string
     */
655
    public function buildLabel($htmlFormElementId, $label) {
656
657
658
659
        $attributes = Support::doAttribute('for', $htmlFormElementId);
        $attributes .= Support::doAttribute('class', 'control-label');

        $html = Support::wrapTag("<label $attributes>", $label);
660
661

        return $html;
662
663
    }

664
665
666
667
    /**
     * 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
668
     *           [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
669
670
671
672
673
     *
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
674
     * @param array $json
675
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
676
     * @return string
677
     * @throws \qfq\UserFormException
678
     */
679
    public function buildInput(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) {
680
        $textarea = '';
681

682
        $attribute = Support::doAttribute('name', $htmlFormElementId);
683
        $attribute .= Support::doAttribute('class', 'form-control');
684

685
        if (isset($formElement[FE_RETYPE_SOURCE_NAME])) {
686
687
            $htmlFormElementIdPrimary = str_replace(RETYPE_FE_NAME_EXTENSION, '', $htmlFormElementId);
            $attribute .= Support::doAttribute('data-match', '[name=' . str_replace(':', '\\:', $htmlFormElementIdPrimary) . ']');
688
689
        }

690
        // Check for input type 'textarea'
691
        $colsRows = explode(',', $formElement['size'], 2);
692
        if (count($colsRows) === 2) {
693
            // <textarea>
694
695
            $htmlTag = '<textarea';

696
697
            $attribute .= Support::doAttribute('cols', $colsRows[0]);
            $attribute .= Support::doAttribute('rows', $colsRows[1]);
698
            $textarea = htmlentities($value) . '</textarea>';
699
700

        } else {
Carsten  Rose's avatar
Carsten Rose committed
701
702
703
704
            $htmlTag = '<input';

            $this->adjustMaxLength($formElement);

705
            if ($formElement['maxLength'] > 0 && $value !== '') {
Carsten  Rose's avatar
Carsten Rose committed
706
                // crop string only if it's not empty (substr returns false on empty strings)
707
                $value = substr($value, 0, $formElement['maxLength']);
708
            }
709
710
711
712

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

715
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
716
        $attribute .= $this->getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR]);
717
718
        $attribute .= Support::doAttribute('data-load', ($formElement['dynamicUpdate'] === 'yes') ? 'data-load' : '');
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
719
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
720

721
        $attribute .= $this->getAttributeFeMode($formElement[FE_MODE]);
722

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

725
        return "$htmlTag $attribute>$textarea" . $this->getHelpBlock();
726

727
728
    }

Carsten  Rose's avatar
Carsten Rose committed
729
    /**
Carsten  Rose's avatar
Carsten Rose committed
730
731
     * Calculates the maxlength of an input field, based on formElement type, formElement user definition and table.field definition.
     *
Carsten  Rose's avatar
Carsten Rose committed
732
733
734
735
736
737
738
     * @param array $formElement
     */
    private function adjustMaxLength(array &$formElement) {

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

739
        switch ($formElement[FE_TYPE]) {
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
            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
760
        if ($maxLength !== false) {
761
            if (is_numeric($formElement['maxLength']) && $formElement['maxLength'] != 0) {
Carsten  Rose's avatar
Carsten Rose committed
762
763
764
765
766
767
768
769
770
                if ($formElement['maxLength'] > $maxLength) {
                    $formElement['maxLength'] = $maxLength;
                }
            } else {
                $formElement['maxLength'] = $maxLength;
            }
        }
    }

771
    /**
772
773
     * Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
     *
774
     * @param $column
775
     * @return bool|int  a) 'false' if there is no length definition, b) length definition, c) date|time|datetime|timestamp use hardcoded length
776
777
778
779
780
     */
    private function getColumnSize($column) {
        $matches = array();

        $typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);
781
782
783
784
785
786
787
788
789
        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:
790
791
792
                if (substr($typeSpec, 0, 4) === 'set(' || substr($typeSpec, 0, 5) === 'enum(') {
                    return $this->maxLengthSetEnum($typeSpec);
                }
793
794
                break;
        }
795

796
        // e.g.: string(64) >> 64, enum('yes','no') >> false
797
798
799
800
801
802
        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
803
804
    }

805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
    /**
     * 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;
    }

828
829
    /**
     * Builds a HTML attribute list, based on  $attributeList.
830
     *
831
     * E.g.: attributeList: [ 'type', 'autofocus' ]
832
     *       generates: 'type="$formElement[FE_TYPE]" autofocus="$formElement['autofocus']" '
833
834
835
     *
     * @param array $formElement
     * @param array $attributeList
836
     * @param bool $flagOmitEmpty
837
838
     * @return string
     */
839
    private function getAttributeList(array $formElement, array $attributeList, $flagOmitEmpty = true) {
840
841
842
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
843
                $attribute .= Support::doAttribute(strtolower($item), $formElement[$item], $flagOmitEmpty);
844
845
846
847
848
849
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
850
     *
851
     *   type     data                      result
852
     *   -------  -----------------------   -------------------------------------------------------------------------------
853
854
     *   min|max  <min value>|<max value>   min="$attrData[0]"|max="$attrData[1]"
     *   pattern  <regexp>                  pattern="$data"
Carsten  Rose's avatar
Carsten Rose committed
855
     *   digit    -                         pattern="^[0-9]*$"
856
     *   email    -                         pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
857
     *   alnumx   -
858
     *
859
     * For 'min/max' and 'pattern' the 'data' will be injected in the attribute string via '%s'.
860
861
862
863
     *
     * @param $type
     * @param $data
     * @return string
864
     * @throws \qfq\UserFormException
865
     */
Carsten  Rose's avatar
Carsten Rose committed
866
    private function getInputCheckPattern($type, $data) {
867
868
        $attribute = '';

869
870
        if ($type === '') {
            return '';
871
        }
872

873
874
875
876
877
        switch ($type) {
            case SANITIZE_ALLOW_MIN_MAX:
            case SANITIZE_ALLOW_MIN_MAX_DATE:
                $arrData = explode("|", $data);
                if (count($arrData) != 2 || $arrData[0] == '' || $arrData[1] == '')
878
                    throw new UserFormException("Missing MIN|MAX values", ERROR_MISSING_MIN_MAX);
879

880
881
882
                $attribute = 'min="' . $arrData[0] . '" ';
                $attribute .= 'max="' . $arrData[1] . '" ';
                break;
883

884
885
886
887
888
889
890
891
892
893
            case SANITIZE_ALLOW_PATTERN:
                $attribute = 'pattern="' . $data . '" ';
                break;

            case SANITIZE_ALLOW_ALL:
                break;

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

896
897
898
899
        return $attribute;
    }

    /**
900
     * Set corresponding html attributes readonly/required/disabled, based on $formElement[FE_MODE].
901
     *
902
     * @param string $feMode
903
     * @return string
904
     * @throws UserFormException
905
     */
906
    private function getAttributeFeMode($feMode) {
907
908
        $attribute = '';

909
910
911
912
913
        $this->getFeMode($feMode, $hidden, $disabled, $required);

        switch ($feMode) {
            case FE_MODE_HIDDEN:
            case FE_MODE_SHOW:
914
                break;
915
916
917
            case FE_MODE_REQUIRED:
            case FE_MODE_READONLY:
                $attribute .= Support::doAttribute($feMode, $feMode);
918
919
                break;
            default:
920
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
921
922
                break;
        }
923

924
        // Attributes: data-...
925
        $attribute .= Support::doAttribute(DATA_HIDDEN, $hidden);
926
        $attribute .= Support::doAttribute(DATA_DISABLED, $disabled);
927
928
        $attribute .= Support::doAttribute(DATA_REQUIRED, $required);

929
930
931
        return $attribute;
    }

932
    /**
Carsten  Rose's avatar
Carsten Rose committed
933
934
     * Build HelpBlock
     *
935
936
937
     * @return string
     */
    private function getHelpBlock() {
938
939
        //TODO: #3066 Class 'hidden' einbauen
//        return '<div class="help-block with-errors hidden"></div>';
940
941
942
        return '<div class="help-block with-errors"></div>';
    }

943
944
945
    /**
     * Builds HTML 'checkbox' element.
     *
946
     * Checkboxes will only be submitted, if they are checked. Therefore, a hidden element with the unchecked value will be transferred first.
947
948
949
     *
     * Format: <input type="hidden" name="$htmlFormElementId" value="$valueUnChecked">
     *         <input name="$htmlFormElementId" type="checkbox" [autofocus="autofocus"]
Carsten  Rose's avatar
Carsten Rose committed
950
     *            [required="required"] [disabled="disabled"] value="<value>" [checked="checked"] >
951
952
953
954
     *
     * @param array $formElement
     * @param $htmlFormElementId
     * @param $value
955
     * @param array $json
956
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE*
957
     * @return string
958
959
     * @throws CodeException
     * @throws \qfq\UserFormException
960
     */
961
    public function buildCheckbox(array $formElement, $htmlFormElementId, $value, array &$json, $mode = FORM_LOAD) {
962
963
964
965
        $itemKey = array();
        $itemValue = array();

        // Fill $itemKey & $itemValue
966
        $this->getKeyValueListFromSqlEnumSpec($formElement, $itemKey, $itemValue);
967
968
969

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

        if ($formElement['checkBoxMode'] === 'multi') {
975
//            $htmlFormElementId .= '[]';
976
        } else {
977
            // Fill meaningfull defaults to parameter: checked|unchecked  (CHECKBOX_VALUE_CHECKED|CHECKBOX_VALUE_UNCHECKED)
978
979
980
            $this->prepareCheckboxCheckedUncheckedValue($itemKey, $formElement);
        }

981
        $attributeBase = $this->getAttributeFeMode($formElement[FE_MODE]);