AbstractBuildForm.php 115 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;
Carsten  Rose's avatar
Carsten Rose committed
12
13
14
15

//use qfq\Store;
//use qfq\OnArray;
//use qfq\UserFormException;
16
17
18
19

require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
20
require_once(__DIR__ . '/../qfq/exceptions/UserFormException.php');
21
require_once(__DIR__ . '/../qfq/Database.php');
22
require_once(__DIR__ . '/../qfq/helper/HelperFormElement.php');
23
require_once(__DIR__ . '/../qfq/helper/Support.php');
24
require_once(__DIR__ . '/../qfq/helper/OnArray.php');
25
require_once(__DIR__ . '/../qfq/report/Link.php');
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
            'fieldset' => 'Fieldset',
101
102
            'pill' => 'Pill',
            'templateGroup' => 'TemplateGroup'
103
104
        ];

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

128
129
130
        $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>";
131

132
        $this->inputCheckPattern = Sanitize::inputCheckPatternArray();
133
134
    }

135
136
    abstract public function fillWrap();

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

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

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

167
        $filter = $this->getProcessFilter();
168

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

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

        // <form>
        if ($mode === FORM_LOAD) {
185
            $htmlT3vars = $this->prepareT3VarsForSave();
186
187
188
            $htmlTail = $this->tail();
            $htmlSubrecords = $this->doSubrecords();
        }
189
        $htmlHidden = $this->buildAdditionalFormElements();
190

191
192
        $htmlSip = $this->buildHiddenSip($json);

193
        return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail . $htmlSubrecords : $json;
194
195
    }

196
    /**
197
     * Builds the head area of the form.
198
     *
199
     * @return string
200
     */
201
202
    public function head() {
        $html = '';
203

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

206
207
208
        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
        $formEditUrl = $this->createFormEditUrl();
209

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

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

214
215
216
        $html .= $this->getFormTag();

        return $html;
217
218
    }

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

Carsten  Rose's avatar
Carsten Rose committed
226
        if (!$this->showDebugInfo) {
227
228
            return '';
        }
229

230
231
232
233
234
        $queryStringArray = [
            'id' => $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3),
            'form' => 'form',
            'r' => $this->formSpec['id']
        ];
235

236
        $queryString = Support::arrayToQueryString($queryStringArray);
237

238
239
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
240

241
        return $url;
242
243
244
    }

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

        if ($flagOmitEmpty && $value === "") {
255
            return '';
256
257
        }

258
259
260
261
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
262
     * Returns '<form ...>'-tag with various attributes.
263
264
265
266
267
268
269
270
271
272
273
274
275
     *
     * @return string
     */
    public function getFormTag() {

        $attribute = $this->getFormTagAtrributes();

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

    /**
     * Build an assoc array with standard form attributes.
     *
276
     * @return array
277
278
279
     */
    public function getFormTagAtrributes() {

280
        $attribute['id'] = $this->getFormId();
281
282
283
284
285
286
287
288
289
290
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

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

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

313
        return API_DIR . '/save.php';
314
315
316
317
318
319
320
321
322
323
324
325
    }

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

326
        $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"');
327
328
329
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
330

331
    abstract public function getProcessFilter();
332
333

    /**
334
     * Process all FormElements: Collect and return all HTML code & JSON.
335
     *
336
     * @param $recordId
337
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
338
     * @param int $feIdContainer
339
340
341
342
     * @param array $json
     * @param string $modeCollectFe
     * @param bool $htmlElementNameIdZero
     * @param string $storeUse
343
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
344
     * @return string
345
346
     * @throws CodeException
     * @throws DbException
347
     * @throws \qfq\UserFormException
348
     */
349
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, array &$json,
350
351
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
                             $storeUse = STORE_USE_DEFAULT, $mode = FORM_LOAD) {
352
        $html = '';
353
        $flagOutput = false;
354

355
        // The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded.
356
        $skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER];
357

358
        // get current data record
359
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
360
361
362
            $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] . "'.");
363
            $this->store->setStore($row, STORE_RECORD);
364
        }
365

366
367
        $this->checkAutoFocus();

368
369
        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
370

371
372
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
373
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'no')
374
375
376
377
            ) {
                continue; // skip this FE
            }

378
            $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); // type='extra' will not displayed not trasnmitted to the form
379

380
381
            $debugStack = array();

382
383
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
384

385
386
            // for Upload FormElements, it's necessary to precalculate an optional given 'slaveId'.
            if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) {
387
                Support::setIfNotSet($fe, FE_SLAVE_ID);
388
389
                $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
390
391
            }

392
            // evaluate current FormElement
393
            $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack);
394

395
            // Some Defaults
396
            $formElement = Support::setFeDefaults($formElement, $this->formSpec);
397

398
            if ($flagOutput === true) {
399
                $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]);
400
            }
401

402
403
            //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];
404

405
            // If there is a value explicit defined: take it
406
407
            $value = $formElement[FE_VALUE];
            if ($value === '') {
408
                // Only take the default, if the FE is a real tablecolumn. See #2064
409
                if ($this->store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES) !== false) {
410
411
412
                    $value = $this->store->getVar($name, $storeUse, $formElement['checkType']);
                }
            }
413

414
415
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
416
417
418
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement[FE_NAME], ($htmlElementNameIdZero) ? 0 : $recordId);
            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->formSpec[F_ID], $formElement[FE_ID], ($htmlElementNameIdZero) ? 0 : $recordId, 0);

419

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

Carsten  Rose's avatar
Carsten Rose committed
423
            $jsonElement = array();
424
            $elementExtra = '';
425
            // Render pure element
426
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
427
428

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
429
            if ($formElement[FE_CLASS] == FE_CLASS_CONTAINER) {
Carsten  Rose's avatar
Carsten Rose committed
430
431
432
433
434
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
                // for non container elements: just add the current json status
435
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
436
437
438
439
440
441
                    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
442
443
                }
            }
444

445
446
447
            if ($flagOutput) {
                // debugStack as Tooltip
                if ($this->showDebugInfo && count($debugStack) > 0) {
448
                    $elementHtml .= Support::doTooltip($formElement[FE_HTML_ID] . HTML_ID_EXTENSION_TOOLTIP, implode("\n", $debugStack));
449
                }
450

451
452
                // Construct Marshaller Name: buildRow
                $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
453

454
                $html .= $formElement[FE_HTML_BEFORE] . $this->$buildRowName($formElement, $elementHtml, $htmlFormElementName) . $formElement[FE_HTML_AFTER];
455
            }
456
        }
457

458
459
460
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

461
462
463
        return $html;
    }

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
506
507
508
509
510
511
512
    /**
     * 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';
        }
    }

513
514
    abstract public function fillWrapLabelInputNote($label, $input, $note);

515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
    /**
     * Copy a subset of current STORE_TYPO3 variables to SIP. Set a hidden form field to submit the assigned SIP to save/update.
     *
     * @throws CodeException
     * @throws UserFormException
     */
    private function prepareT3VarsForSave() {

        $t3VarsSip = $this->store->copyT3VarsToSip();

        return $this->buildNativeHidden(CLIENT_TYPO3VARS, $t3VarsSip);

    }

    /**
     * Builds a real HTML hidden form element. Useful for checkboxes, Multiple-Select and Radios.
     *
532
     * @param $htmlFormElementName
533
534
535
     * @param $value
     * @return string
     */
536
537
    public function buildNativeHidden($htmlFormElementName, $value) {
        return '<input type="hidden" name="' . $htmlFormElementName . '" value="' . htmlentities($value) . '">';
538
539
    }

540
541
542
543
    abstract public function tail();

    abstract public function doSubrecords();

544
545
546
547
548
549
550
551
552
553
554
555
556
557
    /**
     * Get all elements from STORE_ADDITIONAL_FORM_ELEMENTS and return them as a string.
     *
     * @return string
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
    private function buildAdditionalFormElements() {

        $data = $this->store->getStore(STORE_ADDITIONAL_FORM_ELEMENTS);

        return is_array($data) ? implode('', $data) : '';
    }

558
559
560
    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
561
     * @param array $json
562
563
564
565
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
566
    public function buildHiddenSip(array &$json) {
567

568
        $sipArray = $this->store->getStore(STORE_SIP);
569
570

        // do not include system vars
571
572
573
574
575
576
577
578
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

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

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

579
        $json[] = $this->getFormElementForJson(CLIENT_SIP, $sipValue, [FE_MODE => FE_MODE_SHOW]);
580
581
582
583
584

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

    /**
585
586
587
     * Create an array with standard elements for 'mode' (hidden, disabled, required) and add 'form-element', 'value'.
     * 'Generic Element Update': add via API_ELEMENT_UPDATE 'label' and 'note'.
     * All collected data as array - will be later converted to JSON.
Carsten  Rose's avatar
Carsten Rose committed
588
     *
589
     * @param string $htmlFormElementName
590
     * @param string|array $value
591
     * @param array $formElement
592
593
     * @return array
     */
594
    private function getFormElementForJson($htmlFormElementName, $value, array $formElement) {
595

596
        $json = $this->getJsonFeMode($formElement[FE_MODE]);
597

598
        $json['form-element'] = $htmlFormElementName;
599
600
        $json['value'] = $value;

601
602
        if (isset($formElement[FE_LABEL])) {
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_LABEL;
603
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL]);
604
605
606
607
608
609
610
        }

        if (isset($formElement[FE_NOTE])) {
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_NOTE;
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = $formElement[FE_NOTE];
        }

611
612
613
        return $json;
    }

614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
    /**
     * 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
629
630
     * Depending of $feMode set variables $hidden, $disabled, $required to 'yes' or 'no'.
     *
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
     * @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:
649
                $disabled = 'yes';  // convert the UI status 'readonly' to the HTML/CSS status 'disabled'.
650
651
652
653
654
655
656
657
658
659
                break;
            case FE_MODE_HIDDEN:
                $hidden = 'yes';
                break;
            default:
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
                break;
        }
    }

660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
    /**
     * Builds a label, typically for an html-'<input>'-element.
     *
     * @param string $htmlFormElementName
     * @param string $label
     * @return string
     */
    public function buildLabel($htmlFormElementName, $label) {
        $attributes = Support::doAttribute('for', $htmlFormElementName);
        $attributes .= Support::doAttribute('class', 'control-label');

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

        return $html;
    }

Carsten  Rose's avatar
Carsten Rose committed
676
677
678
679
    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @throws CodeException
680
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
681
682
683
684
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
        $urlParam = $this->store->getStore(STORE_SIP);
        $urlParam[SIP_RECORD_ID] = 0;
685

Carsten  Rose's avatar
Carsten Rose committed
686
687
        unset($urlParam[SIP_SIP]);
        unset($urlParam[SIP_URLPARAM]);
688
689

        Support::appendTypo3ParameterToArray($urlParam);
Carsten  Rose's avatar
Carsten Rose committed
690
691
692
693
694

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

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

695
        if ($this->showDebugInfo) {
696
            //TODO: missing decoding of SIP
697
698
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }
Carsten  Rose's avatar
Carsten Rose committed
699
700
701
702

        return $url;
    }

703
    abstract public function buildRowPill(array $formElement, $elementHtml);
704

705
    abstract public function buildRowFieldset(array $formElement, $elementHtml);
706

707
708
    abstract public function buildRowTemplateGroup(array $formElement, $elementHtml);

709
    abstract public function buildRowSubrecord(array $formElement, $elementHtml);
710

711
712
    /**
     * Builds HTML 'input' element.
713
     * Format: <input name="$htmlFormElementName" <type="email|input|password|url" [autocomplete="autocomplete"] [autofocus="autofocus"]
714
     *           [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"]
Carsten  Rose's avatar
Carsten Rose committed
715
     *           [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
716
717
718
     *
     *
     * @param array $formElement
719
720
     * @param string $htmlFormElementName
     * @param string $value
721
     * @param array $json Return updates in this array - will be later converted to JSON.
722
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
723
     * @return string complete rendered HTML input element.
724
     * @throws \qfq\UserFormException
725
     */
726
    public function buildInput(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
727
        $textarea = '';
728
        $attribute = '';
729
730
        $class = 'form-control';

731
732
733
734
735
736
737
        $typeAheadUrlParam = $this->typeAheadBuildParam($formElement);
        if ($typeAheadUrlParam != '') {
            $class .= ' ' . CLASS_TYPEAHEAD;
            $dataSip = $this->sip->queryStringToSip($typeAheadUrlParam, RETURN_SIP);
            $attribute .= Support::doAttribute(DATA_TYPEAHEAD_SIP, $dataSip);
            $attribute .= Support::doAttribute(DATA_TYPEAHEAD_LIMIT, $formElement[FE_TYPEAHEAD_LIMIT]);
            $attribute .= Support::doAttribute(DATA_TYPEAHEAD_MINLENGTH, $formElement[FE_TYPEAHEAD_MINLENGTH]);
738
        }
739

740
741
        $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attribute .= Support::doAttribute('name', $htmlFormElementName);
742
        $attribute .= Support::doAttribute('class', $class);
743

744
        if (isset($formElement[FE_RETYPE_SOURCE_NAME])) {
745
746
            $htmlFormElementNamePrimary = str_replace(RETYPE_FE_NAME_EXTENSION, '', $htmlFormElementName);
            $attribute .= Support::doAttribute('data-match', '[name=' . str_replace(':', '\\:', $htmlFormElementNamePrimary) . ']');
747
748
        }

749
        // Check for input type 'textarea'.
750
        $colsRows = explode(',', $formElement['size'], 2);
751
        if (count($colsRows) === 2) {
752
            // <textarea>
753
754
            $htmlTag = '<textarea';

755
756
            $attribute .= Support::doAttribute('cols', $colsRows[0]);
            $attribute .= Support::doAttribute('rows', $colsRows[1]);
757
            $textarea = htmlentities($value) . '</textarea>';
758
759

        } else {
Carsten  Rose's avatar
Carsten Rose committed
760
761
762
763
            $htmlTag = '<input';

            $this->adjustMaxLength($formElement);

764
            if ($formElement[FE_MAX_LENGTH] > 0 && $value !== '') {
Carsten  Rose's avatar
Carsten Rose committed
765
                // crop string only if it's not empty (substr returns false on empty strings)
766
                $value = substr($value, 0, $formElement[FE_MAX_LENGTH]);
767
            }
768
769
770
771

            // '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
772
        }
773

774
        $attribute .= $this->getAttributeList($formElement, ['autocomplete', 'autofocus', 'placeholder']);
775
        $attribute .= $this->getAttributeList($formElement, [F_FE_DATA_PATTERN_ERROR, F_FE_DATA_REQUIRED_ERROR, F_FE_DATA_MATCH_ERROR, F_FE_DATA_ERROR]);
776
        $attribute .= Support::doAttribute('data-load', ($formElement[FE_DYNAMIC_UPDATE] === 'yes') ? 'data-load' : '');
777
        $attribute .= Support::doAttribute('title', $formElement['tooltip']);
Carsten  Rose's avatar
Carsten Rose committed
778
        $attribute .= $this->getInputCheckPattern($formElement['checkType'], $formElement['checkPattern']);
779

780
        $attribute .= $this->getAttributeFeMode($formElement[FE_MODE]);
781

782
        $json = $this->getFormElementForJson($htmlFormElementName, $value, $formElement);
Carsten  Rose's avatar
Carsten Rose committed
783

784
        return "$htmlTag $attribute>$textarea" . $this->getHelpBlock();
785

786
787
    }

788
789
790
    /**
     * Check $formElement for FE_TYPE_AHEAD_SQL or FE_TYPE_AHEAD_LDAP_SERVER.
     * If one of them is given: fill $urlParam.
Carsten  Rose's avatar
Carsten Rose committed
791
     * Set some parameter for later outside use, especially FE_TYPEAHEAD_LIMIT, FE_TYPEAHEAD_MINLENGTH
792
793
794
795
     *
     * @param array $formElement
     * @return string
     */
Carsten  Rose's avatar
Carsten Rose committed
796
    private function typeAheadBuildParam(array &$formElement) {
797
798
799

        $urlParam = '';

Carsten  Rose's avatar
Carsten Rose committed
800
801
802
        $formElement[FE_TYPEAHEAD_LIMIT] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LIMIT, TYPEAHEAD_DEFAULT_LIMIT);
        $formElement[FE_TYPEAHEAD_MINLENGTH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_MINLENGTH, 2);

803
        if (isset($formElement[FE_TYPEAHEAD_SQL])) {
Carsten  Rose's avatar
Carsten Rose committed
804
805
            $sql = $this->checkSqlAppendLimit($formElement[FE_TYPEAHEAD_SQL], $formElement[FE_TYPEAHEAD_LIMIT]);
            $urlParam = FE_TYPEAHEAD_SQL . '=' . $sql;
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
        } elseif (isset($formElement[FE_TYPEAHEAD_LDAP_SERVER])) {
            $formElement[FE_TYPEAHEAD_LDAP_SERVER] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SERVER);
            $formElement[FE_TYPEAHEAD_LDAP_BASE_DN] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_BASE_DN);
            $formElement[FE_TYPEAHEAD_LDAP_SEARCH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SEARCH);
            $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_VALUE_PRINTF);
            $formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_KEY_PRINTF);

            $arr = [
                FE_TYPEAHEAD_LDAP_SERVER => $formElement[FE_TYPEAHEAD_LDAP_SERVER],
                FE_TYPEAHEAD_LDAP_BASE_DN => $formElement[FE_TYPEAHEAD_LDAP_BASE_DN],
                FE_TYPEAHEAD_LDAP_SEARCH => $formElement[FE_TYPEAHEAD_LDAP_SEARCH],
                FE_TYPEAHEAD_LDAP_VALUE_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF],
                FE_TYPEAHEAD_LDAP_KEY_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF],
                FE_TYPEAHEAD_LIMIT => $formElement[FE_TYPEAHEAD_LIMIT],
            ];

            $urlParam = OnArray::toString($arr);
        }

Carsten  Rose's avatar
Carsten Rose committed
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
        return $urlParam;
    }

    /**
     * Checks if $sql contains a SELECT statement.
     * Check for existence of a LIMIT Parameter. If not found add one.
     *
     * @param $sql
     * @param $limit
     * @return string   Checked and maybe extended $sql statement.
     * @throws \qfq\UserFormException
     */
    private function checkSqlAppendLimit($sql, $limit) {
        $sql = trim($sql);

        if (false === stristr(substr($sql, 0, 7), 'SELECT ')) {
            throw new UserFormException("Expect a SELECT statement in " . FE_TYPEAHEAD_SQL . " - got: " . $sql, ERROR_BROKEN_PARAMETER);
842
843
        }

Carsten  Rose's avatar
Carsten Rose committed
844
845
846
847
848
        if (false === stristr($sql, ' LIMIT ')) {
            $sql .= " LIMIT $limit";
        }

        return $sql;
849
850
    }

Carsten  Rose's avatar
Carsten Rose committed
851
    /**
Carsten  Rose's avatar
Carsten Rose committed
852
853
     * 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
854
855
856
857
858
859
860
     * @param array $formElement
     */
    private function adjustMaxLength(array &$formElement) {

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

861
        switch ($formElement[FE_TYPE]) {
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
            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
882
        if ($maxLength !== false) {
883
            if (is_numeric($formElement['maxLength']) && $formElement['maxLength'] != 0) {
Carsten  Rose's avatar
Carsten Rose committed
884
885
886
887
888
889
890
891
892
                if ($formElement['maxLength'] > $maxLength) {
                    $formElement['maxLength'] = $maxLength;
                }
            } else {
                $formElement['maxLength'] = $maxLength;
            }
        }
    }

893
    /**
894
895
     * Get column spec from tabledefinition and parse size of it. If nothing defined, return false.
     *
896
     * @param $column
897
     * @return bool|int  a) 'false' if there is no length definition, b) length definition, c) date|time|datetime|timestamp use hardcoded length
898
899
900
901
902
     */
    private function getColumnSize($column) {
        $matches = array();

        $typeSpec = $this->store->getVar($column, STORE_TABLE_COLUMN_TYPES);
903
904
905
906
907
908
909
910
911
        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:
912
913
914
                if (substr($typeSpec, 0, 4) === 'set(' || substr($typeSpec, 0, 5) === 'enum(') {
                    return $this->maxLengthSetEnum($typeSpec);
                }
915
916
                break;
        }
917

918
        // e.g.: string(64) >> 64, enum('yes','no') >> false
919
920
921
922
923
924
        if (1 === preg_match('/\((.+)\)/', $typeSpec, $matches)) {
            if (is_numeric($matches[1]))
                return $matches[1];
        }

        return false;
925
926
    }

927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
    /**
     * 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;
    }

950
951
    /**
     * Builds a HTML attribute list, based on  $attributeList.
952
     *
953
     * E.g.: attributeList: [ 'type', 'autofocus' ]
954
     *       generates: 'type="$formElement[FE_TYPE]" autofocus="$formElement['autofocus']" '
955
956
957
     *
     * @param array $formElement
     * @param array $attributeList
958
     * @param bool $flagOmitEmpty
959
960
     * @return string
     */
961
    private function getAttributeList(array $formElement, array $attributeList, $flagOmitEmpty = true) {
962
963
964
        $attribute = '';
        foreach ($attributeList as $item) {
            if (isset($formElement[$item]))
965
                $attribute .= Support::doAttribute(strtolower($item), $formElement[$item], $flagOmitEmpty);
966
967
968
969
970
971
        }
        return $attribute;
    }

    /**
     * Construct HTML Input attribute for Client Validation:
972
     *
973
     *   type     data                      result
974
     *   -------  -----------------------   -------------------------------------------------------------------------------
975
976
     *   min|max  <min value>|<max value>   min="$attrData[0]"|max="$attrData[1]"
     *   pattern  <regexp>                  pattern="$data"
Carsten  Rose's avatar
Carsten Rose committed
977
     *   digit    -                         pattern="^[0-9]*$"
978
     *   email    -                         pattern="^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$"
979
     *   alnumx   -
980
     *
981
     * For 'min/max' and 'pattern' the 'data' will be injected in the attribute string via '%s'.
982
983
984
985
     *
     * @param $type
     * @param $data
     * @return string
986
     * @throws \qfq\UserFormException
987
     */
Carsten  Rose's avatar
Carsten Rose committed
988
    private function getInputCheckPattern($type, $data) {
989
990
        $attribute = '';

991
992
        if ($type === '') {
            return '';
993
        }
994