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

namespace qfq;

require_once(__DIR__ . '/Store.php');
12
require_once(__DIR__ . '/../database/Database.php');
Carsten  Rose's avatar
Carsten Rose committed
13
14
require_once(__DIR__ . '/../Constants.php');
require_once(__DIR__ . '/../helper/HelperFormElement.php');
15
require_once(__DIR__ . '/../helper/Logger.php');
16
require_once(__DIR__ . '/../exceptions/UserFormException.php');
Carsten  Rose's avatar
Carsten Rose committed
17

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

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

    /**
30
     * @var Database[]
Carsten  Rose's avatar
Carsten Rose committed
31
     */
32
33
34
35
    private $dbArray = array();

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

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

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

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

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

58
59
60
61
62
63
64
65
66
67
68
        $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
69
        $this->feSpecNative = $this->loadFormElementsBasedOnSIP();
70

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

78
79
80
81
82
            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]);
        }
83

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

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

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

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

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

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

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

        return $feSpecNative;
    }

Carsten  Rose's avatar
Carsten Rose committed
115
116
117
118
119
    /**
     * 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
120
     *
Carsten  Rose's avatar
Carsten Rose committed
121
122
123
124
125
     * @return array
     */
    private function expandTemplateGroupFormElement(array $feSpecTemplateGroup, array $feSpecNative) {
        $expanded = array();

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

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

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

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

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

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

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

        return $expanded;
    }

Carsten  Rose's avatar
Carsten Rose committed
162
163
164
165
    /**
     * 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.
     *
166
167
     * @param string $formMode
     *
Carsten  Rose's avatar
Carsten Rose committed
168
     * @throws CodeException
169
     * @throws DbException
170
     * @throws UserFormException
171
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
172
     */
173
174
    public function process($formMode = FORM_SAVE) {

175
        // The following will never be used during load (fe.type='upload').
176
        $skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER];
177

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

        $clientValues = $this->store->getStore(STORE_CLIENT);
182
        $formModeGlobal = $this->store->getVar(F_MODE_GLOBAL, STORE_SIP . STORE_EMPTY);
Carsten  Rose's avatar
Carsten Rose committed
183

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

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

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

197
        // Copy SIP Values; not necessarily defined as a FormElement.
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
        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
213
214
215
216
217
218
        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
219

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

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

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

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

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

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

240
241
            if ($formElement[FE_TYPE] === FE_TYPE_EXTRA) {
                // Extra elements will be transferred by SIP
242
                if (!isset($sipValues[$formElement[FE_NAME]])) {
243
244
245
246
247
248
249
                    # Check for reserved names.
                    if ($formElement[FE_NAME] == CLIENT_PAGE_ID || $formElement[FE_NAME] == CLIENT_PAGE_TYPE || $formElement[FE_NAME] == CLIENT_PAGE_LANGUAGE) {
                        throw new UserFormException(
                            json_encode(
                                [ERROR_MESSAGE_TO_USER => 'Reserved name "' . $formElement[FE_NAME] . '" in FormElement.',
                                    ERROR_MESSAGE_SUPPORT => 'FE_TYPE="extra" should not use ' . CLIENT_PAGE_ID . ',' . CLIENT_PAGE_TYPE . ',' . CLIENT_PAGE_LANGUAGE]), ERROR_FORM_RESERVED_NAME);
                    }
250
                    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
251
252
                }

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

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

272
273
274
275
276
277
            // 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] === '')) {
//                    throw new UserFormException("Missing required value.", ERROR_REQUIRED_VALUE_EMPTY);
//                }
//            }
278

279
280
281
282
283
284
285

            // 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.
            if ($formMode == FORM_REST && $formElement[FE_VALUE]!='') {
                $clientValues[$clientFieldName] = $this->evaluate->parse($formElement[FE_VALUE]);
            }

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

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

295
296
                    $val = $clientValues[$clientFieldName];

297
298
299
300
301
302
303
                    // Trim input
                    if (empty($formElement[FE_TRIM])) {
                        $val = trim($val);
                    } elseif ($formElement[FE_TRIM] !== FE_TRIM_NONE) {
                        $val = trim($val, $formElement[FE_TRIM]);
                    }

304
                    switch ($formElement[FE_TYPE]) {
305
306
307
                        case FE_TYPE_DATE:
                        case FE_TYPE_DATETIME:
                        case FE_TYPE_TIME:
308
                            if ($clientValues[$clientFieldName] !== '') { // do not check empty values
309
                                $val = $this->doDateTime($formElement, $val);
310
                            }
311
                            break;
312

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

319
                            // Check only if there is something.
320
                            if ($val !== '' && $formMode != FORM_UPDATE && $formElement[FE_MODE] != FE_MODE_HIDDEN) {
321
                                $val = Sanitize::sanitize($val, $formElement[FE_CHECK_TYPE], $formElement[FE_CHECK_PATTERN],
Carsten  Rose's avatar
Carsten Rose committed
322
                                    $formElement[FE_DECIMAL_FORMAT], SANITIZE_EXCEPTION, $formElement[F_FE_DATA_PATTERN_ERROR] ?? '');
323

324
                                if ($formElement[FE_ENCODE] === FE_ENCODE_SPECIALCHAR) {
325
326
//                                    $val = htmlspecialchars($val, ENT_QUOTES);
                                    $val = Support::htmlEntityEncodeDecode(MODE_ENCODE, $val);
327
                                }
328
                            }
329
                            break;
330
                    }
331

332
                    if ($val !== '') {
333
                        $val = Sanitize::checkMinMax($val, $formElement[FE_MIN], $formElement[FE_MAX], SANITIZE_EXCEPTION);
334
                    }
335
336

                    $newValues[$formElement[FE_NAME]] = $val;
337
                }
Carsten  Rose's avatar
Carsten Rose committed
338
339
340
            }
        }

341
        $this->store->setStore($newValues, STORE_FORM, true);
342

Carsten  Rose's avatar
Carsten Rose committed
343
344
    }

345
    /**
Carsten  Rose's avatar
Carsten Rose committed
346
347
348
     * Steps through all $clientValues (POST vars) and collect all with the name _?_${clientFieldName} in a comma
     * seperated string (MYSQL ENUM type). If there is no element '_h_${clientFieldName}', than there are no multi
     * values - return the already given `$clientValues[$clientFieldName]`.
349
     *
Carsten  Rose's avatar
Carsten Rose committed
350
     * @param       $clientFieldName
351
     * @param array $clientValues
Carsten  Rose's avatar
Carsten Rose committed
352
     *
353
     * @return string
354
     */
355
    private function collectMultiValues($clientFieldName, array $clientValues) {
356

357
        $checkboxKey = HelperFormElement::prependFormElementNameCheckBoxMulti($clientFieldName, 'h');
358

359
360
        // 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
361
        if (!isset($clientValues[$checkboxKey])) {
362
363
364
365
            return false;
        }

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

369
            $pattern = '/' . HelperFormElement::prependFormElementNameCheckBoxMulti($clientFieldName, '\d+') . '/';
370
371
            foreach ($clientValues as $key => $value) {
                if (1 === preg_match($pattern, $key)) {
372
                    $checkboxValue .= ',' . $value;
373
374
375
                }
            }

376
377
378
379
            if (isset($checkboxValue[0]) && $checkboxValue[0] === ',') {
                $checkboxValue = substr($checkboxValue, 1);
            }

380
381
382
383
384
385
            $clientValues[$clientFieldName] = $checkboxValue;
        }

        return $clientValues[$clientFieldName];
    }

386
    /**
387
     * Check  $value as date/datetime/time value and convert it to FORMAT_DATE_INTERNATIONAL.
388
     *
Carsten  Rose's avatar
Carsten Rose committed
389
390
     * @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
391
     *
392
393
394
     * @return string - checked datetime string
     * @throws UserFormException
     */
395
    public function doDateTime(array &$formElement, $value) {
396

397
        $regexp = Support::dateTimeRegexp($formElement[FE_TYPE], $formElement[FE_DATE_FORMAT], $formElement[FE_TIME_IS_OPTIONAL] ?? "");
398
399
400
401
402
403

        if (1 !== preg_match('/' . $regexp . '/', $value, $matches)) {
            $placeholder = Support::getDateTimePlaceholder($formElement);
            throw new UserFormException("DateTime format not recognized: $placeholder / $value ", ERROR_DATE_TIME_FORMAT_NOT_RECOGNISED);
        }

404
        $showTime = $formElement[FE_TYPE] == FE_TYPE_DATE ? '0' : '1';
405
406
        $value = Support::convertDateTime($value, FORMAT_DATE_INTERNATIONAL, '1', $showTime, $formElement[FE_SHOW_SECONDS]);

407
408
409
410
411
412
413
414
        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]))
                throw new UserFormException("$dateValue is not a valid date.", ERROR_INVALID_DATE);
        }

415
416
        return $value;
    }
Carsten  Rose's avatar
Carsten Rose committed
417
}