AbstractBuildForm.php 136 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
20
21
22
23
24
25
26
require_once(__DIR__ . '/store/Store.php');
require_once(__DIR__ . '/Constants.php');
require_once(__DIR__ . '/exceptions/DbException.php');
require_once(__DIR__ . '/exceptions/UserFormException.php');
require_once(__DIR__ . '/database/Database.php');
require_once(__DIR__ . '/helper/HelperFormElement.php');
require_once(__DIR__ . '/helper/Support.php');
require_once(__DIR__ . '/helper/OnArray.php');
require_once(__DIR__ . '/helper/Ldap.php');
require_once(__DIR__ . '/report/Link.php');
27

28
/**
Carsten  Rose's avatar
Carsten Rose committed
29
30
 * Class AbstractBuildForm
 * @package qfq
31
 */
32
abstract class AbstractBuildForm {
33
34
35
36
37
    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();
38
    protected $wrap = array();
39
    protected $symbol = array();
40
    protected $showDebugInfoFlag = false;
41
    protected $inputCheckPattern = array();
Carsten  Rose's avatar
Carsten Rose committed
42

43
//    protected $feDivClass = array(); // Wrap FormElements in <div class="$feDivClass[type]">
44

45
46
47
48
49
50
51
52
    /**
     * @var Store
     */
    protected $store = null;
    /**
     * @var Evaluate
     */
    protected $evaluate = null;
53
54
55
    /**
     * @var string
     */
56
    private $formId = null;
57
58
59
60
61
    /**
     * @var Sip
     */
    private $sip = null;

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

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

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

        $this->buildElementFunctionName = [
84
            FE_TYPE_CHECKBOX => 'Checkbox',
85
            FE_TYPE_DATE     => 'DateTime',
86
            FE_TYPE_DATETIME => 'DateTime',
87
88
89
90
91
92
93
94
95
            'dateJQW'        => 'DateJQW',
            'datetimeJQW'    => 'DateJQW',
            'email'          => 'Input',
            'gridJQW'        => 'GridJQW',
            FE_TYPE_EXTRA    => 'Extra',
            FE_TYPE_TEXT     => 'Input',
            FE_TYPE_EDITOR   => 'Editor',
            FE_TYPE_TIME     => 'DateTime',
            FE_TYPE_NOTE     => 'Note',
96
            FE_TYPE_PASSWORD => 'Input',
97
98
            FE_TYPE_RADIO    => 'Radio',
            FE_TYPE_SELECT   => 'Select',
99
            FE_TYPE_SUBRECORD => 'Subrecord',
100
101
102
103
            FE_TYPE_UPLOAD   => 'File',
            'fieldset'       => 'Fieldset',
            'pill'           => 'Pill',
            'templateGroup'  => 'TemplateGroup',
104
105
        ];

106
        $this->buildRowName = [
107
            FE_TYPE_CHECKBOX => 'Native',
108
            FE_TYPE_DATE     => 'Native',
109
            FE_TYPE_DATETIME => 'Native',
110
111
112
113
114
115
116
117
118
            'dateJQW'        => 'Native',
            'datetimeJQW'    => 'Native',
            'email'          => 'Native',
            'gridJQW'        => 'Native',
            FE_TYPE_EXTRA    => 'Native',
            FE_TYPE_TEXT     => 'Native',
            FE_TYPE_EDITOR   => 'Native',
            FE_TYPE_TIME     => 'Native',
            FE_TYPE_NOTE     => 'Native',
119
            FE_TYPE_PASSWORD => 'Native',
120
121
            FE_TYPE_RADIO    => 'Native',
            FE_TYPE_SELECT   => 'Native',
122
            FE_TYPE_SUBRECORD => 'Subrecord',
123
124
125
126
            FE_TYPE_UPLOAD   => 'Native',
            'fieldset'       => 'Fieldset',
            'pill'           => 'Pill',
            'templateGroup'  => 'TemplateGroup',
127
128
        ];

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

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

136
137
    abstract public function fillWrap();

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

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

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

170
        $filter = $this->getProcessFilter();
171

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

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

            if ($mode === FORM_SAVE && $recordId != 0) {
186
//                $json[] = [API_ELEMENT_UPDATE => [DIRTY_RECORD_HASH_MD5 => [API_ELEMENT_ATTRIBUTE => ['value' => $newMd5]]]];
Carsten  Rose's avatar
Carsten Rose committed
187
//                $json[] = [API_ELEMENT_UPDATE => [DIRTY_RECORD_HASH_MD5 =>  ['value' => $newMd5]]];
188
189
190
191
192
193
194
195

                // element-update: with 'content'
//                $inputMd5 = $this->buildInputRecordHashMd5(false);
//                $json[][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5_SPAN][API_ELEMENT_CONTENT] = $inputMd5;

                // element-update: with 'value'
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
                $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId);
196
197

                // Via 'element-update'
198
199
                $json[][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5][API_ELEMENT_ATTRIBUTE]['value'] = $md5;

200
201
202
                // Via 'form-update'
//                $json[] =  [API_FORM_UPDATE_FORM_ELEMENT => DIRTY_RECORD_HASH_MD5, API_FORM_UPDATE_VALUE => $md5,
//                    API_FORM_UPDATE_DISABLED => false, API_FORM_UPDATE_REQUIRED => false ];
Carsten  Rose's avatar
Carsten Rose committed
203
            }
204
        }
205
206
207

        // <form>
        if ($mode === FORM_LOAD) {
208
            $htmlT3vars = $this->prepareT3VarsForSave();
209
210
211
            $htmlTail = $this->tail();
            $htmlSubrecords = $this->doSubrecords();
        }
212
        $htmlHidden = $this->buildAdditionalFormElements();
213

214
215
        $htmlSip = $this->buildHiddenSip($json);

216
        return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail . $htmlSubrecords : $json;
217
218
    }

219
    /**
220
     * Builds the head area of the form.
221
     *
222
     * @return string
223
     */
224
225
    public function head() {
        $html = '';
226

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

229
230
        // Logged in BE User will see a FormEdit Link
        $sipParamString = OnArray::toString($this->store->getStore(STORE_SIP), ':', ', ', "'");
231
        $formEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM, $this->formSpec[F_ID]);
232

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

235
        $html .= $this->wrapItem(WRAP_SETUP_TITLE, $this->formSpec[F_TITLE], true);
236

237
238
239
        $html .= $this->getFormTag();

        return $html;
240
241
    }

242
    /**
243
244
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of
     * the SIP.
245
     *
246
     * @param string $form FORM_NAME_FORM | FORM_NAME_FORM_ELEMENT
Carsten  Rose's avatar
Carsten Rose committed
247
248
     * @param int    $recordId id of form or formElement
     * @param array  $param
249
250
251
     *
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:...,
     *                ...]</small>
252
     * @throws CodeException
253
     */
254
    public function createFormEditorUrl($form, $recordId, array $param = array()) {
255

256
        if (!$this->showDebugInfoFlag) {
257
258
            return '';
        }
259

260
        $queryStringArray = [
261
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
262
            'form' => $form,
263
            'r'  => $recordId,
264
        ];
265
        $queryStringArray = array_merge($queryStringArray, $param);
266

267
        $queryString = Support::arrayToQueryString($queryStringArray);
268

269
270
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
271

272
        return $url;
273
274
275
    }

    /**
276
277
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
Carsten  Rose's avatar
Carsten Rose committed
278
279
     * @param string     $item
     * @param string     $value
280
     * @param bool|false $flagOmitEmpty
281
     *
282
283
284
     * @return string
     */
    public function wrapItem($item, $value, $flagOmitEmpty = false) {
285
286

        if ($flagOmitEmpty && $value === "") {
287
            return '';
288
289
        }

290
291
292
293
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
294
     * Returns '<form ...>'-tag with various attributes.
295
296
297
298
     *
     * @return string
     */
    public function getFormTag() {
Carsten  Rose's avatar
Carsten Rose committed
299
        $md5 = '';
300
301
302

        $attribute = $this->getFormTagAtrributes();

303
304
        $honeypot = $this->getHoneypotVars();

305
        $md5 = $this->buildInputRecordHashMd5();
Carsten  Rose's avatar
Carsten Rose committed
306
307
308
309
310
311

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

    /**
     * Build MD5 from the current record. Return HTML Input element.
312
     *
313
     * @param bool $flagWithSpan
314
     *
315
316
317
     * @return string
     * @throws \qfq\CodeException
     * @throws \qfq\DbException
Carsten  Rose's avatar
Carsten Rose committed
318
     */
319
    public function buildInputRecordHashMd5() {
320

Carsten  Rose's avatar
Carsten Rose committed
321
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
322
323
324
        $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId);

        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='hidden' value='$md5'>";
325

326
//        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='text' value='$md5'>";
327
328
329
330
331
332
333
334

        return $data;
    }


    /**
     * @param $tableName
     * @param $recordId
335
     *
336
337
338
339
     * @return string
     * @throws \qfq\CodeException
     * @throws \qfq\DbException
     */
340
    public function buildRecordHashMd5($tableName, $recordId) {
341
        $record = array();
Carsten  Rose's avatar
Carsten Rose committed
342
343

        if ($recordId != 0) {
344
            $record = $this->db->sql("SELECT * FROM $tableName WHERE id=?", ROW_EXPECT_1, [$recordId], "Record to load not found.");
Carsten  Rose's avatar
Carsten Rose committed
345
346
        }

347
        return OnArray::getMd5($record);
348
349
    }

350
351
352
353
354
355
356
357
358
359
360
361
362
    /**
     * Create HTML Input vars to detect bot automatic filling of forms.
     *
     * @return string
     */
    public function getHoneypotVars() {
        $html = '';

        $vars = $this->store->getVar(SYSTEM_SECURITY_VARS_HONEYPOT, STORE_SYSTEM);

        // Iterate over all fake vars
        $arr = explode(',', $vars);
        foreach ($arr as $name) {
363
364
365
366
            $name = trim($name);
            if ($name === '') {
                continue;
            }
367
368
369
370
371
            $html .= "<input name='$name' type='hidden' value='' readonly>";
        }

        return $html;
    }
372

Carsten  Rose's avatar
Carsten Rose committed
373

374
375
376
    /**
     * Build an assoc array with standard form attributes.
     *
377
     * @return array
378
379
380
     */
    public function getFormTagAtrributes() {

381
        $attribute['id'] = $this->getFormId();
382
383
384
385
386
387
388
389
390
391
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

392
    /**
Carsten  Rose's avatar
Carsten Rose committed
393
394
     * Return a uniq form id
     *
395
396
397
398
399
400
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
401

402
403
404
        return $this->formId;
    }

405
406
407
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
408
409
     * Notice: the SIP will be transferred as POST Parameter.
     *
410
411
412
413
414
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {

415
        return API_DIR . '/save.php';
416
417
418
419
420
421
422
423
424
425
426
427
    }

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

428
        $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"');
429

430
431
432
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
433

434
    abstract public function getProcessFilter();
435
436

    /**
437
     * Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON.
438
     *
Carsten  Rose's avatar
Carsten Rose committed
439
     * @param int    $recordId
440
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
Carsten  Rose's avatar
Carsten Rose committed
441
442
     * @param int    $feIdContainer
     * @param array  $json
443
     * @param string $modeCollectFe
Carsten  Rose's avatar
Carsten Rose committed
444
     * @param bool   $htmlElementNameIdZero
445
     * @param string $storeUseDefault
446
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
447
     *
448
     * @return string
449
450
     * @throws CodeException
     * @throws DbException
451
     * @throws \qfq\UserFormException
452
     */
453
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, array &$json,
454
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
455
                             $storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD) {
456
        $html = '';
457

Carsten  Rose's avatar
Carsten Rose committed
458
459
//        $html .= $this->buildRecordHashMd5();

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

463
        // get current data record
464
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
465
            $row = $this->db->sql('SELECT * FROM ' . $this->formSpec[F_TABLE_NAME] . " WHERE id = ?", ROW_EXPECT_1,
466
467
                array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$recordId' from table '" .
                $this->formSpec[F_TABLE_NAME] . "'.");
468
            $this->store->setStore($row, STORE_RECORD);
469
        }
470

471
472
        $this->checkAutoFocus();

473
474
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

475
476
        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
477
            $storeUse = $storeUseDefault;
478

479
480
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
481
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'no')
482
483
484
485
            ) {
                continue; // skip this FE
            }

Carsten  Rose's avatar
Carsten Rose committed
486
            $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); // type='extra' will not displayed and not transmitted to the form
487

488
489
            $debugStack = array();

Carsten  Rose's avatar
Carsten Rose committed
490
491
492
493
494
495
            // Copy global readonly mode.
            if ($this->formSpec[F_MODE] == F_MODE_READONLY) {
                $fe[FE_MODE] = FE_MODE_READONLY;
                $fe[FE_MODE_SQL] = '';
            }

496
497
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
498

Carsten  Rose's avatar
Carsten Rose committed
499
500
            // Fill STORE_LDAP
            $fe = $this->prepareFillStoreFireLdap($fe);
501

502
503
            // for Upload FormElements, it's necessary to precalculate an optional given 'slaveId'.
            if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) {
504
                Support::setIfNotSet($fe, FE_SLAVE_ID);
505
506
                $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
507
508
            }

Carsten  Rose's avatar
Carsten Rose committed
509
            // ** evaluate current FormElement **
510
            $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack);
511
            $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName);
512

513
            // Some Defaults
514
            $formElement = Support::setFeDefaults($formElement, $this->formSpec);
515

516
            if ($flagOutput === true) {
517
                $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]);
518
            }
519

520
521
            //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];
522

523
524
525
            $value = '';
            Support::setIfNotSet($formElement, FE_VALUE);

526
527
528
            if (is_array($formElement[FE_VALUE])) {
                $formElement[FE_VALUE] = (count($formElement[FE_VALUE])) > 0 ? current($formElement[FE_VALUE][0]) : '';
            }
529

530
            $value = $formElement[FE_VALUE];
531

532
            if ($value === '') {
533
534
535
536
                // #2064 / Only take the default, if the FE is a real tablecolumn.
                // #3426 / Dynamic Update: Inputs loose the new content and shows the old value.
                if ($storeUse == STORE_USE_DEFAULT && $this->store->getVar($formElement[FE_NAME], STORE_TABLE_COLUMN_TYPES) === false) {
                    $storeUse = str_replace(STORE_TABLE_DEFAULT, '', $storeUse); // Remove STORE_DEFAULT
537
                }
538
539
                // Retrieve value via FSRVD
                $value = $this->store->getVar($name, $storeUse, $formElement[FE_CHECK_TYPE], $foundInStore);
540
541
542
543
            }

            if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
                $value = htmlspecialchars_decode($value, ENT_QUOTES);
544
            }
545

546
547
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
548
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, ($htmlElementNameIdZero) ? 0 : $recordId);
549
550
551
            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->formSpec[F_ID], $formElement[FE_ID],
                ($htmlElementNameIdZero) ? 0 : $recordId,
                $formElement[FE_TG_INDEX]);
552

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

Carsten  Rose's avatar
Carsten Rose committed
556
            $jsonElement = array();
557
            $elementExtra = '';
558
            // Render pure element
559
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
560
561

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
562
            if ($formElement[FE_CLASS] == FE_CLASS_CONTAINER) {
Carsten  Rose's avatar
Carsten Rose committed
563
564
565
566
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
567
                // for non-container elements: just add the current json status
568
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
569
570
571
572
573
574
                    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
575
576
                }
            }
577

578
579
            if ($flagOutput) {
                // debugStack as Tooltip
580
                if ($this->showDebugInfoFlag) {
581
582
583
584
585
                    if (count($debugStack) > 0) {
                        $elementHtml .= Support::doTooltip($formElement[FE_HTML_ID] . HTML_ID_EXTENSION_TOOLTIP, implode("\n", $debugStack));
                    }

                    // Build 'FormElement' Edit symbol
Carsten  Rose's avatar
Carsten Rose committed
586
                    $feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
587
588
                    $titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
                    $icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
589
                    $elementHtml .= Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
590
                }
591

592
593
                // Construct Marshaller Name: buildRow
                $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
594

595
                $html .= $formElement[FE_HTML_BEFORE] . $this->$buildRowName($formElement, $elementHtml, $htmlFormElementName) . $formElement[FE_HTML_AFTER];
596
            }
597
        }
598

599
600
601
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

602
603
604
        return $html;
    }

605
    /**
Carsten  Rose's avatar
Carsten Rose committed
606
607
608
609
610
     * Checks if LDAP search is requested.
     * Yes: prepare configuration and fire the query.
     * No: do nothing.
     *
     * @param array $formElement
611
     *
Carsten  Rose's avatar
Carsten Rose committed
612
613
614
615
616
617
618
619
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
    private function prepareFillStoreFireLdap(array $formElement) {
        $config = array();

        if (isset($formElement[FE_FILL_STORE_LDAP]) || isset($formElement[FE_TYPEAHEAD_LDAP])) {
620
621
622
            $keyNames = [F_LDAP_SERVER, F_LDAP_BASE_DN, F_LDAP_ATTRIBUTES,
                F_LDAP_SEARCH, F_TYPEAHEAD_LDAP_SEARCH, F_TYPEAHEAD_LDAP_SEARCH_PER_TOKEN, F_TYPEAHEAD_LDAP_SEARCH_PREFETCH,
                F_TYPEAHEAD_LIMIT, F_TYPEAHEAD_MINLENGTH, F_TYPEAHEAD_LDAP_VALUE_PRINTF,
623
                F_TYPEAHEAD_LDAP_ID_PRINTF, F_LDAP_TIME_LIMIT, F_LDAP_USE_BIND_CREDENTIALS];
624
            $formElement = OnArray::copyArrayItemsIfNotAlreadyExist($this->formSpec, $formElement, $keyNames);
Carsten  Rose's avatar
Carsten Rose committed
625
626
627
628
629
630
631
        } else {
            return $formElement; // nothing to do.
        }

        if (isset($formElement[FE_FILL_STORE_LDAP])) {

            // Extract necessary elements
632
            $config = OnArray::getArrayItems($formElement, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES]);
Carsten  Rose's avatar
Carsten Rose committed
633
634
            $config = $this->evaluate->parseArray($config);

635
            if ($formElement[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
636
637
638
639
                $config[SYSTEM_LDAP_1_RDN] = $this->store->getVar(SYSTEM_LDAP_1_RDN, STORE_SYSTEM);
                $config[SYSTEM_LDAP_1_PASSWORD] = $this->store->getVar(SYSTEM_LDAP_1_PASSWORD, STORE_SYSTEM);
            }

Carsten  Rose's avatar
Carsten Rose committed
640
641
642
643
644
645
646
647
            $ldap = new Ldap();
            $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
            $this->store->setStore($arr, STORE_LDAP, true);
        }

        return $formElement;
    }

648
649
650
651
652
653
654
    /**
     * 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.
655
656
     *   Reason: checks happens per pill - if there is no explizit definition on the first pill, take the first
     *   editable element of that pill.
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
     */
    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;
680

681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
                    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';
        }
    }

Carsten  Rose's avatar
Carsten Rose committed
698
699
700
    /**
     *
     */
701
702
    abstract public function fillWrapLabelInputNote($label, $input, $note);

703
    /**
704
705
     * Copy a subset of current STORE_TYPO3 variables to SIP. Set a hidden form field to submit the assigned SIP to
     * save/update.
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
     *
     * @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.
     *
Carsten  Rose's avatar
Carsten Rose committed
721
722
     * @param        $htmlFormElementName
     * @param string $value
723
     *
724
725
     * @return string
     */
726
727
    public function buildNativeHidden($htmlFormElementName, $value) {
        return '<input type="hidden" name="' . $htmlFormElementName . '" value="' . htmlentities($value) . '">';
728
729
    }

Carsten  Rose's avatar
Carsten Rose committed
730
731
732
    /**
     *
     */
733
734
    abstract public function tail();

Carsten  Rose's avatar
Carsten Rose committed
735
736
737
    /**
     *
     */
738
739
    abstract public function doSubrecords();

740
741
742
743
744
745
746
747
748
749
750
751
752
753
    /**
     * 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) : '';
    }

754
755
756
    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
757
     * @param array $json
758
     *
759
760
761
762
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
763
    public function buildHiddenSip(array &$json) {
764

765
        $sipArray = $this->store->getStore(STORE_SIP);
766
767

        // do not include system vars
768
769
770
771
772
773
774
775
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

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

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

776
        $json[] = $this->getFormElementForJson(CLIENT_SIP, $sipValue, [FE_MODE => FE_MODE_SHOW]);
777
778
779
780
781

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

    /**
782
783
     * Create an array with standard elements for 'mode' (hidden, disabled, required) and add 'form-element',
     * 'value'.
784
785
     * '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
786
     *
Carsten  Rose's avatar
Carsten Rose committed
787
     * @param string       $htmlFormElementName
788
     * @param string|array $value
Carsten  Rose's avatar
Carsten Rose committed
789
     * @param array        $formElement
790
     *
791
792
     * @return array
     */
793
    private function getFormElementForJson($htmlFormElementName, $value, array $formElement) {
794

795
        $json = $this->getJsonFeMode($formElement[FE_MODE]); // disabled, required
796

797
        $json[API_FORM_UPDATE_FORM_ELEMENT] = $htmlFormElementName;
798
799
800
801
802
803
804

        if (isset($formElement[FE_FLAG_ROW_OPEN_TAG]) && isset($formElement[FE_FLAG_ROW_CLOSE_TAG])) {
            $flagRowUpdate = ($formElement[FE_FLAG_ROW_OPEN_TAG] && $formElement[FE_FLAG_ROW_CLOSE_TAG]);
        } else {
            $flagRowUpdate = true;
        }

805
        // 'value' update via 'form-update' on the full row: only if there is no other FE in that row
806
        if ($flagRowUpdate) {
807
            $json[API_FORM_UPDATE_VALUE] = $value;
808
        }
809

810
811
        if (isset($formElement[FE_LABEL])) {
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_LABEL;
812
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL]);
813
814
815
816
817
        }

        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];
818
        }
819

820
        if (isset($formElement[FE_TYPE])) {
821
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT;
822
823
824
825
826
827
828
829
830
831
832
833

            // For FE.type='note': update the column 'input'
            if ($formElement[FE_TYPE] === FE_TYPE_NOTE) {
                $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = $value;
            }

            // Check show/hide: only FE with FE_MODE_SQL given, might change.
            if (!empty($formElement[FE_MODE_SQL])) {
                $class = 'col-md-' . $formElement[FE_BS_INPUT_COLUMNS] . ' ';
                $class .= ($formElement[FE_MODE] == FE_MODE_HIDDEN) ? 'hidden' : '';
                $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['class'] = $class;
            }
834
835
836
837
838

            // #3647
            if (!$flagRowUpdate) {
                $json[API_ELEMENT_UPDATE][$formElement[FE_HTML_ID]][API_ELEMENT_ATTRIBUTE]['value'] = $value;
            }
839
840
        }

841
        // Show / Hide the complete FormElement Row.
842
843
844
        if (isset($formElement[FE_HTML_ID])) { // HIDDEN_SIP comes without a real existing FE structure.
            // Activate 'show' or 'hidden' on the current FormElement via JSON 'API_ELEMENT_UPDATE'
            $class = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS];
845
            if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
846
847
848
849
850
851
852
                $class .= ' hidden';
            }

            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_ROW;
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_ATTRIBUTE]['class'] = $class;
        }

853
854
855
        return $json;
    }

856
857
858
859
    /**
     * Set corresponding JSON attributes readonly/required/disabled, based on $formElement[FE_MODE].
     *
     * @param array $feMode
860
     *
861
862
863
864
865
     * @return array
     * @throws UserFormException
     */
    private function getJsonFeMode($feMode) {

866
        $this->getFeMode($feMode, $dummy, $disabled, $required);
867

868
        return [API_FORM_UPDATE_DISABLED => $disabled === 'yes', API_FORM_UPDATE_REQUIRED => $required === 'yes'];
869
870
871
    }

    /**
Carsten  Rose's avatar
Carsten Rose committed
872
873
     * Depending of $feMode set variables $hidden, $disabled, $required to 'yes' or 'no'.
     *
Carsten  Rose's avatar
Carsten Rose committed
874
875
876
877
     * @param string $feMode
     * @param string $hidden
     * @param string $disabled
     * @param string $required
878
     *
879
880
881
882
883
884
885
886
887
888
889
890
891
892
     * @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:
893
                $disabled = 'yes';  // convert the UI status 'readonly' to the HTML/CSS status 'disabled'.
894
895
896
897
898
899
900
901
902
903
                break;
            case FE_MODE_HIDDEN:
                $hidden = 'yes';
                break;
            default:
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
                break;
        }
    }

904
905
906
907
908
    /**
     * Builds a label, typically for an html-'<input>'-element.
     *
     * @param string $htmlFormElementName
     * @param string $label
909
     * @param string $addClass
910
     *
911
912
     * @return string
     */
913
    public function buildLabel($htmlFormElementName, $label, $addClass = '') {
914
        $attributes = Support::doAttribute('for', $htmlFormElementName);
915
        $attributes .= Support::doAttribute('class', ['control-label', $addClass]);
916
917
918
919
920
921

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

        return $html;
    }

Carsten  Rose's avatar
Carsten Rose committed
922
923
924
925
    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @throws CodeException
926
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
927
928
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
929

930
931
932
        $urlParam = $this->store->getNonSystemSipParam();
        $urlParam[SIP_RECORD_ID] = 0;
        $urlParam[SIP_FORM] = $this->formSpec[F_NAME];
933
934

        Support::appendTypo3ParameterToArray($urlParam);
Carsten  Rose's avatar
Carsten Rose committed
935
936
937
938
939

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

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

940
        if ($this->showDebugInfoFlag) {
941
            //TODO: missing decoding of SIP
942
943
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }
Carsten  Rose's avatar
Carsten Rose committed
944
945
946
947

        return $url;
    }

948
    abstract public function buildRowPill(array $formElement, $elementHtml);
949

950
    abstract public function buildRowFieldset(array $formElement, $elementHtml);
951

952
953
    abstract public function buildRowTemplateGroup(array $formElement, $elementHtml);

954
    abstract public function buildRowSubrecord(array $formElement, $elementHtml);
Carsten  Rose's avatar