AbstractBuildForm.php 170 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

Marc Egger's avatar
Marc Egger committed
9
10
11
namespace IMATHUZH\Qfq\Core;

use IMATHUZH\Qfq\Core\Database\Database;
12
use IMATHUZH\Qfq\Core\Helper\HelperFile;
Marc Egger's avatar
Marc Egger committed
13
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
14
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
Marc Egger's avatar
Marc Egger committed
15
16
use IMATHUZH\Qfq\Core\Helper\Ldap;
use IMATHUZH\Qfq\Core\Helper\Logger;
17
use IMATHUZH\Qfq\Core\Helper\OnArray;
Marc Egger's avatar
Marc Egger committed
18
use IMATHUZH\Qfq\Core\Helper\Sanitize;
19
20
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Report\Link;
Marc Egger's avatar
Marc Egger committed
21
use IMATHUZH\Qfq\Core\Report\Report;
22
23
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
24

25
/**
Carsten  Rose's avatar
Carsten Rose committed
26
27
 * Class AbstractBuildForm
 * @package qfq
28
 */
29
abstract class AbstractBuildForm {
30
31
32
    /**
     * @var array
     */
33
    protected $formSpec = array();  // copy of the loaded form
34
35
36
    /**
     * @var array
     */
37
    protected $feSpecAction = array(); // copy of all formElement.class='action' of the loaded form
38
39
40
    /**
     * @var array
     */
41
    protected $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form
42
43
44
    /**
     * @var array
     */
45
    protected $buildElementFunctionName = array();
46
47
48
    /**
     * @var array
     */
49
    protected $pattern = array();
50
51
52
    /**
     * @var array
     */
53
    protected $wrap = array();
54
55
56
    /**
     * @var array
     */
57
    protected $symbol = array();
58
59
60
    /**
     * @var bool
     */
61
    protected $showDebugInfoFlag = false;
Carsten  Rose's avatar
Carsten Rose committed
62

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

65
66
67
68
69
70
71
72
    /**
     * @var Store
     */
    protected $store = null;
    /**
     * @var Evaluate
     */
    protected $evaluate = null;
73
74
75
    /**
     * @var string
     */
76
    private $formId = null;
77
78
79
80
    /**
     * @var Sip
     */
    private $sip = null;
81
82
83
84
    /**
     * @var Link
     */
    protected $link = null;
85
86
87
88
89
90
91
92
    /**
     * @var Report
     */
    private $report = null;
    /**
     * @var BodytextParser
     */
    private $bodytextParser = null;
93

94
    /**
95
     * @var Database[] Array of Database instantiated class
96
97
98
     */
    protected $dbArray = array();

99
100
101
    /**
     * @var bool|mixed
     */
102
    protected $dbIndexData = false;
103
104
105
    /**
     * @var bool|string
     */
106
    protected $dbIndexQfq = false;
Carsten  Rose's avatar
Carsten Rose committed
107

108
109
110
111
112
113
    /**
     * AbstractBuildForm constructor.
     *
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
114
     * @param array Database $db
Marc Egger's avatar
Marc Egger committed
115
116
117
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
118
     */
119
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $db = null) {
120
121
122
123
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
124
125
//        $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM);
        $this->dbIndexData = $formSpec[F_DB_INDEX];
126
127
128
129
        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);

        $this->dbArray = $db;
        $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
130
        $this->showDebugInfoFlag = Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $this->store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM));
131

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

134
135
        $this->link = new Link($this->sip, $this->dbIndexData);

136
        // render mode specific
137
        $this->fillWrap();
138
139

        $this->buildElementFunctionName = [
140
            FE_TYPE_CHECKBOX => 'Checkbox',
Carsten  Rose's avatar
Carsten Rose committed
141
            FE_TYPE_DATE => 'DateTime',
142
            FE_TYPE_DATETIME => 'DateTime',
Carsten  Rose's avatar
Carsten Rose committed
143
144
145
146
147
148
149
150
151
            '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',
152
            FE_TYPE_PASSWORD => 'Input',
Carsten  Rose's avatar
Carsten Rose committed
153
154
            FE_TYPE_RADIO => 'Radio',
            FE_TYPE_SELECT => 'Select',
155
            FE_TYPE_SUBRECORD => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
156
            FE_TYPE_UPLOAD => 'File',
157
            FE_TYPE_ANNOTATE => 'Annotate',
158
            FE_TYPE_IMAGE_CUT => 'ImageCut',
Carsten  Rose's avatar
Carsten Rose committed
159
160
161
            'fieldset' => 'Fieldset',
            'pill' => 'Pill',
            'templateGroup' => 'TemplateGroup',
162
163
        ];

164
        $this->buildRowName = [
165
            FE_TYPE_CHECKBOX => 'Native',
Carsten  Rose's avatar
Carsten Rose committed
166
            FE_TYPE_DATE => 'Native',
167
            FE_TYPE_DATETIME => 'Native',
Carsten  Rose's avatar
Carsten Rose committed
168
169
170
171
172
173
174
175
176
            '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',
177
            FE_TYPE_PASSWORD => 'Native',
Carsten  Rose's avatar
Carsten Rose committed
178
179
            FE_TYPE_RADIO => 'Native',
            FE_TYPE_SELECT => 'Native',
180
            FE_TYPE_SUBRECORD => 'Subrecord',
Carsten  Rose's avatar
Carsten Rose committed
181
            FE_TYPE_UPLOAD => 'Native',
182
            FE_TYPE_ANNOTATE => 'Native',
183
            FE_TYPE_IMAGE_CUT => 'Native',
Carsten  Rose's avatar
Carsten Rose committed
184
185
186
            'fieldset' => 'Fieldset',
            'pill' => 'Pill',
            'templateGroup' => 'TemplateGroup',
187
188
        ];

189
        $this->symbol[SYMBOL_EDIT] = "<span class='glyphicon " . GLYPH_ICON_EDIT . "'></span>";
190
        $this->symbol[SYMBOL_SHOW] = "<span class='glyphicon " . GLYPH_ICON_SHOW . "'></span>";
191
192
        $this->symbol[SYMBOL_NEW] = "<span class='glyphicon " . GLYPH_ICON_NEW . "'></span>";
        $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon " . GLYPH_ICON_DELETE . "'></span>";
193
194
    }

195
196
    abstract public function fillWrap();

197
    /**
198
     * Builds complete 'form'. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
199
     *
200
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
201
     *
202
203
204
     * @param bool $htmlElementNameIdZero
     * @param array $latestFeSpecNative
     * @return array|string $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all
205
     *                        formElement.dynamicUpdate-yes  values/states
Marc Egger's avatar
Marc Egger committed
206
207
208
209
210
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
211
212
213
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
214
     */
215
    public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) {
Carsten  Rose's avatar
Carsten Rose committed
216
217
        $htmlHead = '';
        $htmlTail = '';
218
        $htmlT3vars = '';
Carsten  Rose's avatar
Carsten Rose committed
219
220
221
        $htmlSubrecords = '';
        $htmlElements = '';
        $json = array();
222

223
        // After action 'afterSave', it's necessary to reinitialize the FeSpecNative
224
225
226
227
        if (!empty($latestFeSpecNative)) {
            $this->feSpecNative = $latestFeSpecNative;
        }

228
229
230
231
232
233
234
        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

236
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
237
238
239
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
        }
240

241
        $filter = $this->getProcessFilter();
242

243
        if ($this->formSpec[F_MULTI_SQL] !== '') {
244

245
246
247
248
249
250
251
252
253
254
255
256
257
258
            $parentRecords = $this->evaluate->parse($this->formSpec[F_MULTI_SQL], ROW_REGULAR);
            if (!empty($parentRecords)) {
                // Check for 'id' or '_id' as column name
                $idName = F_MULTI_COL_ID;
                if (isset($parentRecords[0]['_' . F_MULTI_COL_ID])) {
                    $idName = '_' . F_MULTI_COL_ID;
                }

                // Check that an column 'id' is given
                if (!isset($parentRecords[0][$idName])) {
                    throw new \UserFormException(
                        json_encode([ERROR_MESSAGE_TO_USER => 'Missing column "_' . F_MULTI_COL_ID . '"', ERROR_MESSAGE_TO_DEVELOPER => $this->formSpec[F_MULTI_SQL]]),
                        ERROR_INVALID_OR_MISSING_PARAMETER);
                }
259

260
261
262
263
264
265
266
                foreach ($parentRecords as $row) {
                    $this->store->setStore($row, STORE_PARENT_RECORD, true);
                    $jsonTmp = array();
                    $htmlElements .= $this->elements($row[$idName], $filter, 0, $jsonTmp, $modeCollectFe);
                    $json[] = $jsonTmp;
                }
            }
267
        } else {
268

269
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
270
            if (!($recordId == '' || is_numeric($recordId))) {
Marc Egger's avatar
Marc Egger committed
271
                throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
272
                    json_encode([ERROR_MESSAGE_TO_USER => 'Invalid record ID', ERROR_MESSAGE_TO_DEVELOPER => 'Invalid record ID: r="' . $recordId]),
273
                    ERROR_INVALID_VALUE);
274
            }
275

276
            // Build FormElements
277
            $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode);
Carsten  Rose's avatar
Carsten Rose committed
278
279

            if ($mode === FORM_SAVE && $recordId != 0) {
280
281
282

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

                // Via 'element-update'
286
                $json[][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5][API_ELEMENT_ATTRIBUTE]['value'] = $md5;
Carsten  Rose's avatar
Carsten Rose committed
287
            }
288
        }
289
290
291

        // <form>
        if ($mode === FORM_LOAD) {
292
            $htmlT3vars = $this->prepareT3VarsForSave();
293
            $htmlTail = $this->tail();
294
295
            $htmlSubrecords = $this->doSubrecords();
        }
296
        $htmlHidden = $this->buildAdditionalFormElements();
297

298
299
        $htmlSip = $this->buildHiddenSip($json);

300
        return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail . $htmlSubrecords : $json;
301
302
    }

303
    /**
304
     * Builds the head area of the form.
305
     *
306
     * @param string $mode
307
     * @return string
Marc Egger's avatar
Marc Egger committed
308
309
310
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
311
     */
312
    public function head($mode = FORM_LOAD) {
313
        $html = '';
314

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

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

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

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

325
326
327
        $html .= $this->getFormTag();

        return $html;
328
329
    }

330
    /**
331
332
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of
     * the SIP.
333
     *
334
     * @param string $form FORM_NAME_FORM | FORM_NAME_FORM_ELEMENT
Carsten  Rose's avatar
Carsten Rose committed
335
336
     * @param int $recordId id of form or formElement
     * @param array $param
337
338
339
     *
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:...,
     *                ...]</small>
Marc Egger's avatar
Marc Egger committed
340
341
     * @throws \CodeException
     * @throws \UserFormException
342
     */
343
    public function createFormEditorUrl($form, $recordId, array $param = array()) {
344

345
        if (!$this->showDebugInfoFlag) {
346
347
            return '';
        }
348

349
        $queryStringArray = [
350
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
351
            'form' => $form,
Carsten  Rose's avatar
Carsten Rose committed
352
            'r' => $recordId,
353
            PARAM_DB_INDEX_DATA => $this->dbIndexQfq,
354
        ];
355
        $queryStringArray = array_merge($queryStringArray, $param);
356

357
        $queryString = Support::arrayToQueryString($queryStringArray);
358

359
360
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
361

362
        return $url;
363
364
365
    }

    /**
366
367
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
Carsten  Rose's avatar
Carsten Rose committed
368
369
     * @param string $item
     * @param string $value
370
     * @param bool|false $flagOmitEmpty
371
     *
372
373
374
     * @return string
     */
    public function wrapItem($item, $value, $flagOmitEmpty = false) {
375
376

        if ($flagOmitEmpty && $value === "") {
377
            return '';
378
379
        }

380
381
382
383
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
384
     * Returns '<form ...>'-tag with various attributes.
385
386
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
387
388
389
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
390
391
     */
    public function getFormTag() {
Carsten  Rose's avatar
Carsten Rose committed
392
        $md5 = '';
393

394
        $attribute = $this->getFormTagAttributes();
395

396
397
        $honeypot = $this->getHoneypotVars();

398
        $md5 = $this->buildInputRecordHashMd5();
Carsten  Rose's avatar
Carsten Rose committed
399
400
401
402
403
404

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

    /**
     * Build MD5 from the current record. Return HTML Input element.
405
     *
406
     * @return string
Marc Egger's avatar
Marc Egger committed
407
408
409
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
410
     */
411
    public function buildInputRecordHashMd5() {
412

Carsten  Rose's avatar
Carsten Rose committed
413
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
414
        $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);
415
416

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

418
//        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='text' value='$md5'>";
419
420
421
422
423
424
425
426

        return $data;
    }


    /**
     * @param $tableName
     * @param $recordId
427
     * @param string $primaryKey
428
     *
429
     * @return string
Marc Egger's avatar
Marc Egger committed
430
431
432
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
433
     */
434
    public function buildRecordHashMd5($tableName, $recordId, $primaryKey = F_PRIMARY_KEY_DEFAULT) {
435
        $record = array();
Carsten  Rose's avatar
Carsten Rose committed
436
437

        if ($recordId != 0) {
438
            $record = $this->dbArray[$this->dbIndexData]->sql("SELECT * FROM $tableName WHERE $primaryKey=?", ROW_EXPECT_1, [$recordId], "Record to load not found.");
Carsten  Rose's avatar
Carsten Rose committed
439
440
        }

441
        return OnArray::getMd5($record);
442
443
    }

444
445
446
447
    /**
     * Create HTML Input vars to detect bot automatic filling of forms.
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
448
449
     * @throws \CodeException
     * @throws \UserFormException
450
451
452
453
454
455
456
457
458
     */
    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) {
459
460
461
462
            $name = trim($name);
            if ($name === '') {
                continue;
            }
463
464
465
466
467
            $html .= "<input name='$name' type='hidden' value='' readonly>";
        }

        return $html;
    }
468

Carsten  Rose's avatar
Carsten Rose committed
469

470
471
472
    /**
     * Build an assoc array with standard form attributes.
     *
473
     * @return array
Marc Egger's avatar
Marc Egger committed
474
475
476
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
477
     */
478
    public function getFormTagAttributes() {
479

480
        $attribute['id'] = $this->getFormId();
481
482
483
484
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
485
        $attribute[FE_INPUT_AUTOCOMPLETE] = 'on';
486
        $attribute['enctype'] = $this->getEncType();
487
        $attribute['data-disable-return-key-submit'] = $this->formSpec[F_ENTER_AS_SUBMIT] == '1' ? "false" : "true"; // attribute meaning is inverted
488
489
490
491

        return $attribute;
    }

492
    /**
Carsten  Rose's avatar
Carsten Rose committed
493
494
     * Return a uniq form id
     *
495
496
497
498
499
500
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
501

502
503
504
        return $this->formId;
    }

505
506
507
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
508
509
     * Notice: the SIP will be transferred as POST Parameter.
     *
510
511
512
513
     * @return string
     */
    public function getActionUrl() {

514
        return API_DIR . '/save.php';
515
516
517
518
519
520
521
522
    }

    /**
     * Determines the enctype.
     *
     * See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
523
524
525
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
526
527
528
     */
    public function getEncType() {

529
        $result = $this->dbArray[$this->dbIndexQfq]->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"');
530

531
532
533
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
534

535
    abstract public function getProcessFilter();
536

537
538
539
540
    /**
     * @param array|string $value
     *
     * @return array|string
Marc Egger's avatar
Marc Egger committed
541
542
543
544
545
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
546
547
548
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
549
     */
550
551
    private function processReportSyntax($value) {

552
553
554
555
556
557
558
559
560
561
562
        if (is_array($value)) {
            $new = array();

            //might happen for e.g Template Groups
            foreach ($value as $item) {
                $new[] = $this->processReportSyntax($item);
            }

            return $new;
        }

563
        $value = trim($value);
564
565
566
567
568
569
570
571
572
        if (substr($value, 0, 8) == SHEBANG_REPORT) {
            if ($this->report === null) {
                $this->report = new Report(array(), $this->evaluate, false);
            }

            if ($this->bodytextParser === null) {
                $this->bodytextParser = new BodytextParser();
            }

573
            $storeRecord = $this->store->getStore(STORE_RECORD);
574
            $value = $this->report->process($this->bodytextParser->process($value));
575
            $this->store->setStore($storeRecord, STORE_RECORD, true);
Carsten  Rose's avatar
Carsten Rose committed
576
            $this->store->setVar(SYSTEM_REPORT_FULL_LEVEL, '', STORE_SYSTEM); // debug
577
578
579
580
581
        }

        return $value;
    }

582
    /**
583
     * Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON.
584
     *
Carsten  Rose's avatar
Carsten Rose committed
585
     * @param int $recordId
586
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
Carsten  Rose's avatar
Carsten Rose committed
587
588
     * @param int $feIdContainer
     * @param array $json
589
     * @param string $modeCollectFe
Carsten  Rose's avatar
Carsten Rose committed
590
     * @param bool $htmlElementNameIdZero
591
     * @param string $storeUseDefault
592
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
593
     *
594
     * @return string
Marc Egger's avatar
Marc Egger committed
595
596
597
598
599
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
600
601
602
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
603
     */
604
    public function elements($recordId, $filter, $feIdContainer, array &$json,
605
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
606
                             $storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD) {
607
        $html = '';
608

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

612
        // get current data record
613
614
        $primaryKey = $this->formSpec[F_PRIMARY_KEY];
        if ($recordId > 0 && $this->store->getVar($primaryKey, STORE_RECORD) === false) {
615
            $tableName = $this->formSpec[F_TABLE_NAME];
616
617
            $row = $this->dbArray[$this->dbIndexData]->sql("SELECT * FROM $tableName WHERE $primaryKey = ?", ROW_EXPECT_1,
                array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$primaryKey'='$recordId' from table '" .
618
                $this->formSpec[F_TABLE_NAME] . "'.");
619
            $this->store->setStore($row, STORE_RECORD);
620
        }
621

622
623
        $this->checkAutoFocus();

624
625
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

626
627
        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
628
            $storeUse = $storeUseDefault;
629

630
631
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
632
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'no')
633
634
635
636
            ) {
                continue; // skip this FE
            }

637
            $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); // type='extra' will not displayed and not transmitted to the form.
638

639
640
            $debugStack = array();

641
642
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
643
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID], STORE_SYSTEM);
644

Carsten  Rose's avatar
Carsten Rose committed
645
646
            // Fill STORE_LDAP
            $fe = $this->prepareFillStoreFireLdap($fe);
647

648
649
650
651
652
653
            if (isset($fe[FE_FILL_STORE_VAR])) {
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_FILL_STORE_VAR, STORE_SYSTEM); // debug
                $fe[FE_FILL_STORE_VAR] = $this->evaluate->parse($fe[FE_FILL_STORE_VAR], ROW_EXPECT_0_1);
                $this->store->appendToStore($fe[FE_FILL_STORE_VAR], STORE_VAR);
            }

654
            // for Upload FormElements, it's necessary to pre-calculate an optional given 'slaveId'.
655
            if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) {
656
                Support::setIfNotSet($fe, FE_SLAVE_ID);
657
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_SLAVE_ID, STORE_SYSTEM); // debug
658
659
                $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
660
661
            }

662
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_VALUE, STORE_SYSTEM); // debug
663
            $fe[FE_VALUE] = $this->processReportSyntax($fe[FE_VALUE]);
664
665

            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_NOTE, STORE_SYSTEM); // debug
666
667
            $fe[FE_NOTE] = $this->processReportSyntax($fe[FE_NOTE]);

Carsten  Rose's avatar
Carsten Rose committed
668
            // ** evaluate current FormElement **
669
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Some of the columns of current FormElement', STORE_SYSTEM); // debug
670
            $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack);
671
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Set language', STORE_SYSTEM); // debug
672
            $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName);
673

674
            // Some Defaults
675
            $formElement = Support::setFeDefaults($formElement, $this->formSpec);
676

677
678
679
680
681
682
//            // Copy global readonly mode.
//            if ($this->formSpec[F_MODE] == F_MODE_READONLY) {
//                $fe[FE_MODE] = FE_MODE_READONLY;
//                $fe[FE_MODE_SQL] = '';
//            }

683
            if ($flagOutput === true) {
684
                $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]);
685
            }
686

687
688
689
            $value = '';
            Support::setIfNotSet($formElement, FE_VALUE);

690
691
692
            if (is_array($formElement[FE_VALUE])) {
                $formElement[FE_VALUE] = (count($formElement[FE_VALUE])) > 0 ? current($formElement[FE_VALUE][0]) : '';
            }
693

694
            $value = $formElement[FE_VALUE];
695

696
            if ($value === '') {
697
698
699
700
                // #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
701
                }
702
703
704
705

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

706
                // Retrieve value via FSRVD
707
                $sanitizeClass = ($mode == FORM_UPDATE) ? SANITIZE_ALLOW_ALL : $formElement[FE_CHECK_TYPE];
708
                $value = $this->store->getVar($name, $storeUse, $sanitizeClass, $foundInStore);
709
710
711
            }

            if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
712
713
//                $value = htmlspecialchars_decode($value, ENT_QUOTES);
                $value = Support::htmlEntityEncodeDecode(MODE_DECODE, $value);
714
            }
715

716
717
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
718
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, ($htmlElementNameIdZero) ? 0 : $recordId);
719
720
721
            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->formSpec[F_ID], $formElement[FE_ID],
                ($htmlElementNameIdZero) ? 0 : $recordId,
                $formElement[FE_TG_INDEX]);
722

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

Carsten  Rose's avatar
Carsten Rose committed
726
            $jsonElement = array();
727
            $elementExtra = '';
728
            // Render pure element
729
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
730
731

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
732
            if ($formElement[FE_CLASS] == FE_CLASS_CONTAINER) {
Carsten  Rose's avatar
Carsten Rose committed
733
734
735
736
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
737
                // for non-container elements: just add the current json status
738
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
739
740
741
742
743
744
                    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
745
746
                }
            }
747

748
749
            if ($flagOutput) {
                // debugStack as Tooltip
750
                if ($this->showDebugInfoFlag) {
751
752
753
754
755
                    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
756
                    $feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
757
758
                    $titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
                    $icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
759
                    $elementHtml .= Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
760
                }
761

762
763
                // Construct Marshaller Name: buildRow
                $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
764

765
                $html .= $formElement[FE_HTML_BEFORE] . $this->$buildRowName($formElement, $elementHtml, $htmlFormElementName) . $formElement[FE_HTML_AFTER];
766
            }
767
        }
768

769
770
        $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, '', STORE_SYSTEM); // debug

771
772
773
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

774
775
776
        return $html;
    }

777
    /**
Carsten  Rose's avatar
Carsten Rose committed
778
779
780
781
782
     * Checks if LDAP search is requested.
     * Yes: prepare configuration and fire the query.
     * No: do nothing.
     *
     * @param array $formElement
783
     *
Carsten  Rose's avatar
Carsten Rose committed
784
     * @return array
Marc Egger's avatar
Marc Egger committed
785
786
787
788
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
789
790
791
792
     */
    private function prepareFillStoreFireLdap(array $formElement) {

        if (isset($formElement[FE_FILL_STORE_LDAP]) || isset($formElement[FE_TYPEAHEAD_LDAP])) {
793
794
795
            $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,
796
                F_TYPEAHEAD_LDAP_ID_PRINTF, F_LDAP_TIME_LIMIT, F_LDAP_USE_BIND_CREDENTIALS];
797
            $formElement = OnArray::copyArrayItemsIfNotAlreadyExist($this->formSpec, $formElement, $keyNames);
Carsten  Rose's avatar
Carsten Rose committed
798
799
800
801
802
803
804
        } else {
            return $formElement; // nothing to do.
        }

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

            // Extract necessary elements
805
            $config = OnArray::getArrayItems($formElement, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES]);
806
807
808

            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN,
                FE_LDAP_SERVER . ',' . FE_LDAP_BASE_DN . ',' . FE_LDAP_SEARCH . ',' . FE_LDAP_ATTRIBUTES, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
809
810
            $config = $this->evaluate->parseArray($config);

811
            if ($formElement[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
812
813
814
815
                $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
816
817
818
819
820
821
822
823
            $ldap = new Ldap();
            $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
            $this->store->setStore($arr, STORE_LDAP, true);
        }

        return $formElement;
    }

824
825
826
827
828
829
830
    /**
     * 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.
831
832
     *   Reason: checks happens per pill - if there is no explizit definition on the first pill, take the first
     *   editable element of that pill.
833
834
835
836
837
838
839
840
841
842
843
844
845
     */
    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 &&
846
847
                ($this->feSpecNative[$i][FE_MODE] == FE_MODE_SHOW || $this->feSpecNative[$i][FE_MODE] == FE_MODE_REQUIRED ||
                    $this->feSpecNative[$i][FE_MODE] == FE_MODE_SHOW_REQUIRED)
848
849
850
851
852
853
854
855
856
            ) {
                // 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;
857

858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
                    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
875
    /**
876
877
878
     * @param $label
     * @param $input
     * @param $note
Carsten  Rose's avatar
Carsten Rose committed
879
     */
880
881
    abstract public function fillWrapLabelInputNote($label, $input, $note);

882
    /**
883
884
     * Copy a subset of current STORE_TYPO3 variables to SIP. Set a hidden form field to submit the assigned SIP to
     * save/update.
885
     *
Carsten  Rose's avatar
Carsten Rose committed
886
     * @return string
Marc Egger's avatar