AbstractBuildForm.php 134 KB
Newer Older
1
<?php
Carsten  Rose's avatar
Carsten Rose committed
2
3
4
5
6
7
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/6/16
 * Time: 8:02 PM
 */
Carsten  Rose's avatar
Carsten Rose committed
8

9
10
11
namespace qfq;

use qfq;
Carsten  Rose's avatar
Carsten Rose committed
12
13
14
15

//use qfq\Store;
//use qfq\OnArray;
//use qfq\UserFormException;
16

17
18
19
20
21
22
23
24
25
26
require_once(__DIR__ . '/store/Store.php');
require_once(__DIR__ . '/Constants.php');
require_once(__DIR__ . '/exceptions/DbException.php');
require_once(__DIR__ . '/exceptions/UserFormException.php');
require_once(__DIR__ . '/database/Database.php');
require_once(__DIR__ . '/helper/HelperFormElement.php');
require_once(__DIR__ . '/helper/Support.php');
require_once(__DIR__ . '/helper/OnArray.php');
require_once(__DIR__ . '/helper/Ldap.php');
require_once(__DIR__ . '/report/Link.php');
27

28
/**
Carsten  Rose's avatar
Carsten Rose committed
29
30
 * Class AbstractBuildForm
 * @package qfq
31
 */
32
abstract class AbstractBuildForm {
33
34
35
36
37
    protected $formSpec = array();  // copy of the loaded form
    protected $feSpecAction = array(); // copy of all formElement.class='action' of the loaded form
    protected $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form
    protected $buildElementFunctionName = array();
    protected $pattern = array();
38
    protected $wrap = array();
39
    protected $symbol = array();
40
    protected $showDebugInfoFlag = false;
41
    protected $inputCheckPattern = array();
Carsten  Rose's avatar
Carsten Rose committed
42

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

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

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

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

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

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

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

129
130
131
        $this->symbol[SYMBOL_EDIT] = "<span class='glyphicon " . GLYPH_ICON_EDIT . "'></span>";
        $this->symbol[SYMBOL_NEW] = "<span class='glyphicon " . GLYPH_ICON_NEW . "'></span>";
        $this->symbol[SYMBOL_DELETE] = "<span class='glyphicon " . GLYPH_ICON_DELETE . "'></span>";
132

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

136
137
    abstract public function fillWrap();

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

        $modeCollectFe = FLAG_DYNAMIC_UPDATE;
        $storeUse = STORE_USE_DEFAULT;

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

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

168
        $filter = $this->getProcessFilter();
169

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

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

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

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

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

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

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

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

212
213
        $htmlSip = $this->buildHiddenSip($json);

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

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

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

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

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

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

235
236
237
        $html .= $this->getFormTag();

        return $html;
238
239
    }

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

251
        if (!$this->showDebugInfoFlag) {
252
253
            return '';
        }
254

255
        $queryStringArray = [
256
            'id' => $this->store->getVar(SYSTEM_EDIT_FORM_PAGE, STORE_SYSTEM),
257
258
            'form' => $form,
            'r' => $recordId
259
        ];
260
        $queryStringArray = array_merge($queryStringArray, $param);
261

262
        $queryString = Support::arrayToQueryString($queryStringArray);
263

264
265
        $sip = $this->store->getSipInstance();
        $url = $sip->queryStringToSip($queryString);
266

267
        return $url;
268
269
270
    }

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

        if ($flagOmitEmpty && $value === "") {
281
            return '';
282
283
        }

284
285
286
287
        return $this->wrap[$item][WRAP_SETUP_START] . $value . $this->wrap[$item][WRAP_SETUP_END];
    }

    /**
288
     * Returns '<form ...>'-tag with various attributes.
289
290
291
292
     *
     * @return string
     */
    public function getFormTag() {
Carsten  Rose's avatar
Carsten Rose committed
293
        $md5 = '';
294
295
296

        $attribute = $this->getFormTagAtrributes();

297
298
        $honeypot = $this->getHoneypotVars();

299
        $md5 = $this->buildInputRecordHashMd5();
Carsten  Rose's avatar
Carsten Rose committed
300
301
302
303
304
305

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

    /**
     * Build MD5 from the current record. Return HTML Input element.
306
307
308
309
     * @param bool $flagWithSpan
     * @return string
     * @throws \qfq\CodeException
     * @throws \qfq\DbException
Carsten  Rose's avatar
Carsten Rose committed
310
     */
311
    public function buildInputRecordHashMd5() {
312

Carsten  Rose's avatar
Carsten Rose committed
313
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
314
315
316
        $md5 = $this->buildRecordHashMd5($this->formSpec[F_TABLE_NAME], $recordId);

        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='hidden' value='$md5'>";
317
//        $data = "<input id='" . DIRTY_RECORD_HASH_MD5 . "' name='" . DIRTY_RECORD_HASH_MD5 . "' type='text' value='$md5'>";
318
319
320
321
322
323
324
325
326
327
328
329
330
331

        return $data;
    }


    /**
     * @param $tableName
     * @param $recordId
     * @return string
     * @throws \qfq\CodeException
     * @throws \qfq\DbException
     */
    public function buildRecordHashMd5($tableName, $recordId){
        $record = array();
Carsten  Rose's avatar
Carsten Rose committed
332
333

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

337
        return OnArray::getMd5($record);
338
339
    }

340
341
342
343
344
345
346
347
348
349
350
351
352
    /**
     * Create HTML Input vars to detect bot automatic filling of forms.
     *
     * @return string
     */
    public function getHoneypotVars() {
        $html = '';

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

        // Iterate over all fake vars
        $arr = explode(',', $vars);
        foreach ($arr as $name) {
353
354
355
356
            $name = trim($name);
            if ($name === '') {
                continue;
            }
357
358
359
360
361
            $html .= "<input name='$name' type='hidden' value='' readonly>";
        }

        return $html;
    }
362

Carsten  Rose's avatar
Carsten Rose committed
363

364
365
366
    /**
     * Build an assoc array with standard form attributes.
     *
367
     * @return array
368
369
370
     */
    public function getFormTagAtrributes() {

371
        $attribute['id'] = $this->getFormId();
372
373
374
375
376
377
378
379
380
381
        $attribute['method'] = 'post';
        $attribute['action'] = $this->getActionUrl();
        $attribute['target'] = '_top';
        $attribute['accept-charset'] = 'UTF-8';
        $attribute['autocomplete'] = 'on';
        $attribute['enctype'] = $this->getEncType();

        return $attribute;
    }

382
    /**
Carsten  Rose's avatar
Carsten Rose committed
383
384
     * Return a uniq form id
     *
385
386
387
388
389
390
391
392
393
     * @return string
     */
    public function getFormId() {
        if ($this->formId === null) {
            $this->formId = uniqid('qfq-form-');
        }
        return $this->formId;
    }

394
395
396
    /**
     * Builds the HTML 'form'-tag inlcuding all attributes and target.
     *
397
398
     * Notice: the SIP will be transferred as POST Parameter.
     *
399
400
401
402
403
     * @return string
     * @throws DbException
     */
    public function getActionUrl() {

404
        return API_DIR . '/save.php';
405
406
407
408
409
410
411
412
413
414
415
416
    }

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

417
        $result = $this->db->sql("SELECT id FROM FormElement AS fe WHERE fe.formId=? AND fe.type='upload' LIMIT 1", ROW_REGULAR, [$this->formSpec['id']], 'Look for Formelement.type="upload"');
418
419
420
        return (count($result) === 1) ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    }
421

422
    abstract public function getProcessFilter();
423
424

    /**
425
     * Process all FormElements in $this->feSpecNative: Collect and return all HTML code & JSON.
426
     *
Carsten  Rose's avatar
Carsten Rose committed
427
     * @param int    $recordId
428
     * @param string $filter FORM_ELEMENTS_NATIVE | FORM_ELEMENTS_SUBRECORD | FORM_ELEMENTS_NATIVE_SUBRECORD
Carsten  Rose's avatar
Carsten Rose committed
429
430
     * @param int    $feIdContainer
     * @param array  $json
431
     * @param string $modeCollectFe
Carsten  Rose's avatar
Carsten Rose committed
432
     * @param bool   $htmlElementNameIdZero
433
     * @param string $storeUseDefault
434
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
435
     * @return string
436
437
     * @throws CodeException
     * @throws DbException
438
     * @throws \qfq\UserFormException
439
     */
440
    public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, array &$json,
441
                             $modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
442
                             $storeUseDefault = STORE_USE_DEFAULT, $mode = FORM_LOAD) {
443
        $html = '';
444

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

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

450
        // get current data record
451
        if ($recordId > 0 && $this->store->getVar('id', STORE_RECORD) === false) {
452
453
454
            $row = $this->db->sql("SELECT * FROM " . $this->formSpec[F_TABLE_NAME] . " WHERE id = ?", ROW_EXPECT_1,
                array($recordId), "Form '" . $this->formSpec[F_NAME] . "' failed to load record '$recordId' from table '" .
                $this->formSpec[F_TABLE_NAME] . "'.");
455
            $this->store->setStore($row, STORE_RECORD);
456
        }
457

458
459
        $this->checkAutoFocus();

460
461
        // Iterate over all FormElements
        foreach ($this->feSpecNative as $fe) {
462
            $storeUse = $storeUseDefault;
463

464
465
            if (($filter === FORM_ELEMENTS_NATIVE && $fe[FE_TYPE] === 'subrecord')
                || ($filter === FORM_ELEMENTS_SUBRECORD && $fe[FE_TYPE] !== 'subrecord')
466
//                || ($filter === FORM_ELEMENTS_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] === 'no')
467
468
469
470
            ) {
                continue; // skip this FE
            }

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

473
474
            $debugStack = array();

Carsten  Rose's avatar
Carsten Rose committed
475
476
477
478
479
480
            // Copy global readonly mode.
            if ($this->formSpec[F_MODE] == F_MODE_READONLY) {
                $fe[FE_MODE] = FE_MODE_READONLY;
                $fe[FE_MODE_SQL] = '';
            }

481
482
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);
483

Carsten  Rose's avatar
Carsten Rose committed
484
485
            // Fill STORE_LDAP
            $fe = $this->prepareFillStoreFireLdap($fe);
486

487
488
            // for Upload FormElements, it's necessary to precalculate an optional given 'slaveId'.
            if ($fe[FE_TYPE] === FE_TYPE_UPLOAD) {
489
                Support::setIfNotSet($fe, FE_SLAVE_ID);
490
491
                $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR);
492
493
            }

Carsten  Rose's avatar
Carsten Rose committed
494
            // ** evaluate current FormElement **
495
            $formElement = $this->evaluate->parseArray($fe, $skip, $debugStack);
496

497
            // Some Defaults
498
            $formElement = Support::setFeDefaults($formElement, $this->formSpec);
499

500
            if ($flagOutput === true) {
501
                $this->fillWrapLabelInputNote($formElement[FE_BS_LABEL_COLUMNS], $formElement[FE_BS_INPUT_COLUMNS], $formElement[FE_BS_NOTE_COLUMNS]);
502
            }
503

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

507
508
509
            $value = '';
            Support::setIfNotSet($formElement, FE_VALUE);

510
511
512
            if (is_array($formElement[FE_VALUE])) {
                $formElement[FE_VALUE] = (count($formElement[FE_VALUE])) > 0 ? current($formElement[FE_VALUE][0]) : '';
            }
513

514
            $value = $formElement[FE_VALUE];
515

516
            if ($value === '') {
517
518
519
520
                // #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
521
                }
522
523
                // Retrieve value via FSRVD
                $value = $this->store->getVar($name, $storeUse, $formElement[FE_CHECK_TYPE], $foundInStore);
524
525
526
527
            }

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

530
531
            // Typically: $htmlElementNameIdZero = true
            // After Saving a record, staying on the form, the FormElements on the Client are still known as '<feName>:0'.
532
            $htmlFormElementName = HelperFormElement::buildFormElementName($formElement, ($htmlElementNameIdZero) ? 0 : $recordId);
533
534
535
            $formElement[FE_HTML_ID] = HelperFormElement::buildFormElementId($this->formSpec[F_ID], $formElement[FE_ID],
                ($htmlElementNameIdZero) ? 0 : $recordId,
                $formElement[FE_TG_INDEX]);
536

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

Carsten  Rose's avatar
Carsten Rose committed
540
            $jsonElement = array();
541
            $elementExtra = '';
542
            // Render pure element
543
            $elementHtml = $this->$buildElementFunctionName($formElement, $htmlFormElementName, $value, $jsonElement, $mode);
Carsten  Rose's avatar
Carsten Rose committed
544
545

            // container elements do not have dynamicUpdate='yes'. Instead they deliver nested elements.
546
            if ($formElement[FE_CLASS] == FE_CLASS_CONTAINER) {
Carsten  Rose's avatar
Carsten Rose committed
547
548
549
550
                if (count($jsonElement) > 0) {
                    $json = array_merge($json, $jsonElement);
                }
            } else {
551
                // for non-container elements: just add the current json status
552
                if ($modeCollectFe === FLAG_ALL || ($modeCollectFe == FLAG_DYNAMIC_UPDATE && $fe[FE_DYNAMIC_UPDATE] == 'yes')) {
Carsten  Rose's avatar
Carsten Rose committed
553
554
555
556
557
558
                    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
559
560
                }
            }
561

562
563
            if ($flagOutput) {
                // debugStack as Tooltip
564
                if ($this->showDebugInfoFlag) {
565
566
567
568
569
                    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
570
                    $feEditUrl = $this->createFormEditorUrl(FORM_NAME_FORM_ELEMENT, $formElement[FE_ID], ['formId' => $formElement[FE_FORM_ID]]);
571
572
                    $titleAttr = Support::doAttribute('title', $this->formSpec[FE_NAME] . ' / ' . $formElement[FE_NAME] . ' [' . $formElement[FE_ID] . ']');
                    $icon = Support::wrapTag('<span class="' . GLYPH_ICON . ' ' . GLYPH_ICON_EDIT . '">', '');
573
                    $elementHtml .= Support::wrapTag("<a class='hidden " . CLASS_FORM_ELEMENT_EDIT . "' href='$feEditUrl' $titleAttr>", $icon);
574
                }
575

576
577
                // Construct Marshaller Name: buildRow
                $buildRowName = 'buildRow' . $this->buildRowName[$formElement[FE_TYPE]];
578

579
                $html .= $formElement[FE_HTML_BEFORE] . $this->$buildRowName($formElement, $elementHtml, $htmlFormElementName) . $formElement[FE_HTML_AFTER];
580
            }
581
        }
582

583
584
585
        // Log / Debug: Last FormElement has been processed.
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

586
587
588
        return $html;
    }

589
    /**
Carsten  Rose's avatar
Carsten Rose committed
590
591
592
593
594
595
596
597
598
599
600
601
602
     * Checks if LDAP search is requested.
     * Yes: prepare configuration and fire the query.
     * No: do nothing.
     *
     * @param array $formElement
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
    private function prepareFillStoreFireLdap(array $formElement) {
        $config = array();

        if (isset($formElement[FE_FILL_STORE_LDAP]) || isset($formElement[FE_TYPEAHEAD_LDAP])) {
603
604
605
            $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,
606
                F_TYPEAHEAD_LDAP_ID_PRINTF, F_LDAP_TIME_LIMIT, F_LDAP_USE_BIND_CREDENTIALS];
607
            $formElement = OnArray::copyArrayItemsIfNotAlreadyExist($this->formSpec, $formElement, $keyNames);
Carsten  Rose's avatar
Carsten Rose committed
608
609
610
611
612
613
614
        } else {
            return $formElement; // nothing to do.
        }

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

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

618
            if ($formElement[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
619
620
621
622
                $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
623
624
625
626
627
628
629
630
            $ldap = new Ldap();
            $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
            $this->store->setStore($arr, STORE_LDAP, true);
        }

        return $formElement;
    }

631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
    /**
     * Check if there is an explicit 'autofocus' definition in at least one FE.
     * Found: do nothing, it will be rendered at the correct position.
     * Not found: set 'autofocus' on the first FE.
     *
     * Accepted misbehaviour on forms with pills: if there is at least one editable element on the first pill,
     *   the other pills are not checked - independent if there was a definition on the first pill or not.
     *   Reason: checks happens per pill - if there is no explizit definition on the first pill, take the first editable
     *           element of that pill.
     */
    private function checkAutoFocus() {
        static $found = false;
        $idx = false;

        if ($found) {
            return;
        }

        // Search if there is an explicit autofocus definition.
        for ($i = 0; $i < count($this->feSpecNative); ++$i) {
            // Only check native elements which will be shown
            if ($this->feSpecNative[$i][FE_CLASS] == FE_CLASS_NATIVE &&
                ($this->feSpecNative[$i][FE_MODE] == FE_MODE_SHOW || $this->feSpecNative[$i][FE_MODE] == FE_MODE_REQUIRED)
            ) {
                // Check if there is an explicit definition.
                if (isset($this->feSpecNative[$i][FE_AUTOFOCUS])) {
                    if ($this->feSpecNative[$i][FE_AUTOFOCUS] == '' || $this->feSpecNative[$i][FE_AUTOFOCUS] == '1') {
                        $this->feSpecNative[$i][FE_AUTOFOCUS] = '1'; // fix to '=1'
                    } else {
                        unset($this->feSpecNative[$i][FE_AUTOFOCUS]);
                    }
                    $found = true;
                    return;
                }

                if ($idx === false) {
                    $idx = $i;
                }
            }
        }

        // No explicit definition found: take the first found editable element.
        if ($idx !== false) {
            $found = true;
            // No explicit definition found: set autofocus.
            $this->feSpecNative[$idx][FE_AUTOFOCUS] = '1';
        }
    }

Carsten  Rose's avatar
Carsten Rose committed
680
681
682
    /**
     *
     */
683
684
    abstract public function fillWrapLabelInputNote($label, $input, $note);

685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
    /**
     * Copy a subset of current STORE_TYPO3 variables to SIP. Set a hidden form field to submit the assigned SIP to save/update.
     *
     * @throws CodeException
     * @throws UserFormException
     */
    private function prepareT3VarsForSave() {

        $t3VarsSip = $this->store->copyT3VarsToSip();

        return $this->buildNativeHidden(CLIENT_TYPO3VARS, $t3VarsSip);

    }

    /**
     * Builds a real HTML hidden form element. Useful for checkboxes, Multiple-Select and Radios.
     *
Carsten  Rose's avatar
Carsten Rose committed
702
703
     * @param        $htmlFormElementName
     * @param string $value
704
705
     * @return string
     */
706
707
    public function buildNativeHidden($htmlFormElementName, $value) {
        return '<input type="hidden" name="' . $htmlFormElementName . '" value="' . htmlentities($value) . '">';
708
709
    }

Carsten  Rose's avatar
Carsten Rose committed
710
711
712
    /**
     *
     */
713
714
    abstract public function tail();

Carsten  Rose's avatar
Carsten Rose committed
715
716
717
    /**
     *
     */
718
719
    abstract public function doSubrecords();

720
721
722
723
724
725
726
727
728
729
730
731
732
733
    /**
     * Get all elements from STORE_ADDITIONAL_FORM_ELEMENTS and return them as a string.
     *
     * @return string
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
    private function buildAdditionalFormElements() {

        $data = $this->store->getStore(STORE_ADDITIONAL_FORM_ELEMENTS);

        return is_array($data) ? implode('', $data) : '';
    }

734
735
736
    /**
     * Create a hidden sip, based on latest STORE_SIP Values. Return complete HTML 'hidden' element.
     *
737
     * @param array $json
738
739
740
741
     * @return string  <input type='hidden' name='s' value='<sip>'>
     * @throws CodeException
     * @throws \qfq\UserFormException
     */
742
    public function buildHiddenSip(array &$json) {
743

744
        $sipArray = $this->store->getStore(STORE_SIP);
745
746

        // do not include system vars
747
748
749
750
751
752
753
754
        unset($sipArray[SIP_SIP]);
        unset($sipArray[SIP_URLPARAM]);

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

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

755
        $json[] = $this->getFormElementForJson(CLIENT_SIP, $sipValue, [FE_MODE => FE_MODE_SHOW]);
756
757
758
759
760

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

    /**
761
762
763
     * Create an array with standard elements for 'mode' (hidden, disabled, required) and add 'form-element', 'value'.
     * 'Generic Element Update': add via API_ELEMENT_UPDATE 'label' and 'note'.
     * All collected data as array - will be later converted to JSON.
Carsten  Rose's avatar
Carsten Rose committed
764
     *
Carsten  Rose's avatar
Carsten Rose committed
765
     * @param string       $htmlFormElementName
766
     * @param string|array $value
Carsten  Rose's avatar
Carsten Rose committed
767
     * @param array        $formElement
768
769
     * @return array
     */
770
    private function getFormElementForJson($htmlFormElementName, $value, array $formElement) {
771

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

774
        $json[API_FORM_UPDATE_FORM_ELEMENT] = $htmlFormElementName;
775
776
777
778
779
780
781

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

782
        // 'value' update via 'form-update' on the full row: only if there is no other FE in that row
783
        if ($flagRowUpdate) {
784
            $json[API_FORM_UPDATE_VALUE] = $value;
785
        }
786

787
788
        if (isset($formElement[FE_LABEL])) {
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_LABEL;
789
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = $this->buildLabel($htmlFormElementName, $formElement[FE_LABEL]);
790
791
792
793
794
        }

        if (isset($formElement[FE_NOTE])) {
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_NOTE;
            $json[API_ELEMENT_UPDATE][$key][API_ELEMENT_CONTENT] = $formElement[FE_NOTE];
795
        }
796

797
        if (isset($formElement[FE_TYPE])) {
798
            $key = $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_INPUT;
799
800
801
802
803
804
805
806
807
808
809
810

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

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

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

818
        // Show / Hide the complete FormElement Row.
819
820
821
        if (isset($formElement[FE_HTML_ID])) { // HIDDEN_SIP comes without a real existing FE structure.
            // Activate 'show' or 'hidden' on the current FormElement via JSON 'API_ELEMENT_UPDATE'
            $class = $this->wrap[WRAP_SETUP_ELEMENT][WRAP_SETUP_CLASS];
822
            if ($formElement[FE_MODE] == FE_MODE_HIDDEN) {
823
824
825
826
827
828
829
                $class .= ' hidden';
            }

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

830
831
832
        return $json;
    }

833
834
835
836
837
838
839
840
841
    /**
     * Set corresponding JSON attributes readonly/required/disabled, based on $formElement[FE_MODE].
     *
     * @param array $feMode
     * @return array
     * @throws UserFormException
     */
    private function getJsonFeMode($feMode) {

842
        $this->getFeMode($feMode, $dummy, $disabled, $required);
843

844
        return [API_FORM_UPDATE_DISABLED => $disabled === 'yes', API_FORM_UPDATE_REQUIRED => $required === 'yes'];
845
846
847
    }

    /**
Carsten  Rose's avatar
Carsten Rose committed
848
849
     * Depending of $feMode set variables $hidden, $disabled, $required to 'yes' or 'no'.
     *
Carsten  Rose's avatar
Carsten Rose committed
850
851
852
853
     * @param string $feMode
     * @param string $hidden
     * @param string $disabled
     * @param string $required
854
855
856
857
858
859
860
861
862
863
864
865
866
867
     * @throws \qfq\UserFormException
     */
    private function getFeMode($feMode, &$hidden, &$disabled, &$required) {
        $hidden = 'no';
        $disabled = 'no';
        $required = 'no';

        switch ($feMode) {
            case FE_MODE_SHOW:
                break;
            case FE_MODE_REQUIRED:
                $required = 'yes';
                break;
            case FE_MODE_READONLY:
868
                $disabled = 'yes';  // convert the UI status 'readonly' to the HTML/CSS status 'disabled'.
869
870
871
872
873
874
875
876
877
878
                break;
            case FE_MODE_HIDDEN:
                $hidden = 'yes';
                break;
            default:
                throw new UserFormException("Unknown mode '$feMode'", ERROR_UNKNOWN_MODE);
                break;
        }
    }

879
880
881
882
883
    /**
     * Builds a label, typically for an html-'<input>'-element.
     *
     * @param string $htmlFormElementName
     * @param string $label
884
     * @param string $addClass
885
886
     * @return string
     */
887
    public function buildLabel($htmlFormElementName, $label, $addClass = '') {
888
        $attributes = Support::doAttribute('for', $htmlFormElementName);
889
        $attributes .= Support::doAttribute('class', ['control-label', $addClass]);
890
891
892
893
894
895

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

        return $html;
    }

Carsten  Rose's avatar
Carsten Rose committed
896
897
898
899
    /**
     * Takes the current SIP ('form' and additional parameter), set SIP_RECORD_ID=0 and create a new 'NewRecordUrl'.
     *
     * @throws CodeException
900
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
901
902
     */
    public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
903

904
905
906
        $urlParam = $this->store->getNonSystemSipParam();
        $urlParam[SIP_RECORD_ID] = 0;
        $urlParam[SIP_FORM] = $this->formSpec[F_NAME];
907
908

        Support::appendTypo3ParameterToArray($urlParam);
Carsten  Rose's avatar
Carsten Rose committed
909
910
911
912
913

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

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

914
        if ($this->showDebugInfoFlag) {
915
            //TODO: missing decoding of SIP
916
917
            $toolTipNew .= PHP_EOL . PHP_EOL . OnArray::toString($urlParam, ' = ', PHP_EOL, "'");
        }
Carsten  Rose's avatar
Carsten Rose committed
918
919
920
921

        return $url;
    }

922
    abstract public function buildRowPill(array $formElement, $elementHtml);
923

924
    abstract public function buildRowFieldset(array $formElement, $elementHtml);
925

926
927
    abstract public function buildRowTemplateGroup(array $formElement, $elementHtml);

928
    abstract public function buildRowSubrecord(array $formElement, $elementHtml);
929

930
931
    /**
     * Builds HTML 'input' element.
932
     * Format: <input name="$htmlFormElementName" <type="email|input|password|url" [autocomplete="autocomplete"] [autofocus="autofocus"]
933
     *           [maxlength="$maxLength"] [placeholder="$placeholder"] [size="$size"] [min="$min"] [max="$max"]
Carsten  Rose's avatar
Carsten Rose committed
934
     *           [pattern="$pattern"] [required="required"] [disabled="disabled"] value="$value">
935
     *
Carsten  Rose's avatar
Carsten Rose committed
936
     * @param array  $formElement
937
938
     * @param string $htmlFormElementName
     * @param string $value
Carsten  Rose's avatar
Carsten Rose committed
939
     * @param array  $json Return updates in this array - will be later converted to JSON.
940
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
941
     * @return string complete rendered HTML input element.
942
     * @throws \qfq\UserFormException
943
     */
944
    public function buildInput(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
945
        $textarea = '';
946
        $attribute = '';
947
        $class = 'form-control';
948
        $elementCharacterCount = '';
949

950
951
952
953
954
955
956
        $typeAheadUrlParam = $this->typeAheadBuildParam($formElement);
        if ($typeAheadUrlParam != '') {
            $class .= ' ' . CLASS_TYPEAHEAD;
            $dataSip = $this->sip->queryStringToSip($typeAheadUrlParam, RETURN_SIP);
            $attribute .= Support::doAttribute(DATA_TYPEAHEAD_SIP, $dataSip);
            $attribute .= Support::doAttribute(DATA_TYPEAHEAD_LIMIT, $formElement[FE_TYPEAHEAD_LIMIT]);
            $attribute .= Support::doAttribute(DATA_TYPEAHEAD_MINLENGTH, $formElement[FE_TYPEAHEAD_MINLENGTH]);
957
958
959
            if (isset($formElement[FE_TYPEAHEAD_PEDANTIC])) {
                $attribute .= Support::doAttribute(DATA_TYPEAHEAD_PEDANTIC, 'true');
            }
960
        }
961

962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
        if (isset($formElement[FE_CHARACTER_COUNT_WRAP])) {
            $class .= ' ' . CLASS_CHARACTER_COUNT;
            $attribute .= Support::doAttribute(DATA_CHARACTER_COUNT_ID, $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT);
            $attributeCC = Support::doAttribute('id', $formElement[FE_HTML_ID] . HTML_ID_EXTENSION_CHARACTER_COUNT);

            $classCC = ($formElement[FE_CHARACTER_COUNT_WRAP] == '') ? Support::doAttribute('class', 'qfq-cc-style') : '';
            $elementCharacterCount = "<span $attributeCC $classCC></span>";

            if ($formElement[FE_CHARACTER_COUNT_WRAP] != '') {
                $arr = explode('|', $formElement[FE_CHARACTER_COUNT_WRAP], 2);
                $arr[] = '';
                $arr[] = ''; //skip check that at least 2 elements exist
                $elementCharacterCount = $arr[0] . $elementCharacterCount . $arr[1];
            }
        }

978
979
        $attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
        $attribute .= Support::doAttribute('name', $htmlFormElementName);
980
        $attribute .= Support::doAttribute('class', $class);
981