AbstractBuildForm.php 173 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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302

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

        // Per row, iterate over all form elements
        foreach ($parentRecords as $row) {
            $this->store->setStore($row, STORE_PARENT_RECORD, true);
            $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);
            $this->feSpecNative = $feTmp;
            $rcJson[] = $jsonTmp;
        }

        //TODO <th>: 'class' konfigurierbar machen. Spalten ausblendbar machen. 'link' Klasse unterstuetzen.
        $tableHead = Support::wrapTag('<tr>', $this->buildMultiFormTableHead($parentRecords[0]));

        //TODO <table>: 'class' konfigurierbar machen (tablesorter)
        //TODO <thead>: 'class' konfigurierbar machen (sticky)
        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
     */
    private function buildMultiFormTableHead(array $row) {
        $line = '';

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

        // Collect label from FormElements
        foreach ($this->feSpecNative as $fe) {
            $line .= '<th>' . $fe[FE_LABEL] . '</th>';
        }

        return $line;
    }

303
    /**
304
     * Builds complete 'form'. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
305
     *
306
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
307
     *
308
309
310
     * @param bool $htmlElementNameIdZero
     * @param array $latestFeSpecNative
     * @return array|string $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all
311
     *                        formElement.dynamicUpdate-yes  values/states
Marc Egger's avatar
Marc Egger committed
312
313
314
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
315
316
317
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
318
319
320
321
322
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
323
     */
324
    public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) {
Carsten  Rose's avatar
Carsten Rose committed
325
326
        $htmlHead = '';
        $htmlTail = '';
327
        $htmlT3vars = '';
Carsten  Rose's avatar
Carsten Rose committed
328
329
        $htmlElements = '';
        $json = array();
330

331
        // After action 'afterSave', it's necessary to reinitialize the FeSpecNative
332
333
334
335
        if (!empty($latestFeSpecNative)) {
            $this->feSpecNative = $latestFeSpecNative;
        }

336
337
338
339
340
341
342
        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

344
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
345
346
347
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
        }
348

349
        $filter = $this->getProcessFilter();
350

351
352
        // Form type
        if ($this->formSpec[F_MULTI_SQL] === '') {
353

354
            // Regular Form
355
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
356
            if (!($recordId == '' || is_numeric($recordId))) {
Marc Egger's avatar
Marc Egger committed
357
                throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
358
                    json_encode([ERROR_MESSAGE_TO_USER => 'Invalid record ID', ERROR_MESSAGE_TO_DEVELOPER => 'Invalid record ID: r="' . $recordId]),
359
                    ERROR_INVALID_VALUE);
360
            }
361

362
            // Build FormElements
363
            $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode);
Carsten  Rose's avatar
Carsten Rose committed
364
365

            if ($mode === FORM_SAVE && $recordId != 0) {
366
367
368

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

                // Via 'element-update'
372
                $json[][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5][API_ELEMENT_ATTRIBUTE]['value'] = $md5;
Carsten  Rose's avatar
Carsten Rose committed
373
            }
374
375
376
377
378
379

        } else {
            // Multi Form
            if ($mode === FORM_LOAD) {
                $htmlElements = $this->buildMultiForm($filter, $modeCollectFe, $json);
            }
380
        }
381
382
383

        // <form>
        if ($mode === FORM_LOAD) {
384
            $htmlT3vars = $this->prepareT3VarsForSave();
385
            $htmlTail = $this->tail();
386
        }
387
        $htmlHidden = $this->buildAdditionalFormElements();
388

389
390
        $htmlSip = $this->buildHiddenSip($json);

391
        return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail : $json;
392
393
    }

394
    /**
395
     * Builds the head area of the form.
396
     *
397
     * @param string $mode
398
     * @return string
Marc Egger's avatar
Marc Egger committed
399
400
401
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
402
     */
403
    public function head($mode = FORM_LOAD) {
404
        $html = '';
405

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

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

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

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

416
417
418
        $html .= $this->getFormTag();

        return $html;
419
420
    }

421
    /**
422
423
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of
     * the SIP.
424
     *
425
     * @param string $form FORM_NAME_FORM | FORM_NAME_FORM_ELEMENT
Carsten  Rose's avatar
Carsten Rose committed
426
427
     * @param int $recordId id of form or formElement
     * @param array $param
428
429
430
     *
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:...,
     *                ...]</small>
Marc Egger's avatar
Marc Egger committed
431
432
     * @throws \CodeException
     * @throws \UserFormException
433
     */
434
    public function createFormEditorUrl($form, $recordId, array $param = array()) {
435

436
        if (!$this->showDebugInfoFlag) {
437
438
            return '';
        }
439

440
        $queryStringArray = [
441
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
442
            'form' => $form,
Carsten  Rose's avatar
Carsten Rose committed
443
            'r' => $recordId,
444
            PARAM_DB_INDEX_DATA => $this->dbIndexQfq,
445
        ];
446
        $queryStringArray = array_merge($queryStringArray, $param);
447

448
        $queryString = Support::arrayToQueryString($queryStringArray);
449

450
451
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
452

453
        return $url;
454
455
456
    }

    /**
457
458
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
Carsten  Rose's avatar
Carsten Rose committed
459
460
     * @param string $item
     * @param string $value
461
     * @param bool|false $flagOmitEmpty
462
     *
463
464
465
     * @return string
     */
    public function wrapItem($item, $value, $flagOmitEmpty = false) {
466
467

        if ($flagOmitEmpty && $value === "") {
468
            return '';
469
470
        }

471
472
473
474
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
475
     * Returns '<form ...>'-tag with various attributes.
476
477
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
478
479
480
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
481
482
     */
    public function getFormTag() {
Carsten  Rose's avatar
Carsten Rose committed
483
        $md5 = '';
484

485
        $attribute = $this->getFormTagAttributes();
486

487
488
        $honeypot = $this->getHoneypotVars();

489
        $md5 = $this->buildInputRecordHashMd5();
Carsten  Rose's avatar
Carsten Rose committed
490
491
492
493
494
495

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

    /**
     * Build MD5 from the current record. Return HTML Input element.
496
     *
497
     * @return string
Marc Egger's avatar
Marc Egger committed
498
499
500
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
501
     */
502
    public function buildInputRecordHashMd5() {
503

Carsten  Rose's avatar
Carsten Rose committed
504
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
505
        $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);
506
507

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

509
//        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='text' value='$md5'>";
510
511
512
513
514
515
516
517

        return $data;
    }


    /**
     * @param $tableName
     * @param $recordId
518
     * @param string $primaryKey
519
     *
520
     * @return string
Marc Egger's avatar
Marc Egger committed
521
522
523
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
524
     */
525
    public function buildRecordHashMd5($tableName, $recordId, $primaryKey = F_PRIMARY_KEY_DEFAULT) {
526
        $record = array();
Carsten  Rose's avatar
Carsten Rose committed
527
528

        if ($recordId != 0) {
529
            $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
530
531
        }

532
        return OnArray::getMd5($record);
533
534
    }

535
536
537
538
    /**
     * Create HTML Input vars to detect bot automatic filling of forms.
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
539
540
     * @throws \CodeException
     * @throws \UserFormException
541
542
543
544
545
546
547
548
549
     */
    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) {
550
551
552
553
            $name = trim($name);
            if ($name === '') {
                continue;
            }
554
555
556
557
558
            $html .= "<input name='$name' type='hidden' value='' readonly>";
        }

        return $html;
    }
559

Carsten  Rose's avatar
Carsten Rose committed
560

561
562
563
    /**
     * Build an assoc array with standard form attributes.
     *
564
     * @return array
Marc Egger's avatar
Marc Egger committed
565
566
567
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
568
     */
569
    public function getFormTagAttributes() {
570

571
        $attribute['id'] = $this->getFormId();
572
573
574
575
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
576
        $attribute[FE_INPUT_AUTOCOMPLETE] = 'on';
577
        $attribute['enctype'] = $this->getEncType();
578
        $attribute['data-disable-return-key-submit'] = $this->formSpec[F_ENTER_AS_SUBMIT] == '1' ? "false" : "true"; // attribute meaning is inverted
579
580
581
582

        return $attribute;
    }

583
    /**
Carsten  Rose's avatar
Carsten Rose committed
584
585
     * Return a uniq form id
     *
586
587
588
589
590
591
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
592

593
594
595
        return $this->formId;
    }

596
597
598
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
599
600
     * Notice: the SIP will be transferred as POST Parameter.
     *
601
602
603
604
     * @return string
     */
    public function getActionUrl() {

605
        return API_DIR . '/save.php';
606
607
608
609
610
611
612
613
    }

    /**
     * Determines the enctype.
     *
     * See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
614
615
616
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
617
618
619
     */
    public function getEncType() {

620
        $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"');
621

622
623
624
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
625

626
    abstract public function getProcessFilter();
627

628
629
630
631
    /**
     * @param array|string $value
     *
     * @return array|string
Marc Egger's avatar
Marc Egger committed
632
633
634
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
635
636
637
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
638
639
640
641
642
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
643
     */
644
645
    private function processReportSyntax($value) {

646
647
648
649
650
651
652
653
654
655
656
        if (is_array($value)) {
            $new = array();

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

            return $new;
        }

657
        $value = trim($value);
658
659
660
661
662
663
664
665
666
        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();
            }

667
            $storeRecord = $this->store->getStore(STORE_RECORD);
668
            $value = $this->report->process($this->bodytextParser->process($value));
669
            $this->store->setStore($storeRecord, STORE_RECORD, true);
Carsten  Rose's avatar
Carsten Rose committed
670
            $this->store->setVar(SYSTEM_REPORT_FULL_LEVEL, '', STORE_SYSTEM); // debug
671
672
673
674
675
        }

        return $value;
    }

676

677
    /**
678
     * Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON.
679
     *
Carsten  Rose's avatar
Carsten Rose committed
680
     * @param int $recordId
681
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
Carsten  Rose's avatar
Carsten Rose committed
682
683
     * @param int $feIdContainer
     * @param array $json
684
     * @param string $modeCollectFe
Carsten  Rose's avatar
Carsten Rose committed
685
     * @param bool $htmlElementNameIdZero
686
     * @param string $storeUseDefault
687
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
688
     * @param bool $flagMulti
689
     *
690
     * @return string
Marc Egger's avatar
Marc Egger committed
691
692
693
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
694
695
696
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
697
698
699
700
701
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
702
     */
703
    public function elements($recordId, $filter, $feIdContainer, array &$json,
704
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
705
                             $storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD, $flagMulti = false) {
706
        $html = '';
707

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

711
        // get current data record
712
713
        $primaryKey = $this->formSpec[F_PRIMARY_KEY];
        if ($recordId > 0 && $this->store->getVar($primaryKey, STORE_RECORD) === false) {
714
            $tableName = $this->formSpec[F_TABLE_NAME];
715
716
            $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 '" .
717
                $this->formSpec[F_TABLE_NAME] . "'.");
718
            $this->store->setStore($row, STORE_RECORD);
719
        }
720

721
722
        $this->checkAutoFocus();

723
724
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

725
726
        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
727
            $storeUse = $storeUseDefault;
728

729
730
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
731
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'no')
732
733
734
735
            ) {
                continue; // skip this FE
            }

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

738
739
            $debugStack = array();

740
741
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
742
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID], STORE_SYSTEM);
743

Carsten  Rose's avatar
Carsten Rose committed
744
745
            // Fill STORE_LDAP
            $fe = $this->prepareFillStoreFireLdap($fe);
746

747
748
749
750
751
752
            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);
            }

753
            // for Upload FormElements, it's necessary to pre-calculate an optional given 'slaveId'.
754
            if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) {
755
                Support::setIfNotSet($fe, FE_SLAVE_ID);
756
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_SLAVE_ID, STORE_SYSTEM); // debug
757
758
                $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
759
760
            }

761
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_VALUE, STORE_SYSTEM); // debug
762
            $fe[FE_VALUE] = $this->processReportSyntax($fe[FE_VALUE]);
763
764

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

Carsten  Rose's avatar
Carsten Rose committed
767
            // ** evaluate current FormElement **
768
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Some of the columns of current FormElement', STORE_SYSTEM); // debug
769
            $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack);
770
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Set language', STORE_SYSTEM); // debug
771
            $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName);
772

773
            // Some Defaults
774
            $formElement = Support::setFeDefaults($formElement, $this->formSpec);
775

776
777
778
779
780
781
//            // Copy global readonly mode.
//            if ($this->formSpec[F_MODE] == F_MODE_READONLY) {
//                $fe[FE_MODE] = FE_MODE_READONLY;
//                $fe[FE_MODE_SQL] = '';
//            }

782
            if ($flagOutput === true && !$flagMulti) {
783
                $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]);
784
            }
785

786
787
            Support::setIfNotSet($formElement, FE_VALUE);

788
789
790
            if (is_array($formElement[FE_VALUE])) {
                $formElement[FE_VALUE] = (count($formElement[FE_VALUE])) > 0 ? current($formElement[FE_VALUE][0]) : '';
            }
791

792
            $value = $formElement[FE_VALUE];
793

794
            if ($value === '') {
795
796
797
798
                // #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
799
                }
800
801
802
803

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

804
                // Retrieve value via FSRVD
805
                $sanitizeClass = ($mode == FORM_UPDATE) ? SANITIZE_ALLOW_ALL : $formElement[FE_CHECK_TYPE];
806
                $value = $this->store->getVar($name, $storeUse, $sanitizeClass, $foundInStore);
807
808
809
            }

            if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
810
811
//                $value = htmlspecialchars_decode($value, ENT_QUOTES);
                $value = Support::htmlEntityEncodeDecode(MODE_DECODE, $value);
812
            }
813

814
815
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
816
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, ($htmlElementNameIdZero) ? 0 : $recordId);
817
818
819
            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->formSpec[F_ID], $formElement[FE_ID],
                ($htmlElementNameIdZero) ? 0 : $recordId,
                $formElement[FE_TG_INDEX]);
820

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

Carsten  Rose's avatar
Carsten Rose committed
824
            $jsonElement = array();
825
            $elementExtra = '';
826
            // Render pure element
827
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
828
829

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
830
            if ($formElement[FE_CLASS] == FE_CLASS_CONTAINER) {
Carsten  Rose's avatar
Carsten Rose committed
831
832
833
834
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
835
                // for non-container elements: just add the current json status
836
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
837
838
839
840
841
842
                    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
843
844
                }
            }
845

846
847
            if ($flagOutput) {
                // debugStack as Tooltip
848
                if ($this->showDebugInfoFlag && !$flagMulti) {
849
850
851
852
853
                    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
854
                    $feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
855
856
                    $titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
                    $icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
857
                    $elementHtml .= Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
858
                }
859

860
861
862
                // Construct Marshaller Name: buildRow...
                $tmpName = $flagMulti ? 'MultiElement' : $this->buildRowName[$formElement[FE_TYPE]];
                $buildRowName = 'buildRow' . $tmpName;
863

864
                $html .= $formElement[FE_HTML_BEFORE] . $this->$buildRowName($formElement, $elementHtml, $htmlFormElementName) . $formElement[FE_HTML_AFTER];
865
            }
866
        }
867

868
869
        $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, '', STORE_SYSTEM); // debug

870
871
872
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

873
874
875
        return $html;
    }

876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
    /**
     * @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;
    }


891
    /**
Carsten  Rose's avatar
Carsten Rose committed
892
893
894
895
896
     * Checks if LDAP search is requested.
     * Yes: prepare configuration and fire the query.
     * No: do nothing.
     *
     * @param array $formElement
897
     *
Carsten  Rose's avatar
Carsten Rose committed
898
     * @return array
Marc Egger's avatar
Marc Egger committed
899
900
901
902
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
903
904
905
906
     */
    private function prepareFillStoreFireLdap(array $formElement) {

        if (isset($formElement[FE_FILL_STORE_LDAP]) || isset($formElement[FE_TYPEAHEAD_LDAP])) {
907
908
909
            $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,
910
                F_TYPEAHEAD_LDAP_ID_PRINTF, F_LDAP_TIME_LIMIT, F_LDAP_USE_BIND_CREDENTIALS];
911
            $formElement = OnArray::copyArrayItemsIfNotAlreadyExist($this->formSpec, $formElement, $keyNames);
Carsten  Rose's avatar
Carsten Rose committed
912
913
914
915
916
917
918
        } else {
            return $formElement; // nothing to do.
        }

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

            // Extract necessary elements
919
            $config = OnArray::getArrayItems($formElement, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES]);
920
921
922

            $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
923
924
            $config = $this->evaluate->parseArray($config);

925
            if ($formElement[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
926
927
928
929
                $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
930
931
932
933
934
935
936
937
            $ldap = new Ldap();
            $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
            $this->store->setStore($arr, STORE_LDAP, true);
        }

        return $formElement;
    }

938
939
940
941
942
943
944
    /**
     * 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,
     *