AbstractBuildForm.php 174 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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228

    /**
     * @param $filter
     * @param $modeCollectFe
     * @param array $rcJson
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function buildMultiForm($filter, $modeCollectFe, array $rcJson) {

        $htmlElements = '';
        $rcJson = array();

        $parentRecords = $this->evaluate->parse($this->formSpec[F_MULTI_SQL], ROW_REGULAR);

        // No rows: nothing to do.
        if (empty($parentRecords)) {
            //TODO Meldung konfigurierbar machen.
            return 'No data';
        }

        // Check for 'id' or '_id' as column name
229
        $idName = isset($parentRecords[0]['_' . F_MULTI_COL_ID]) ? '_' . F_MULTI_COL_ID : F_MULTI_COL_ID;
230
231
232
233
234
235
236
237
238
239

        // 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);
        }

        // Per row, iterate over all form elements
        foreach ($parentRecords as $row) {
240

241
            $this->store->setStore($row, STORE_PARENT_RECORD, true);
242

243
244
245
246
247
248
249
250
            $jsonTmp = array();
            $feTmp = $this->feSpecNative;

            $leftColumns = $this->buildMultiFormLeftColumns($row);
            $rightInputs = $this->elements($row[$idName], $filter, 0, $jsonTmp, $modeCollectFe,
                false, STORE_USE_DEFAULT, FORM_LOAD, true);

            $htmlElements .= Support::wrapTag('<tr>', $leftColumns . $rightInputs);
251
252

            // Clean for the next round
253
            $this->feSpecNative = $feTmp;
254
255
            $this->store::unsetStore(STORE_RECORD);

256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
            $rcJson[] = $jsonTmp;
        }

        $tableHead = Support::wrapTag('<tr>', $this->buildMultiFormTableHead($parentRecords[0]));

        return '<table class="table"><thead>' . $tableHead . '</thead><tbody>' . $htmlElements . '</tbody></table>';
    }

    /**
     * @param array $row
     * @return string
     */
    private function buildMultiFormLeftColumns(array $row) {
        $line = '';

        // Collect columns
        foreach ($row as $key => $value) {
            if (($key[0] ?? '') != '_') {
                $line .= "<td>$value</td>";
            }
        }

        return $line;
    }

    /**
     * @param array $row
     * @return string
284
285
     * @throws \CodeException
     * @throws \UserFormException
286
287
288
289
290
291
292
293
294
295
296
297
     */
    private function buildMultiFormTableHead(array $row) {
        $line = '';

        // Collect columns
        foreach ($row as $key => $value) {
            if (($key[0] ?? '') != '_') {
                $line .= "<th>$key</th>";
            }
        }

        // Collect label from FormElements
298
299
300
301
302
303
304
305
306
307
308
309
310
        foreach ($this->feSpecNative as $formElement) {
            $editFeHtml = '';

            // debugStack as Tooltip
            if ($this->showDebugInfoFlag) {
                // Build 'FormElement' Edit symbol
                $feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
                $titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
                $icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
                $editFeHtml = ' ' . Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
            }

            $line .= '<th>' . $formElement[FE_LABEL] . $editFeHtml . '</th>';
311
312
313
314
315
        }

        return $line;
    }

316
    /**
317
     * Builds complete 'form'. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
318
     *
319
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
320
     *
321
322
323
     * @param bool $htmlElementNameIdZero
     * @param array $latestFeSpecNative
     * @return array|string $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all
324
     *                        formElement.dynamicUpdate-yes  values/states
Marc Egger's avatar
Marc Egger committed
325
326
327
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
328
329
330
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
331
332
333
334
335
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
336
     */
337
    public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) {
Carsten  Rose's avatar
Carsten Rose committed
338
339
        $htmlHead = '';
        $htmlTail = '';
340
        $htmlT3vars = '';
Carsten  Rose's avatar
Carsten Rose committed
341
342
        $htmlElements = '';
        $json = array();
343

344
        // After action 'afterSave', it's necessary to reinitialize the FeSpecNative
345
346
347
348
        if (!empty($latestFeSpecNative)) {
            $this->feSpecNative = $latestFeSpecNative;
        }

349
350
351
352
353
354
355
        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

357
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
358
359
360
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
        }
361

362
        $filter = $this->getProcessFilter();
363

364
365
        // Form type
        if ($this->formSpec[F_MULTI_SQL] === '') {
366

367
            // Regular Form
368
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
369
            if (!($recordId == '' || is_numeric($recordId))) {
Marc Egger's avatar
Marc Egger committed
370
                throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
371
                    json_encode([ERROR_MESSAGE_TO_USER => 'Invalid record ID', ERROR_MESSAGE_TO_DEVELOPER => 'Invalid record ID: r="' . $recordId]),
372
                    ERROR_INVALID_VALUE);
373
            }
374

375
            // Build FormElements
376
            $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode);
Carsten  Rose's avatar
Carsten Rose committed
377
378

            if ($mode === FORM_SAVE && $recordId != 0) {
379
380
381

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

                // Via 'element-update'
385
                $json[][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5][API_ELEMENT_ATTRIBUTE]['value'] = $md5;
Carsten  Rose's avatar
Carsten Rose committed
386
            }
387
388
389
390
391
392

        } else {
            // Multi Form
            if ($mode === FORM_LOAD) {
                $htmlElements = $this->buildMultiForm($filter, $modeCollectFe, $json);
            }
393
        }
394
395
396

        // <form>
        if ($mode === FORM_LOAD) {
397
            $htmlT3vars = $this->prepareT3VarsForSave();
398
            $htmlTail = $this->tail();
399
        }
400
        $htmlHidden = $this->buildAdditionalFormElements();
401

402
403
        $htmlSip = $this->buildHiddenSip($json);

404
        return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail : $json;
405
406
    }

407
    /**
408
     * Build the head area of the form.
409
     *
410
     * @param string $mode
411
     * @return string
Marc Egger's avatar
Marc Egger committed
412
413
414
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
415
     */
416
    public function head($mode = FORM_LOAD) {
417
        $html = '';
418

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

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

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

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

429
430
431
        $html .= $this->getFormTag();

        return $html;
432
433
    }

434
    /**
435
436
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of
     * the SIP.
437
     *
438
     * @param string $form FORM_NAME_FORM | FORM_NAME_FORM_ELEMENT
Carsten  Rose's avatar
Carsten Rose committed
439
440
     * @param int $recordId id of form or formElement
     * @param array $param
441
442
443
     *
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:...,
     *                ...]</small>
Marc Egger's avatar
Marc Egger committed
444
445
     * @throws \CodeException
     * @throws \UserFormException
446
     */
447
    public function createFormEditorUrl($form, $recordId, array $param = array()) {
448

449
        if (!$this->showDebugInfoFlag) {
450
451
            return '';
        }
452

453
        $queryStringArray = [
454
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
455
            'form' => $form,
Carsten  Rose's avatar
Carsten Rose committed
456
            'r' => $recordId,
457
            PARAM_DB_INDEX_DATA => $this->dbIndexQfq,
458
        ];
459
        $queryStringArray = array_merge($queryStringArray, $param);
460

461
        $queryString = Support::arrayToQueryString($queryStringArray);
462

463
464
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
465

466
        return $url;
467
468
469
    }

    /**
470
471
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
Carsten  Rose's avatar
Carsten Rose committed
472
473
     * @param string $item
     * @param string $value
474
     * @param bool|false $flagOmitEmpty
475
     *
476
477
478
     * @return string
     */
    public function wrapItem($item, $value, $flagOmitEmpty = false) {
479
480

        if ($flagOmitEmpty && $value === "") {
481
            return '';
482
483
        }

484
485
486
487
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
488
     * Returns '<form ...>'-tag with various attributes.
489
490
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
491
492
493
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
494
495
     */
    public function getFormTag() {
Carsten  Rose's avatar
Carsten Rose committed
496
        $md5 = '';
497

498
        $attribute = $this->getFormTagAttributes();
499

500
501
        $honeypot = $this->getHoneypotVars();

502
        $md5 = $this->buildInputRecordHashMd5();
Carsten  Rose's avatar
Carsten Rose committed
503
504
505
506
507
508

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

    /**
     * Build MD5 from the current record. Return HTML Input element.
509
     *
510
     * @return string
Marc Egger's avatar
Marc Egger committed
511
512
513
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
514
     */
515
    public function buildInputRecordHashMd5() {
516

Carsten  Rose's avatar
Carsten Rose committed
517
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
518
        $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);
519
520

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

522
//        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='text' value='$md5'>";
523
524
525
526
527
528
529
530

        return $data;
    }


    /**
     * @param $tableName
     * @param $recordId
531
     * @param string $primaryKey
532
     *
533
     * @return string
Marc Egger's avatar
Marc Egger committed
534
535
536
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
537
     */
538
    public function buildRecordHashMd5($tableName, $recordId, $primaryKey = F_PRIMARY_KEY_DEFAULT) {
539
        $record = array();
Carsten  Rose's avatar
Carsten Rose committed
540
541

        if ($recordId != 0) {
542
            $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
543
544
        }

545
        return OnArray::getMd5($record);
546
547
    }

548
549
550
551
    /**
     * Create HTML Input vars to detect bot automatic filling of forms.
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
552
553
     * @throws \CodeException
     * @throws \UserFormException
554
555
556
557
558
559
560
561
562
     */
    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) {
563
564
565
566
            $name = trim($name);
            if ($name === '') {
                continue;
            }
567
568
569
570
571
            $html .= "<input name='$name' type='hidden' value='' readonly>";
        }

        return $html;
    }
572

Carsten  Rose's avatar
Carsten Rose committed
573

574
575
576
    /**
     * Build an assoc array with standard form attributes.
     *
577
     * @return array
Marc Egger's avatar
Marc Egger committed
578
579
580
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
581
     */
582
    public function getFormTagAttributes() {
583

584
        $attribute['id'] = $this->getFormId();
585
586
587
588
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
589
        $attribute[FE_INPUT_AUTOCOMPLETE] = 'on';
590
        $attribute['enctype'] = $this->getEncType();
591
        $attribute['data-disable-return-key-submit'] = $this->formSpec[F_ENTER_AS_SUBMIT] == '1' ? "false" : "true"; // attribute meaning is inverted
592
593
594
595

        return $attribute;
    }

596
    /**
Carsten  Rose's avatar
Carsten Rose committed
597
598
     * Return a uniq form id
     *
599
600
601
602
603
604
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
605

606
607
608
        return $this->formId;
    }

609
610
611
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
612
613
     * Notice: the SIP will be transferred as POST Parameter.
     *
614
615
616
617
     * @return string
     */
    public function getActionUrl() {

618
        return API_DIR . '/save.php';
619
620
621
622
623
624
625
626
    }

    /**
     * Determines the enctype.
     *
     * See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
627
628
629
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
630
631
632
     */
    public function getEncType() {

633
        $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"');
634

635
636
637
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
638

639
    abstract public function getProcessFilter();
640

641
642
643
644
    /**
     * @param array|string $value
     *
     * @return array|string
Marc Egger's avatar
Marc Egger committed
645
646
647
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
648
649
650
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
651
652
653
654
655
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
656
     */
657
658
    private function processReportSyntax($value) {

659
660
661
662
663
664
665
666
667
668
669
        if (is_array($value)) {
            $new = array();

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

            return $new;
        }

670
        $value = trim($value);
671
672
673
674
675
676
677
678
679
        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();
            }

680
            $storeRecord = $this->store->getStore(STORE_RECORD);
681
            $value = $this->report->process($this->bodytextParser->process($value));
682
            $this->store->setStore($storeRecord, STORE_RECORD, true);
Carsten  Rose's avatar
Carsten Rose committed
683
            $this->store->setVar(SYSTEM_REPORT_FULL_LEVEL, '', STORE_SYSTEM); // debug
684
685
686
687
688
        }

        return $value;
    }

689

690
    /**
691
     * Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON.
692
     *
Carsten  Rose's avatar
Carsten Rose committed
693
     * @param int $recordId
694
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
Carsten  Rose's avatar
Carsten Rose committed
695
696
     * @param int $feIdContainer
     * @param array $json
697
     * @param string $modeCollectFe
Carsten  Rose's avatar
Carsten Rose committed
698
     * @param bool $htmlElementNameIdZero
699
     * @param string $storeUseDefault
700
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
701
     * @param bool $flagMulti
702
     *
703
     * @return string
Marc Egger's avatar
Marc Egger committed
704
705
706
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
707
708
709
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
710
711
712
713
714
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
715
     */
716
    public function elements($recordId, $filter, $feIdContainer, array &$json,
717
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
718
                             $storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD, $flagMulti = false) {
719
        $html = '';
720

721
        // The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded.
722
        $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];
723

724
        // get current data record
725
726
        $primaryKey = $this->formSpec[F_PRIMARY_KEY];
        if ($recordId > 0 && $this->store->getVar($primaryKey, STORE_RECORD) === false) {
727
            $tableName = $this->formSpec[F_TABLE_NAME];
728
729
            $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 '" .
730
                $this->formSpec[F_TABLE_NAME] . "'.");
731
            $this->store->setStore($row, STORE_RECORD);
732
        }
733

734
735
        $this->checkAutoFocus();

736
737
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

738
739
        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
740
            $storeUse = $storeUseDefault;
741

742
743
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
744
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'no')
745
746
747
748
            ) {
                continue; // skip this FE
            }

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

751
752
            $debugStack = array();

753
754
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
755
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID], STORE_SYSTEM);
756

Carsten  Rose's avatar
Carsten Rose committed
757
758
            // Fill STORE_LDAP
            $fe = $this->prepareFillStoreFireLdap($fe);
759

760
761
762
763
764
765
            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);
            }

766
            // for Upload FormElements, it's necessary to pre-calculate an optional given 'slaveId'.
767
            if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) {
768
                Support::setIfNotSet($fe, FE_SLAVE_ID);
769
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_SLAVE_ID, STORE_SYSTEM); // debug
770
771
                $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
772
773
            }

774
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_VALUE, STORE_SYSTEM); // debug
775
            $fe[FE_VALUE] = $this->processReportSyntax($fe[FE_VALUE]);
776
777

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

Carsten  Rose's avatar
Carsten Rose committed
780
            // ** evaluate current FormElement **
781
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Some of the columns of current FormElement', STORE_SYSTEM); // debug
782
            $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack);
783
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Set language', STORE_SYSTEM); // debug
784
            $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName);
785

786
            // Some Defaults
787
            $formElement = Support::setFeDefaults($formElement, $this->formSpec);
788

789
790
791
792
793
794
//            // Copy global readonly mode.
//            if ($this->formSpec[F_MODE] == F_MODE_READONLY) {
//                $fe[FE_MODE] = FE_MODE_READONLY;
//                $fe[FE_MODE_SQL] = '';
//            }

795
            if ($flagOutput === true && !$flagMulti) {
796
                $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]);
797
            }
798

799
800
            Support::setIfNotSet($formElement, FE_VALUE);

801
802
803
            if (is_array($formElement[FE_VALUE])) {
                $formElement[FE_VALUE] = (count($formElement[FE_VALUE])) > 0 ? current($formElement[FE_VALUE][0]) : '';
            }
804

805
            $value = $formElement[FE_VALUE];
806

807
            if ($value === '') {
808
809
810
811
                // #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
812
                }
813
814
815
816

                // 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];

817
                // Retrieve value via FSRVD
818
                $sanitizeClass = ($mode == FORM_UPDATE) ? SANITIZE_ALLOW_ALL : $formElement[FE_CHECK_TYPE];
819
                $value = $this->store->getVar($name, $storeUse, $sanitizeClass, $foundInStore);
820
821
822
            }

            if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
823
824
//                $value = htmlspecialchars_decode($value, ENT_QUOTES);
                $value = Support::htmlEntityEncodeDecode(MODE_DECODE, $value);
825
            }
826

827
828
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
829
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, ($htmlElementNameIdZero) ? 0 : $recordId);
830
831
832
            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->formSpec[F_ID], $formElement[FE_ID],
                ($htmlElementNameIdZero) ? 0 : $recordId,
                $formElement[FE_TG_INDEX]);
833

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

Carsten  Rose's avatar
Carsten Rose committed
837
            $jsonElement = array();
838
            $elementExtra = '';
839
            // Render pure element
840
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
841
842

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
843
            if ($formElement[FE_CLASS] == FE_CLASS_CONTAINER) {
Carsten  Rose's avatar
Carsten Rose committed
844
845
846
847
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
848
                // for non-container elements: just add the current json status
849
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
850
851
852
853
854
855
                    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
856
857
                }
            }
858

859
860
            if ($flagOutput) {
                // debugStack as Tooltip
861
862
                if ($this->showDebugInfoFlag) {

863
864
865
866
                    if (count($debugStack) > 0) {
                        $elementHtml .= Support::doTooltip($formElement[FE_HTML_ID] . HTML_ID_EXTENSION_TOOLTIP, implode("\n", $debugStack));
                    }

867
868
869
870
871
872
873
                    if (!$flagMulti) {
                        // Build 'FormElement' Edit symbol. MultiForms: Edit symbol is in thead.
                        $feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
                        $titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
                        $icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
                        $elementHtml .= Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
                    }
874
                }
875

876
877
878
                // Construct Marshaller Name: buildRow...
                $tmpName = $flagMulti ? 'MultiElement' : $this->buildRowName[$formElement[FE_TYPE]];
                $buildRowName = 'buildRow' . $tmpName;
879

880
                $html .= $formElement[FE_HTML_BEFORE] . $this->$buildRowName($formElement, $elementHtml, $htmlFormElementName) . $formElement[FE_HTML_AFTER];
881
            }
882
        }
883

884
885
        $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, '', STORE_SYSTEM); // debug

886
887
888
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

889
890
891
        return $html;
    }

892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
    /**
     * @param array $formElement
     * @param $elementHtml
     *
     * @return string
     */
    public function buildRowMultiElement(array $formElement, $elementHtml) {

        $before = ($formElement[FE_HTML_BEFORE] == '') ? '<td>' : $formElement[FE_HTML_BEFORE];
        $after = ($formElement[FE_HTML_AFTER] == '') ? '</td>' : $formElement[FE_HTML_AFTER];

        return $before . $elementHtml . $after;
    }


907
    /**
Carsten  Rose's avatar
Carsten Rose committed
908
909
910
911
912
     * Checks if LDAP search is requested.
     * Yes: prepare configuration and fire the query.
     * No: do nothing.
     *
     * @param array $formElement
913
     *
Carsten  Rose's avatar
Carsten Rose committed
914
     * @return array
Marc Egger's avatar
Marc Egger committed
915
916
917
918
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
919
920
921
922
     */
    private function prepareFillStoreFireLdap(array $formElement) {

        if (isset($formElement[FE_FILL_STORE_LDAP]) || isset($formElement[FE_TYPEAHEAD_LDAP])) {
923
924
925