AbstractBuildForm.php 160 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\Form\Checkbox;
13
use IMATHUZH\Qfq\Core\Form\FormAsFile;
14
use IMATHUZH\Qfq\Core\Form\TypeAhead;
15
use IMATHUZH\Qfq\Core\Helper\HelperFile;
Marc Egger's avatar
Marc Egger committed
16
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
17
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
Marc Egger's avatar
Marc Egger committed
18
19
use IMATHUZH\Qfq\Core\Helper\Ldap;
use IMATHUZH\Qfq\Core\Helper\Logger;
20
use IMATHUZH\Qfq\Core\Helper\OnArray;
21
use IMATHUZH\Qfq\Core\Helper\Path;
Marc Egger's avatar
Marc Egger committed
22
use IMATHUZH\Qfq\Core\Helper\Sanitize;
23
use IMATHUZH\Qfq\Core\Helper\Support;
24
use IMATHUZH\Qfq\Core\Helper\OnString;
25
use IMATHUZH\Qfq\Core\Report\Link;
Marc Egger's avatar
Marc Egger committed
26
use IMATHUZH\Qfq\Core\Report\Report;
27
28
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
29

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

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

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

99
    /**
Carsten  Rose's avatar
Carsten Rose committed
100
     * @var Database[] - Array of Database instantiated class
101
102
103
     */
    protected $dbArray = array();

104
105
106
    /**
     * @var bool|mixed
     */
107
    protected $dbIndexData = false;
108
109
110
    /**
     * @var bool|string
     */
111
    protected $dbIndexQfq = false;
Carsten  Rose's avatar
Carsten Rose committed
112

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

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

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

139
140
        $this->link = new Link($this->sip, $this->dbIndexData);

141
        // render mode specific
142
        $this->fillWrap();
143
144

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

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

194
        $this->symbol[SYMBOL_EDIT] = "<span class='glyphicon " . GLYPH_ICON_EDIT . "'></span>";
195
        $this->symbol[SYMBOL_SHOW] = "<span class='glyphicon " . GLYPH_ICON_SHOW . "'></span>";
196
197
        $this->symbol[SYMBOL_NEW] = "<span class='glyphicon " . GLYPH_ICON_NEW . "'></span>";
        $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon " . GLYPH_ICON_DELETE . "'></span>";
198
199
    }

200
201
    abstract public function fillWrap();

202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219

    /**
     * @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
     */
220
    private function buildMultiForm($filter, $modeCollectFe, array &$rcJson) {
221
222
223
224
225
226
227
228

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

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

        // No rows: nothing to do.
        if (empty($parentRecords)) {
229
            return $this->formSpec[F_MULTI_MSG_NO_RECORD];
230
231
232
        }

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

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

242
243
244
245
246
247
248
249
250
251
        // This is a dirty workaround for formSave: clear FORM STORE (already outdated values).
        // Otherwise those outdated values will be taken to fill non primary FE in multiForm (which are garbage).
        // Better solution would be to have FORM_STORE in sync.
        $this->store::unsetStore(STORE_FORM);

        $storeVarBase = $this->evaluate->parse($this->formSpec[FE_FILL_STORE_VAR]);
        if (!is_array($storeVarBase)) {
            $storeVarBase = array();
        }

252
253
        // Per row, iterate over all form elements
        foreach ($parentRecords as $row) {
254
255
            // Always start with a clean STORE_VAR
            $this->store->setStore($storeVarBase, STORE_VAR, true);
256

257
            $this->store->setStore($row, STORE_PARENT_RECORD, true);
258
259
            $this->store->setVar(F_MULTI_COL_ID, $row[$idName], STORE_PARENT_RECORD); // In case '_id' is used, both '_id' and 'id' should be accessible.

260
            $record = $this->dbArray[$this->dbIndexData]->sql('SELECT * FROM `' . $this->formSpec[F_TABLE_NAME] . '` WHERE `id`=' . $row[F_MULTI_COL_ID], ROW_EXPECT_1);
261
            $this->store->setStore($record, STORE_RECORD, true);
262

263
264
265
266
267
268
269
270
            $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);
271
272

            // Clean for the next round
273
            $this->feSpecNative = $feTmp;
274
275
            $this->store::unsetStore(STORE_RECORD);

276
            $rcJson = array_merge($rcJson, $jsonTmp);
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
303
        }

        $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
304
305
     * @throws \CodeException
     * @throws \UserFormException
306
307
308
309
310
311
312
313
314
315
316
317
     */
    private function buildMultiFormTableHead(array $row) {
        $line = '';

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

        // Collect label from FormElements
318
319
320
321
322
323
324
325
326
327
328
329
330
        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>';
331
332
333
334
335
        }

        return $line;
    }

336
    /**
337
     * Builds complete 'form'. Depending of form specification, the layout will be 'plain' / 'table' / 'bootstrap'.
338
     *
339
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
340
     *
341
342
343
     * @param bool $htmlElementNameIdZero
     * @param array $latestFeSpecNative
     * @return array|string $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all
344
     *                        formElement.dynamicUpdate-yes  values/states
Marc Egger's avatar
Marc Egger committed
345
346
347
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
348
349
350
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
351
352
353
354
355
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
356
     */
357
    public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) {
Carsten  Rose's avatar
Carsten Rose committed
358
359
        $htmlHead = '';
        $htmlTail = '';
360
        $htmlT3vars = '';
Carsten  Rose's avatar
Carsten Rose committed
361
362
        $htmlElements = '';
        $json = array();
363

364
        // After action 'afterSave', it's necessary to reinitialize the FeSpecNative
365
366
367
368
        if (!empty($latestFeSpecNative)) {
            $this->feSpecNative = $latestFeSpecNative;
        }

369
370
371
372
373
374
375
        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

377
        // <form>
Carsten  Rose's avatar
Carsten Rose committed
378
379
380
        if ($mode === FORM_LOAD) {
            $htmlHead = $this->head();
        }
381

382
        $filter = $this->getProcessFilter();
383

384
385
        // Form type
        if ($this->formSpec[F_MULTI_SQL] === '') {
386

387
            // Regular Form
388
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
389
            if (!($recordId == '' || is_numeric($recordId))) {
Marc Egger's avatar
Marc Egger committed
390
                throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
391
                    json_encode([ERROR_MESSAGE_TO_USER => 'Invalid record ID', ERROR_MESSAGE_TO_DEVELOPER => 'Invalid record ID: r="' . $recordId]),
392
                    ERROR_INVALID_VALUE);
393
            }
394

395
            // Build FormElements
396
            $htmlElements = $this->elements($recordId, $filter, 0, $json, $modeCollectFe, $htmlElementNameIdZero, $storeUse, $mode);
Carsten  Rose's avatar
Carsten Rose committed
397
398

            if ($mode === FORM_SAVE && $recordId != 0) {
399
400
401

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

                // Via 'element-update'
405
                $json[][API_ELEMENT_UPDATE][DIRTY_RECORD_HASH_MD5][API_ELEMENT_ATTRIBUTE]['value'] = $md5;
Carsten  Rose's avatar
Carsten Rose committed
406
            }
407
408
409

        } else {
            // Multi Form
410
            if ($mode === FORM_LOAD || $mode === FORM_SAVE) {
411
412
                $htmlElements = $this->buildMultiForm($filter, $modeCollectFe, $json);
            }
413
        }
414
415
416

        // <form>
        if ($mode === FORM_LOAD) {
417
            $htmlT3vars = $this->prepareT3VarsForSave();
418
            $htmlTail = $this->tail();
419
        }
420
        $htmlHidden = $this->buildAdditionalFormElements();
421

422
423
        $htmlSip = $this->buildHiddenSip($json);

424
        return ($mode === FORM_LOAD) ? $htmlHead . $htmlHidden . $htmlElements . $htmlSip . $htmlT3vars . $htmlTail : $json;
425
426
    }

427
    /**
428
     * Build the head area of the form.
429
     *
430
     * @param string $mode
431
     * @return string
Marc Egger's avatar
Marc Egger committed
432
433
434
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
435
     */
436
    public function head($mode = FORM_LOAD) {
437
        $html = '';
438

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

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

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

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

449
450
451
        $html .= $this->getFormTag();

        return $html;
452
453
    }

454
    /**
455
456
     * If SHOW_DEBUG_INFO=yes: create a link (incl. SIP) to edit the current form. Show also the hidden content of
     * the SIP.
457
     *
458
     * @param string $form FORM_NAME_FORM | FORM_NAME_FORM_ELEMENT
Carsten  Rose's avatar
Carsten Rose committed
459
460
     * @param int $recordId id of form or formElement
     * @param array $param
461
462
463
     *
     * @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:...,
     *                ...]</small>
Marc Egger's avatar
Marc Egger committed
464
465
     * @throws \CodeException
     * @throws \UserFormException
466
     */
467
    public function createFormEditorUrl($form, $recordId, array $param = array()) {
468

469
        if (!$this->showDebugInfoFlag) {
470
471
            return '';
        }
472

473
        $queryStringArray = [
474
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
475
            'form' => $form,
Carsten  Rose's avatar
Carsten Rose committed
476
            'r' => $recordId,
477
            PARAM_DB_INDEX_DATA => $this->dbIndexQfq,
478
        ];
479
        $queryStringArray = array_merge($queryStringArray, $param);
480

481
        $queryString = Support::arrayToQueryString($queryStringArray);
482

483
484
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
485

486
        return $url;
487
488
489
    }

    /**
490
491
     * Wrap's $this->wrap[$item][WRAP_SETUP_START] around $value. If $flagOmitEmpty==true && $value=='': return ''.
     *
Carsten  Rose's avatar
Carsten Rose committed
492
493
     * @param string $item
     * @param string $value
494
     * @param bool|false $flagOmitEmpty
495
     *
496
497
498
     * @return string
     */
    public function wrapItem($item, $value, $flagOmitEmpty = false) {
499
500

        if ($flagOmitEmpty && $value === "") {
501
            return '';
502
503
        }

504
505
506
507
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
508
     * Returns '<form ...>'-tag with various attributes.
509
510
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
511
512
513
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
514
515
     */
    public function getFormTag() {
Carsten  Rose's avatar
Carsten Rose committed
516
        $md5 = '';
517

518
        $attribute = $this->getFormTagAttributes();
519

520
521
        $honeypot = $this->getHoneypotVars();

522
        $md5 = $this->buildInputRecordHashMd5();
Carsten  Rose's avatar
Carsten Rose committed
523
524
525
526
527
528

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

    /**
     * Build MD5 from the current record. Return HTML Input element.
529
     *
530
     * @return string
Marc Egger's avatar
Marc Egger committed
531
532
533
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
534
     */
535
    public function buildInputRecordHashMd5() {
536

Carsten  Rose's avatar
Carsten Rose committed
537
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
538
        $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);
539
540

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

542
//        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='text' value='$md5'>";
543
544
545
546
547
548
549
550

        return $data;
    }


    /**
     * @param $tableName
     * @param $recordId
551
     * @param string $primaryKey
552
     *
553
     * @return string
Marc Egger's avatar
Marc Egger committed
554
555
556
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
557
     */
558
    public function buildRecordHashMd5($tableName, $recordId, $primaryKey = F_PRIMARY_KEY_DEFAULT) {
559
        $record = array();
Carsten  Rose's avatar
Carsten Rose committed
560
561

        if ($recordId != 0) {
562
            $record = $this->dbArray[$this->dbIndexData]->sql("SELECT * FROM `$tableName` WHERE `$primaryKey`=?", ROW_EXPECT_1, [$recordId], "Record to load not found. "
563
                . (FEATURE_FORM_FILE_SYNC ? FormAsFile::errorHintFormImport($tableName) : ''));
564
565
566
567
568
        }

        if (isset($record[F_FILE_STATS])) {
            // why: The column "fileStats" in the Form table is modified when a form is exported to a file but nothing else changes.
            unset($record[F_FILE_STATS]);
Carsten  Rose's avatar
Carsten Rose committed
569
570
        }

571
        return OnArray::getMd5($record);
572
573
    }

574
575
576
577
    /**
     * Create HTML Input vars to detect bot automatic filling of forms.
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
578
579
     * @throws \CodeException
     * @throws \UserFormException
580
581
582
583
584
585
586
587
588
     */
    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) {
589
590
591
592
            $name = trim($name);
            if ($name === '') {
                continue;
            }
593
594
595
596
597
            $html .= "<input name='$name' type='hidden' value='' readonly>";
        }

        return $html;
    }
598

Carsten  Rose's avatar
Carsten Rose committed
599

600
601
602
    /**
     * Build an assoc array with standard form attributes.
     *
603
     * @return array
Marc Egger's avatar
Marc Egger committed
604
605
606
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
607
     */
608
    public function getFormTagAttributes() {
609

610
        $attribute['id'] = $this->getFormId();
611
612
613
614
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
615
        $attribute[FE_INPUT_AUTOCOMPLETE] = 'on';
616
        $attribute['enctype'] = $this->getEncType();
617
        $attribute['data-disable-return-key-submit'] = $this->formSpec[F_ENTER_AS_SUBMIT] == '1' ? "false" : "true"; // attribute meaning is inverted
618
        $attribute['data-activate-first-required-tab'] = $this->formSpec[F_ACTIVATE_FIRST_REQUIRED_TAB] == '1' ? "true" : "false"; // attribute meaning is inverted
619
620
621
622

        return $attribute;
    }

623
    /**
Carsten  Rose's avatar
Carsten Rose committed
624
625
     * Return a uniq form id
     *
626
627
628
629
630
631
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
632

633
634
635
        return $this->formId;
    }

636
637
638
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
639
640
     * Notice: the SIP will be transferred as POST Parameter.
     *
641
642
643
644
     * @return string
     */
    public function getActionUrl() {

645
        return Path::urlApi(API_SAVE_PHP);
646
647
648
649
650
651
652
653
    }

    /**
     * Determines the enctype.
     *
     * See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes
     *
     * @return string
Marc Egger's avatar
Marc Egger committed
654
655
656
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
657
658
659
     */
    public function getEncType() {

660
        $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"');
661

662
663
664
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
665

666
667
668
    /**
     * @return mixed
     */
669
    abstract public function getProcessFilter();
670

671
672
673
674
    /**
     * @param array|string $value
     *
     * @return array|string
Marc Egger's avatar
Marc Egger committed
675
676
677
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
678
679
680
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
681
682
683
684
685
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
686
     */
687
688
    private function processReportSyntax($value) {

689
690
691
692
693
694
695
696
697
698
699
        if (is_array($value)) {
            $new = array();

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

            return $new;
        }

700
        $value = trim($value);
701
702
703
704
705
706
707
708
709
        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();
            }

710
            $storeRecord = $this->store->getStore(STORE_RECORD);
711
            $value = $this->report->process($this->bodytextParser->process($value));
712
            $this->store->setStore($storeRecord, STORE_RECORD, true);
Carsten  Rose's avatar
Carsten Rose committed
713
            $this->store->setVar(SYSTEM_REPORT_FULL_LEVEL, '', STORE_SYSTEM); // debug
714
715
716
717
718
        }

        return $value;
    }

719

720
    /**
721
     * Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON.
722
     *
Carsten  Rose's avatar
Carsten Rose committed
723
     * @param int $recordId
724
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
Carsten  Rose's avatar
Carsten Rose committed
725
726
     * @param int $feIdContainer
     * @param array $json
727
     * @param string $modeCollectFe
Carsten  Rose's avatar
Carsten Rose committed
728
     * @param bool $htmlElementNameIdZero
729
     * @param string $storeUseDefault
730
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
731
     * @param bool $flagMulti
732
     *
733
     * @return string
Marc Egger's avatar
Marc Egger committed
734
735
736
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
737
738
739
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
740
741
742
743
744
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
745
     */
746
    public function elements($recordId, $filter, $feIdContainer, array &$json,
747
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
748
                             $storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD, $flagMulti = false) {
749
        $html = '';
750

751
        // The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded.
752
        $skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER,
753
754
            FE_FILL_STORE_VAR, FE_FILE_DOWNLOAD_BUTTON, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE,
            FE_TYPEAHEAD_TAG_INSERT, FE_TYPEAHEAD_INITIAL_SUGGESTION];
755

756
        // get current data record
757
758
        $primaryKey = $this->formSpec[F_PRIMARY_KEY];
        if ($recordId > 0 && $this->store->getVar($primaryKey, STORE_RECORD) === false) {
759
            $tableName = $this->formSpec[F_TABLE_NAME];
760
            $row = $this->dbArray[$this->dbIndexData]->sql("SELECT * FROM `$tableName` WHERE `$primaryKey` = ?", ROW_EXPECT_1,
761
                array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$primaryKey'='$recordId' from table '" .
762
                $this->formSpec[F_TABLE_NAME] . "'.");
763
            $this->store->setStore($row, STORE_RECORD);
764
        }
765

766
767
        $this->checkAutoFocus();

768
769
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);

770
771
        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
772
            $storeUse = $storeUseDefault;
773

774
775
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
776
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'no')
777
778
779
780
            ) {
                continue; // skip this FE
            }

781
782
783
784
            // #7705. Dirty fix: 'form_save and fresh created tg element can't be updated, cause the HTML id is unknown: skip those.
            if ($mode == FORM_SAVE && false != stripos($fe[FE_NAME], '%d')) {
                continue; // skip this FE
            }
785
            $flagOutput = ($fe[FE_TYPE] !== FE_TYPE_EXTRA); // type='extra' will not displayed and not transmitted to the form.
786

787
788
            $debugStack = array();

789
790
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
791
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID], STORE_SYSTEM);
792

Carsten  Rose's avatar
Carsten Rose committed
793
794
            // Fill STORE_LDAP
            $fe = $this->prepareFillStoreFireLdap($fe);
795

796
797
798
799
800
801
            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);
            }

802
803
            // If given, slaveId will be copied to STORE_VAR
            if (!empty($fe[FE_SLAVE_ID])) {
804
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_SLAVE_ID, STORE_SYSTEM); // debug
805
806
                $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
807
808
            }

809
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_VALUE, STORE_SYSTEM); // debug
810
            $fe[FE_VALUE] = $this->processReportSyntax($fe[FE_VALUE]);
811
812

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

Carsten  Rose's avatar
Carsten Rose committed
815
            // ** evaluate current FormElement **
816
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Some of the columns of current FormElement', STORE_SYSTEM); // debug
817
            $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack);
818
            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, 'Set language', STORE_SYSTEM); // debug
819
            $formElement = HelperFormElement::setLanguage($formElement, $parameterLanguageFieldName);
820

821
            // Some Defaults
822
            $formElement = Support::setFeDefaults($formElement, $this->formSpec);
823

824
825
826
827
828
829
//            // Copy global readonly mode.
//            if ($this->formSpec[F_MODE] == F_MODE_READONLY) {
//                $fe[FE_MODE] = FE_MODE_READONLY;
//                $fe[FE_MODE_SQL] = '';
//            }

830
            if ($flagOutput === true && !$flagMulti) {
831
                $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]);
832
            }
833

834
835
            Support::setIfNotSet($formElement, FE_VALUE);

836
837
838
            if (is_array($formElement[FE_VALUE])) {
                $formElement[FE_VALUE] = (count($formElement[FE_VALUE])) > 0 ? current($formElement[FE_VALUE][0]) : '';
            }
839

840
            $value = $formElement[FE_VALUE];
841

842
            if ($value === '') {
843
844
845
846
                // #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
847
                }
848
849
850
851

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

852
                // Retrieve value via FSRVD
853
                $sanitizeClass = ($mode == FORM_UPDATE) ? SANITIZE_ALLOW_ALL : $formElement[FE_CHECK_TYPE];
854
                $value = $this->store->getVar($name, $storeUse, $sanitizeClass, $foundInStore);
855
856
857
858
859
860

                // For typeAhead fields: perform prefetch to display description instead of key (#5444)
                if ($mode == FORM_SAVE && isset($fe[FE_TYPEAHEAD_SQL_PREFETCH])) {
                    $config = [FE_TYPEAHEAD_SQL_PREFETCH => $fe[FE_TYPEAHEAD_SQL_PREFETCH]];
                    $value = TypeAhead::typeAheadSqlPrefetch($config, $value, $this->dbArray[$this->dbIndexData]);
                }
861
862
863
            }

            if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
864
865
//                $value = htmlspecialchars_decode($value, ENT_QUOTES);
                $value = Support::htmlEntityEncodeDecode(MODE_DECODE, $value);
866
867
            } elseif ($formElement[FE_ENCODE] === FE_ENCODE_SINGLE_TICK) {
                $value = OnString::escapeSingleTickInHtml($value);
868
            }
869

870
871
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
872
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, ($htmlElementNameIdZero) ? 0 : $recordId);
873
874
875
            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->formSpec[F_ID], $formElement[FE_ID],
                ($htmlElementNameIdZero) ? 0 : $recordId,
                $formElement[FE_TG_INDEX]);
876

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

Carsten  Rose's avatar
Carsten Rose committed
880
            $jsonElement = array();
881
            $elementExtra = '';
882
            // Render pure element
883
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
884
885

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
886
            if ($formElement[FE_CLASS] == FE_CLASS_CONTAINER) {
Carsten  Rose's avatar
Carsten Rose committed
887
888
889
890
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
891
                // for non-container elements: just add the current json status
892
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
893
894
895
896
897
898
                    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
899
900
                }
            }
901

902
903
            if ($flagOutput) {
                // debugStack as Tooltip
904
905
                if ($this->showDebugInfoFlag) {

906
907
908
909
                    if (count($debugStack) > 0) {
                        $elementHtml .= Support::doTooltip($formElement[FE_HTML_ID] . HTML_ID_EXTENSION_TOOLTIP, implode("\n", $debugStack));
                    }

910
911
912
913
914
915
916
                    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);
                    }
917
                }
918

919
920
921
                // Construct Marshaller Name: buildRow...
                $tmpName = $flagMulti ? 'MultiElement' : $this->buildRowName[$formElement[FE_TYPE]];
                $buildRowName = 'buildRow' . $tmpName;
922

923
                $html .= $formElement[FE_HTML_BEFORE] . $this->$buildRowName($formElement, $elementHtml, $htmlFormElementName) . $formElement[FE_HTML_AFTER];
924
            }
925
        }
926

927
928
        $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, '', STORE_SYSTEM); // debug