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

Marc Egger's avatar
Marc Egger committed
9
namespace IMATHUZH\Qfq\Core\Store;
Carsten  Rose's avatar
Carsten Rose committed
10

Marc Egger's avatar
Marc Egger committed
11
12
13
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
14
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
Marc Egger's avatar
Marc Egger committed
15
16
17
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
Carsten  Rose's avatar
Carsten Rose committed
18

19
20
21
22
/**
 * Class FillStoreForm
 * @package qfq
 */
Carsten  Rose's avatar
Carsten Rose committed
23
24
25
26
27
28
29
30
class FillStoreForm {

    /**
     * @var Store
     */
    private $store = null;

    /**
Carsten  Rose's avatar
Carsten Rose committed
31
     * @var Database[] - Array of Database instantiated class
Carsten  Rose's avatar
Carsten Rose committed
32
     */
33
34
35
36
    private $dbArray = array();

    private $dbIndexData = false;
    private $dbIndexQfq = false;
Carsten  Rose's avatar
Carsten Rose committed
37
38
39
40
41
42

    /**
     * @var array
     */
    private $feSpecNative = array();

43
44
45
46
47
    /**
     * @var Evaluate
     */
    private $evaluate = null;

Carsten  Rose's avatar
Carsten Rose committed
48
    /**
49
     * FillStoreForm constructor.
Marc Egger's avatar
Marc Egger committed
50
51
52
53
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
54
55
     */
    public function __construct() {
56

Carsten  Rose's avatar
Carsten Rose committed
57
        $this->store = Store::getInstance();
58

59
60
61
62
63
64
65
66
67
68
69
        $this->dbIndexData = $this->store->getVar(PARAM_DB_INDEX_DATA, STORE_SIP);
        if ($this->dbIndexData === false) {
            $this->dbIndexData = DB_INDEX_DEFAULT; // Fallback for FORMs which are not called via SIP;
        }
        $this->dbArray[$this->dbIndexData] = new Database($this->dbIndexData);

        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);
        if ($this->dbIndexQfq != $this->dbIndexData) {
            $this->dbArray[$this->dbIndexQfq] = new Database($this->dbIndexQfq);
        }

Carsten  Rose's avatar
Carsten Rose committed
70
        $this->feSpecNative = $this->loadFormElementsBasedOnSIP();
71

72
        $form = $this->store->getVar(SIP_FORM, STORE_SIP, SANITIZE_ALLOW_ALNUMX);
Carsten  Rose's avatar
Carsten Rose committed
73
        if (!empty($form) && !defined('PHPUNIT_QFQ')) {
74
            // To make STORE_RECORD available at a very early stage.
75
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP, SANITIZE_ALLOW_DIGIT);
76
            $tableFromFormSql = "SELECT `tableName`, `primaryKey` FROM `Form` WHERE `name`=?";
77
            $form = $this->dbArray[$this->dbIndexQfq]->sql($tableFromFormSql, ROW_EXPECT_1, [$form]);
78

79
80
81
82
83
            if (empty($form[F_PRIMARY_KEY])) {
                $form[F_PRIMARY_KEY] = F_PRIMARY_KEY_DEFAULT;
            }
            $this->store->fillStoreWithRecord($form[F_TABLE_NAME], $recordId, $this->dbArray[$this->dbIndexData], $form[F_PRIMARY_KEY]);
        }
84

85
        $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
Carsten  Rose's avatar
Carsten Rose committed
86
87
    }

88
89
90
91
    /**
     * Loads a minimal definition of FormElement of the form specified in SIP.
     *
     * @return array
Marc Egger's avatar
Marc Egger committed
92
93
94
95
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
96
97
     */
    private function loadFormElementsBasedOnSIP() {
98

99
100
101
102
103
        $formName = $this->store->getVar(SIP_FORM, STORE_SIP);

        // Preparation for Log, Debug
        $this->store->setVar(SYSTEM_FORM, $formName, STORE_SYSTEM);

104
        $feSpecNative = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER, ROW_REGULAR, [$formName],
105
            'Form or FormElements not found: ' . ERROR_FORM_NOT_FOUND);
106
        HelperFormElement::explodeParameterInArrayElements($feSpecNative, FE_PARAMETER);
107

108
        $feSpecTemplateGroup = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_CONTAINER_TEMPLATE_GROUP, ROW_REGULAR, [$formName]);
109
        HelperFormElement::explodeParameterInArrayElements($feSpecTemplateGroup, FE_PARAMETER);
110
111
112
113
114
115

        $feSpecNative = $this->expandTemplateGroupFormElement($feSpecTemplateGroup, $feSpecNative);

        return $feSpecNative;
    }

Carsten  Rose's avatar
Carsten Rose committed
116
117
118
119
120
    /**
     * Checks if there are templateGroups defined. If yes, expand them. Return expanded feSpecNative array.
     *
     * @param array $feSpecTemplateGroup
     * @param array $feSpecNative
Carsten  Rose's avatar
Carsten Rose committed
121
     *
Carsten  Rose's avatar
Carsten Rose committed
122
123
124
125
126
     * @return array
     */
    private function expandTemplateGroupFormElement(array $feSpecTemplateGroup, array $feSpecNative) {
        $expanded = array();

127
        if (count($feSpecTemplateGroup) == 0) {
Carsten  Rose's avatar
Carsten Rose committed
128
129
130
131
            return $feSpecNative; // No templateGroups >> nothing to do >> just return
        }

        // Iterate over all 'FormElements': part of a templateGroup?
132
133
        foreach ($feSpecNative as $fe) {
            $flagCopied = false;
Carsten  Rose's avatar
Carsten Rose committed
134

135
            if ($fe[FE_ID_CONTAINER] > 0) {
Carsten  Rose's avatar
Carsten Rose committed
136
137
                // Search for a corresponding template group.
                foreach ($feSpecTemplateGroup as $templateGroup) {
138
                    if ($fe[FE_ID_CONTAINER] == $templateGroup[FE_ID]) {
Carsten  Rose's avatar
Carsten Rose committed
139

140
                        $flagCopied = true;
Carsten  Rose's avatar
Carsten Rose committed
141
142

                        // Get max copies per template group
143
                        $maxCopies = HelperFormElement::tgGetMaxLength($templateGroup[FE_MAX_LENGTH]);
Carsten  Rose's avatar
Carsten Rose committed
144
145
146
147

                        // Copy each native FormElement
                        $template = $fe[FE_NAME];
                        for ($ii = 1; $ii <= $maxCopies; $ii++) {
148
                            $fe[FE_NAME] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $template);
Carsten  Rose's avatar
Carsten Rose committed
149
150
151
152
153
154
                            $expanded[] = $fe;
                        }
                    }
                }
            }

155
            if (!$flagCopied) {
Carsten  Rose's avatar
Carsten Rose committed
156
157
158
159
160
161
162
                $expanded[] = $fe;
            }
        }

        return $expanded;
    }

Carsten  Rose's avatar
Carsten Rose committed
163
164
165
166
    /**
     * Copies all current form parameter from STORE_CLIENT to STORE_FORM. Checks the values against FormElement
     * definition and throws an exception if check fails. FormElements.type=hidden will be taken from STORE_SIP.
     *
167
168
     * @param string $formMode
     *
Marc Egger's avatar
Marc Egger committed
169
170
171
172
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
173
     */
174
175
    public function process($formMode = FORM_SAVE) {

176
        // The following will never be used during load (fe.type='upload').
177
178
        $skip = [FE_SLAVE_ID, FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER,
            FE_VALUE, FE_FILL_STORE_VAR, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE, FE_TYPEAHEAD_TAG_INSERT];
179

Carsten  Rose's avatar
Carsten Rose committed
180
181
182
183
        $html = '';
        $newValues = array();

        $clientValues = $this->store->getStore(STORE_CLIENT);
184
        $formModeGlobal = Support::getFormModeGlobal($this->formSpec[F_MODE_GLOBAL] ?? '');
Carsten  Rose's avatar
Carsten Rose committed
185

186
        if ($formMode == FORM_UPDATE && $formModeGlobal == '') {
Carsten  Rose's avatar
Carsten Rose committed
187
            # During 'update': fake all elements to be not 'required'.
188
            $formModeGlobal = F_MODE_REQUIRED_OFF;
189
190
        }

191
        // If called through 'api/...': get STORE_TYPO3 via SIP parameter.
Carsten  Rose's avatar
Carsten Rose committed
192
        if (isset($clientValues[CLIENT_TYPO3VARS]) && $formMode != FORM_REST) {
193
194
195
            $this->store->fillTypo3StoreFromSip($clientValues[CLIENT_TYPO3VARS]);
        }

Carsten  Rose's avatar
Carsten Rose committed
196
197
198
        // Retrieve SIP vars, e.g. for HIDDEN elements.
        $sipValues = $this->store->getStore(STORE_SIP);

199
        // Copy SIP Values; not necessarily defined as a FormElement.
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
        foreach ($sipValues as $key => $value) {
            switch ($key) {
                case SIP_SIP:
                case SIP_RECORD_ID:
                case SIP_FORM:
                case SIP_TABLE:
                case SIP_URLPARAM:
                case 'id':
                    break;
                default:
                    $newValues[$key] = $value;
                    break;
            }
        }

Carsten  Rose's avatar
Carsten Rose committed
215
216
217
218
219
220
        if ($formMode != FORM_REST) {
            // Check if there is a 'new record already saved' situation:
            // yes: the names of the input fields are submitted with '<fieldname>:0' instead of '<fieldname>:<id>'
            // no: regular situation, take real 'recordid'
            $fakeRecordId = isset($sipValues[SIP_MAKE_URLPARAM_UNIQ]) ? 0 : $sipValues[SIP_RECORD_ID];
        }
Carsten  Rose's avatar
#2067    
Carsten Rose committed
221

Carsten  Rose's avatar
Carsten Rose committed
222
        // Iterate over all FormElements. Sanitize values. Built an assoc array $newValues.
Carsten  Rose's avatar
Carsten Rose committed
223
224
225
        foreach ($this->feSpecNative AS $formElement) {

            // Never get a predefined 'id'
226
            if ($formElement[FE_NAME] === COLUMN_ID) {
Carsten  Rose's avatar
Carsten Rose committed
227
                continue;
228
            }
Carsten  Rose's avatar
Carsten Rose committed
229

230
231
232
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);

233
            // Evaluate current FormElement: e.g. FE_MODE_SQL
234
            $formElement = $this->evaluate->parseArray($formElement, $skip, $debugStack);
235

236
            // Get related formElement. Construct the field name used in the form.
Carsten  Rose's avatar
Carsten Rose committed
237
            $clientFieldName = ($formMode == FORM_REST) ? $formElement[FE_NAME] : HelperFormElement::buildFormElementName($formElement, $fakeRecordId);
Carsten  Rose's avatar
Carsten Rose committed
238

239
            // Some Defaults
240
            $formElement = Support::setFeDefaults($formElement, [FE_MODE => $formModeGlobal]);
Carsten  Rose's avatar
Carsten Rose committed
241

242
243
            if ($formElement[FE_TYPE] === FE_TYPE_EXTRA) {
                // Extra elements will be transferred by SIP
244
                if (!isset($sipValues[$formElement[FE_NAME]])) {
245
246
                    # Check for reserved names.
                    if ($formElement[FE_NAME] == CLIENT_PAGE_ID || $formElement[FE_NAME] == CLIENT_PAGE_TYPE || $formElement[FE_NAME] == CLIENT_PAGE_LANGUAGE) {
Marc Egger's avatar
Marc Egger committed
247
                        throw new \UserFormException(
248
249
                            json_encode(
                                [ERROR_MESSAGE_TO_USER => 'Reserved name "' . $formElement[FE_NAME] . '" in FormElement.',
Marc Egger's avatar
Marc Egger committed
250
                                    ERROR_MESSAGE_TO_DEVELOPER => 'FE_TYPE="extra" should not use ' . CLIENT_PAGE_ID . ',' . CLIENT_PAGE_TYPE . ',' . CLIENT_PAGE_LANGUAGE]), ERROR_FORM_RESERVED_NAME);
251
                    }
Marc Egger's avatar
Marc Egger committed
252
                    throw new \CodeException("Missing the " . FE_TYPE_EXTRA . " field '" . $formElement[FE_NAME] . "' in SIP.", ERROR_MISSING_HIDDEN_FIELD_IN_SIP);
Carsten  Rose's avatar
Carsten Rose committed
253
254
                }

255
                $newValues[$formElement[FE_NAME]] = $sipValues[$formElement[FE_NAME]] ?? '';
Carsten  Rose's avatar
Carsten Rose committed
256
257
258
                continue;
            }

259
260
261
            switch ($formElement[FE_TYPE]) {
                case FE_TYPE_CHECKBOX:
                    // Checkbox Multi: collect values
262
                    $val = $this->collectCheckBoxValues($clientFieldName, $clientValues, $formElement[FE_CHECKBOX_CHECKED] ?? '');
263
264
265
266
267
268
269
270
271
                    if ($val !== false) {
                        $clientValues[$clientFieldName] = $val;
                    }
                    break;
                case FE_TYPE_ANNOTATE:
                    $formElement[FE_ENCODE] = FE_ENCODE_NONE;
                    break;
                default:
                    break;
272
273
            }

274
275
276
            // Bug #5077 / 'Required' FormElement with Dynamic Update - required FE will be checked later - at this point there is no F, R store.
//            if ($formElement[FE_MODE] === FE_MODE_REQUIRED) {
//                if (!isset($clientValues[$clientFieldName]) || ($clientValues[$clientFieldName] === '')) {
Marc Egger's avatar
Marc Egger committed
277
//                    throw new \UserFormException("Missing required value.", ERROR_REQUIRED_VALUE_EMPTY);
278
279
//                }
//            }
280

281
282
283

            // FORM_REST: typically form elements are filled and created on form load. This does not exist for REST Forms.
            // If a FE.value is defined, this has precedence over client supplied content.
284
            if ($formMode == FORM_REST && $formElement[FE_VALUE] != '') {
285
286
287
                $clientValues[$clientFieldName] = $this->evaluate->parse($formElement[FE_VALUE]);
            }

288
289
            // copy value to $newValues
            if (isset($clientValues[$clientFieldName])) {
290
291
292

                if ($formElement[FE_DYNAMIC_UPDATE] === 'yes' ||
                    $formElement[FE_MODE] === FE_MODE_REQUIRED ||
293
                    $formElement[FE_MODE] === FE_MODE_SHOW_REQUIRED ||
294
295
296
                    $formElement[FE_MODE] === FE_MODE_SHOW ||
                    (isset($formElement[FE_PROCESS_READ_ONLY]) && $formElement[FE_PROCESS_READ_ONLY] != '0')) {

297
298
299
300
301
                    if (HelperFormElement::booleParameter($formElement[FE_TYPEAHEAD_TAG] ?? '-')) {
                        // TYPEAHEAD_TAG will be delivered as JSON. Check and sanitize every key/value pair.
                        $arr = json_decode($clientValues[$clientFieldName], true);
                        $arrTmp = array();
                        foreach ($arr as $row) {
302

303
304
305
306
307
                            $arrKey = $this->doValue($formElement, $formMode, $row['key']);
                            $arrValue = $this->doValue($formElement, $formMode, $row['value']);
                            $arrTmp[$arrKey] = $arrValue;
                        }
                        $val = KeyValueStringParser::unparse($arrTmp);
308

309
310
311
                    } else {
                        // Single Value
                        $val = $this->doValue($formElement, $formMode, $clientValues[$clientFieldName]);
312
                    }
313
314

                    $newValues[$formElement[FE_NAME]] = $val;
315
                }
Carsten  Rose's avatar
Carsten Rose committed
316
317
318
            }
        }

319
        $this->store->setStore($newValues, STORE_FORM, true);
320

Carsten  Rose's avatar
Carsten Rose committed
321
322
    }

323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
    /**
     * @param $formElement
     * @param $value
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function doValue($formElement, $formMode, $value) {
        // Trim input
        if (empty($formElement[FE_TRIM])) {
            $value = trim($value);
        } elseif ($formElement[FE_TRIM] !== FE_TRIM_NONE) {
            $value = trim($value, $formElement[FE_TRIM]);
        }

        switch ($formElement[FE_TYPE]) {
            case FE_TYPE_DATE:
            case FE_TYPE_DATETIME:
            case FE_TYPE_TIME:
                if ($value !== '') { // do not check empty values
                    $value = $this->doDateTime($formElement, $value);
                }
                break;

            default:
                if ($formElement[FE_TYPE] == FE_TYPE_EDITOR) {
                    // Tiny MCE always wrap a '<p>' around the content. Remove it before saving.
                    $value = Support::unWrapTag('<p>', $value);
                }

                // Check only if there is something.
                if ($value !== '' && $formMode != FORM_UPDATE && $formElement[FE_MODE] != FE_MODE_HIDDEN) {
                    $value = Sanitize::sanitize($value, $formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN],
                        $formElement[FE_DECIMAL_FORMAT], SANITIZE_EXCEPTION, $formElement[F_FE_DATA_PATTERN_ERROR] ?? '');

                    if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
//                                    $value = htmlspecialchars($value, ENT_QUOTES);
                        $value = Support::htmlEntityEncodeDecode(MODE_ENCODE, $value);
                    }
                }
                break;
        }

        if ($value !== '') {
            $value = Sanitize::checkMinMax($value, $formElement[FE_MIN], $formElement[FE_MAX], SANITIZE_EXCEPTION);
        }

        return $value;
    }

373
    /**
Carsten  Rose's avatar
Carsten Rose committed
374
     * Steps through all $clientValues (POST vars) and collect all with the name _?_${clientFieldName} in a comma
375
     * separated string (MYSQL ENUM type). If there is no element '_h_${clientFieldName}', than there are no multi
Carsten  Rose's avatar
Carsten Rose committed
376
     * values - return the already given `$clientValues[$clientFieldName]`.
377
     *
Carsten  Rose's avatar
Carsten Rose committed
378
     * @param       $clientFieldName
379
     * @param array $clientValues
Carsten  Rose's avatar
Carsten Rose committed
380
     *
381
     * @return string
382
     */
383
    private function collectCheckBoxValues($clientFieldName, array $clientValues, $unchecked) {
384

385
386
        // Check for Single
//        $checkboxKey = HelperFormElement::prependFormElementNameCheckBoxMulti($clientFieldName, '', false);
387

388
        if (isset($clientValues[$clientFieldName])) {
389

390
391
392
393
394
            if (is_array($clientValues[$clientFieldName])) {
                return implode(',', $clientValues[$clientFieldName]);
            }

            return $clientValues[$clientFieldName];
395
396
        }

397
        return $unchecked;
398

399

400
401
        // For templateGroups: all expanded FormElements will be tried to collect - this fails for not submitted fields.
        // Therefore skip not existing clientvalues.
Carsten  Rose's avatar
Carsten Rose committed
402
        if (!isset($clientValues[$checkboxKey])) {
403
404
405
406
            return false;
        }

        // Check if there is a hidden value with naming in checkbox multi syntax
407
        if (isset($clientValues[$checkboxKey])) {
408
            $checkboxValue = $clientValues[$checkboxKey];
409

410
            $pattern = '/' . HelperFormElement::prependFormElementNameCheckBoxMulti($clientFieldName, '\d+') . '/';
411
412
            foreach ($clientValues as $key => $value) {
                if (1 === preg_match($pattern, $key)) {
413
                    $checkboxValue .= ',' . $value;
414
415
416
                }
            }

417
418
419
420
            if (isset($checkboxValue[0]) && $checkboxValue[0] === ',') {
                $checkboxValue = substr($checkboxValue, 1);
            }

421
422
423
424
425
426
            $clientValues[$clientFieldName] = $checkboxValue;
        }

        return $clientValues[$clientFieldName];
    }

427
    /**
428
     * Check  $value as date/datetime/time value and convert it to FORMAT_DATE_INTERNATIONAL.
429
     *
Carsten  Rose's avatar
Carsten Rose committed
430
431
     * @param array $formElement - if not set, set $formElement[FE_DATE_FORMAT]
     * @param string $value - date/datetime/time value in format FORMAT_DATE_INTERNATIONAL or FORMAT_DATE_GERMAN
Carsten  Rose's avatar
Carsten Rose committed
432
     *
433
     * @return string - checked datetime string
Marc Egger's avatar
Marc Egger committed
434
     * @throws \UserFormException
435
     */
436
    public function doDateTime(array &$formElement, $value) {
437

438
        $regexp = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement[FE_DATE_FORMAT], $formElement[FE_TIME_IS_OPTIONAL] ?? "");
439
440
441

        if (1 !== preg_match('/' . $regexp . '/', $value, $matches)) {
            $placeholder = Support::getDateTimePlaceholder($formElement);
Marc Egger's avatar
Marc Egger committed
442
            throw new \UserFormException("DateTime format not recognized: $placeholder / $value ", ERROR_DATE_TIME_FORMAT_NOT_RECOGNISED);
443
444
        }

445
        $showTime = $formElement[FE_TYPE] == FE_TYPE_DATE ? '0' : '1';
446
447
        $value = Support::convertDateTime($value, FORMAT_DATE_INTERNATIONAL, '1', $showTime, $formElement[FE_SHOW_SECONDS]);

448
449
450
451
452
        if ($formElement[FE_TYPE] !== FE_TYPE_TIME) {
            // Validate date (e.g. 2010-02-31)
            $dateValue = explode(' ', $value)[0];
            $dateParts = explode('-', $dateValue);
            if (!checkdate($dateParts[1], $dateParts[2], $dateParts[0]))
Marc Egger's avatar
Marc Egger committed
453
                throw new \UserFormException("$dateValue is not a valid date.", ERROR_INVALID_DATE);
454
455
        }

456
457
        return $value;
    }
Carsten  Rose's avatar
Carsten Rose committed
458
}