QuickFormQuery.php 74.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
<?php
/**
 * Created by PhpStorm.
 * User: ep
 * Date: 12/23/15
 * Time: 6:33 PM
 */

namespace qfq;

11
12
13
14
15
16
17
18
//use qfq\Report;

//use qfq\BuildFormPlain;
//use qfq\BuildFormTable;
//use qfq\BuildFormBootstrap;
//use qfq\UserException;
//use qfq\CodeException;
//use qfq\DbException;
19
//use qfq\helper;
20
//use qfq\Store;
Carsten  Rose's avatar
Carsten Rose committed
21

Carsten  Rose's avatar
Carsten Rose committed
22

23
require_once(__DIR__ . '/store/Store.php');
24
require_once(__DIR__ . '/store/Sip.php');
25
26
27
28
29
30
31
32
33
34
require_once(__DIR__ . '/store/FillStoreForm.php');
require_once(__DIR__ . '/store/Session.php');
require_once(__DIR__ . '/Constants.php');
require_once(__DIR__ . '/Save.php');
require_once(__DIR__ . '/helper/KeyValueStringParser.php');
require_once(__DIR__ . '/helper/HelperFormElement.php');
require_once(__DIR__ . '/exceptions/UserFormException.php');
require_once(__DIR__ . '/exceptions/CodeException.php');
require_once(__DIR__ . '/exceptions/DbException.php');
require_once(__DIR__ . '/exceptions/ErrorHandler.php');
35
36
require_once(__DIR__ . '/database/Database.php');
require_once(__DIR__ . '/database/DatabaseUpdate.php');
37
38
39
40
41
require_once(__DIR__ . '/Evaluate.php');
require_once(__DIR__ . '/BuildFormPlain.php');
require_once(__DIR__ . '/BuildFormTable.php');
require_once(__DIR__ . '/BuildFormBootstrap.php');
require_once(__DIR__ . '/report/Report.php');
42
require_once(__DIR__ . '/report/Monitor.php');
43
44
require_once(__DIR__ . '/BodytextParser.php');
require_once(__DIR__ . '/Delete.php');
45
require_once(__DIR__ . '/form/FormAction.php');
Carsten  Rose's avatar
Carsten Rose committed
46
require_once(__DIR__ . '/form/Dirty.php');
47
require_once(__DIR__ . '/form/DragAndDrop.php');
48
49
50
51
52
53
54
55
56
57
58
59
/*
 * Form will be called
 * a) with a SIP identifier, or
 * b) without a SIP identifier (form setting has to allow this) and will create on the fly a new SIP.
 *
 * The SIP-Store stores:
 *  form=<formname>
 *  r=<record id>  (table.id for a single record form)
 *  keySemId,keySemIduser
 *  <further individual variables>
 */

Carsten  Rose's avatar
Carsten Rose committed
60
/**
61
 * Class Qfq
Carsten  Rose's avatar
Carsten Rose committed
62
63
 * @package qfq
 */
64
class QuickFormQuery {
65

66
    /**
67
     * @var Store instantiated class
68
     */
Carsten  Rose's avatar
Carsten Rose committed
69
    protected $store = null;
70

71
    /**
72
     * @var \qfq\Database[] - Array of Database instantiated class
73
     */
74
    protected $dbArray = array();
75

76
77
78
    /**
     * @var Evaluate instantiated class
     */
79
80
    protected $evaluate = null;

81
82
83
    protected $formSpec = array();
    protected $feSpecAction = array();  // Form Definition: copy of the loaded form
    protected $feSpecNative = array(); // FormEelement Definition: all formElement.class='action' of the loaded form
84
    protected $feSpecNativeRaw = array(); // FormEelement Definition: all formElement.class='action' of the loaded form
85

86
87
88
    /**
     * @var array
     */
89
    private $t3data = array(); // FormElement Definition: all formElement.class='native' of the loaded form
90

91
92
93
    /**
     * @var bool
     */
94
95
    private $phpUnit = false;

96
97
98
99
100
    /**
     * @var bool
     */
    private $inlineReport = false;

101
102
103
104
105
    /**
     * @var Session
     */
    private $session = null;

106
107
108
    private $dbIndexData = false;
    private $dbIndexQfq = false;

109
110
111
112
113
114
115
116
117
118
119
    /*
     * TODO:
     *  Preparation: setup logging, database access, record locking
     *  fill stores
     *  Check permission_create / permission_update
     *  Multi: iterate over all records, Single: activate record
     *      Check mode: Load | Save
     *      doActions 'Before'
     *      Do all FormElements
     *      doActions 'After'
     */
120

121
122
123
    /**
     * Construct the Form Class and Store too. This is the base initialization moment.
     *
124
125
     * As a result of instantiating of Form, the class Store will initially called the first time and therefore
     * instantiated automatically. Store might throw an exception, in case the URL-passed SIP is invalid.
126
     *
127
     * @param array $t3data
Carsten  Rose's avatar
Carsten Rose committed
128
     * @param bool $phpUnit
129
     * @param bool $inlineReport
130
     *
131
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
132
     * @throws DbException
133
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
134
     * @throws UserReportException
135
     */
136
    public function __construct(array $t3data = array(), $phpUnit = false, $inlineReport = true) {
137
        $this->phpUnit = $phpUnit;
138
        $this->inlineReport = $inlineReport;
139

140
        mb_internal_encoding("UTF-8");
141

142
143
        $this->session = Session::getInstance($phpUnit);

144
        // Refresh the session even if no new data saved.
145
        Session::set(SESSION_LAST_ACTIVITY, time());
146

147
        set_error_handler("\\qfq\\ErrorHandler::exception_error_handler");
Carsten  Rose's avatar
Carsten Rose committed
148
149
        // PHPExcel
        set_include_path(get_include_path() . PATH_SEPARATOR . '../../Resources/Private/Classes/');
150

151
152
153
154
155
156
157
        if (!isset($t3data[T3DATA_BODYTEXT])) {
            $t3data[T3DATA_BODYTEXT] = '';
        }

        if (!isset($t3data[T3DATA_UID])) {
            $t3data[T3DATA_UID] = 0;
        }
158

159
        $btp = new BodytextParser();
160
        $t3data[T3DATA_BODYTEXT_RAW] = $t3data[T3DATA_BODYTEXT];
161
        $t3data[T3DATA_BODYTEXT] = $btp->process($t3data[T3DATA_BODYTEXT]);
162

163
164
        $this->t3data = $t3data;

165
        $bodytext = $this->t3data[T3DATA_BODYTEXT];
166
167

        $this->store = Store::getInstance($bodytext, $phpUnit);
168

169
170
171
        $timeout = $this->store::getVar(SYSTEM_SESSION_TIMEOUT_SECONDS, STORE_SYSTEM);
        Session::checkSessionExpired($timeout);

172
        // If an FE user logs out and a different user logs in (same browser session) - the old values has to be destroyed!
173
        if (Session::getAndDestroyFlagFeUserHasChanged()) {
174
175
176
            $this->store->unsetStore(STORE_USER);
        }

177
        $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data[T3DATA_UID], STORE_TYPO3);
178

179
180
        $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM);
        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);
181

182
        $this->dbArray[$this->dbIndexData] = new Database($this->dbIndexData);
183

184
185
186
        if ($this->dbIndexData != $this->dbIndexQfq) {
            $this->dbArray[$this->dbIndexQfq] = new Database($this->dbIndexQfq);
        }
187

188
        $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
189

190
        $dbUpdate = $this->store->getVar(SYSTEM_DB_UPDATE, STORE_SYSTEM);
191
        $updateDb = new DatabaseUpdate($this->dbArray[$this->dbIndexQfq]);
192
        $updateDb->checkNupdate($dbUpdate);
193

194
        $this->store->FillStoreSystemBySql(); // Do this after the DB-update
195
196
197

        // Set dbIndex, evaluate any
        $dbIndex = $this->store->getVar(TOKEN_DB_INDEX, STORE_TYPO3 . STORE_EMPTY);
198
        $dbIndex = $this->evaluate->parse($dbIndex);
199
200
        $dbIndex = ($dbIndex == '') ? DB_INDEX_DEFAULT : $dbIndex;
        $this->store->setVar(TOKEN_DB_INDEX, $dbIndex, STORE_TYPO3);
Carsten  Rose's avatar
Carsten Rose committed
201
202
    }

203
    /**
204
     * Returns the defined forwardMode and set forwardPage
205
     *
206
     * @return array
207
208
     * @throws CodeException
     * @throws UserFormException
209
     */
210
211
    public function getForwardMode() {

212
        if (!isset($this->formSpec[F_FORWARD_PAGE])) {
Carsten  Rose's avatar
Carsten Rose committed
213
            // For QFQ inline editing: no redirect and no further processing.
214
215
216
            return [API_REDIRECT => API_ANSWER_REDIRECT_NO, API_REDIRECT_URL => ''];
        }

217
        $forwardPage = $this->formSpec[F_FORWARD_PAGE];
218

219
220
221
222
223
224
225
226
227
228
229
230
231
232
        switch ($this->formSpec[F_FORWARD_MODE]) {
            case F_FORWARD_MODE_URL_SIP:
                $forwardPage = store::getSipInstance()->queryStringToSip($forwardPage, RETURN_URL);
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL;
                break;
            case F_FORWARD_MODE_URL_SIP_SKIP_HISTORY:
                // F_FORWARD_MODE_URL_SIP is not defined in API PROTOCOL. At the moment it's only used for 'copyForm'.
                // 'copyForm' behaves better if the page is not in history.
                // An option for better implementing would be to separate SKIP History from ForwardMode. For API, it can be combined again.
                $forwardPage = store::getSipInstance()->queryStringToSip($forwardPage, RETURN_URL);
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL_SKIP_HISTORY;
                break;
            default:
                break;
Carsten  Rose's avatar
Carsten Rose committed
233
234
        }

235
236
        return ([
            API_REDIRECT => $this->formSpec[F_FORWARD_MODE],
Carsten  Rose's avatar
Carsten Rose committed
237
            API_REDIRECT_URL => $forwardPage,
238
        ]);
239
240
    }

241
    /**
Carsten  Rose's avatar
Carsten Rose committed
242
     * Main entrypoint for display content: a) form and/or b) report
243
     *
244
     * @return string
245
246
     * @throws CodeException
     * @throws DbException
247
     * @throws DownloadException
248
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
249
     * @throws UserReportException
250
251
252
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
Carsten  Rose's avatar
Carsten Rose committed
253
     */
254
    public function process() {
255
        $html = '';
256

257
        if ($this->store->getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === 'yes') {
258
259
            $htmlId = HelperFormElement::buildFormElementId($this->formSpec[F_ID], 0, 0, 0);
            $html .= Support::doTooltip($htmlId . HTML_ID_EXTENSION_TOOLTIP, $this->t3data['bodytext']);
260
261
262
        }

        $html .= $this->doForm(FORM_LOAD);
263
        $html .= $this->doReport();
Carsten  Rose's avatar
Carsten Rose committed
264

265
        // Only needed if there are potential 'download'-links, which shall show a popup during processing of the download.
Carsten  Rose's avatar
Carsten Rose committed
266
267
268
269
        if ($this->store->getVar(SYSTEM_DOWNLOAD_POPUP, STORE_SYSTEM) == DOWNLOAD_POPUP_REQUEST) {
            $html .= $this->getModalCode();
        }

270
271
272
273
274
        // Only needed if there are 'drag and drop' elements.
        if ($this->store->getVar(SYSTEM_DRAG_AND_DROP_JS, STORE_SYSTEM) == 'true') {
            $html .= $this->getDragAndDropCode();
        }

275
        $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
276
        if ($class) {
277
            $html = Support::wrapTag("<div class='$class'>", $html);
Carsten  Rose's avatar
Carsten Rose committed
278
279
        }

280
        return $html;
281
282
    }

283
284
    /**
     * Determine the name of the language parameter field, which has to be taken to fill language specific defintions.
285
286
287
     *
     * @throws CodeException
     * @throws UserFormException
288
289
290
291
292
293
294
295
296
     */
    private function setParameterLanguageFieldName() {

        $typo3PageLanguage = $this->store->getVar(TYPO3_PAGE_LANGUAGE, STORE_TYPO3);
        if (empty($typo3PageLanguage)) {
            return;
        }

        foreach (['A', 'B', 'C', 'D'] as $key) {
297
            $languageIdx = SYSTEM_FORM_LANGUAGE . "$key" . "Id";
298
299
300
301
302
303
304
            if ($this->store->getVar($languageIdx, STORE_SYSTEM) == $typo3PageLanguage) {
                $this->store->setVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, 'parameterLanguage' . $key, STORE_SYSTEM);
                break;
            }
        }
    }

305
    /**
306
307
     * Creates an empty file. This indicates that the current form is in debug mode. Returns HTML element which will be
     * replaced by the logfile.
308
309
310
     *
     * @param $formName
     * @param $formLogMode
311
     * @return string
312
313
314
315
316
317
318
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
    private function getFormLog($formName, $formLogMode) {

        $formLogFileName = Support::getFormLogFileName($formName, $formLogMode);
319
        file_put_contents($formLogFileName, '');
320

321
322
        $monitor = new Monitor();

323
324
        return "<pre id='" . FORM_LOG_HTML_ID . "'>Please wait</pre>" .
            $monitor->process([TOKEN_L_FILE => $formLogFileName, TOKEN_L_APPEND => '1', TOKEN_L_HTML_ID => FORM_LOG_HTML_ID]);
325
    }
326

327
    /**
328
     * Process form.
329
330
331
332
333
     * $mode=
     *   FORM_LOAD: The whole form will be rendered as HTML Code, including the values of all form elements
     *   FORM_UPDATE: States and values of all form elements will be returned as JSON.
     *   FORM_SAVE: The submitted form will be saved. Return Failure or Success as JSON.
     *   FORM_DELETE:
334
     *
335
     * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE
336
     *
337
     * @return array|string
338
     * @throws CodeException
339
     * @throws DbException
340
     * @throws DownloadException
341
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
342
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
343
344
345
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
346
     */
347
    private function doForm($formMode) {
Carsten  Rose's avatar
Carsten Rose committed
348
        $data = '';
Carsten  Rose's avatar
Carsten Rose committed
349
        $foundInStore = '';
350
        $flagApiStructureReGroup = true;
351

352
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO);
353
        $this->setParameterLanguageFieldName();
354

355
356
357
        $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore, $formLogMode);
        if ($formName !== false && $formLogMode !== false) {
            return $this->getFormLog($formName, $formLogMode);
358
359
        }

360
        if ($formName === false) {
361
            switch ($formMode) {
362
363
364
365
366
367
368
                case FORM_DELETE:
                    break;
                case FORM_DRAG_AND_DROP:
                    throw new CodeException('Missing form in SIP', ERROR_MISSING_FORM);
                default:
                    return '';// No form found: do nothing
            }
369
        }
370

Carsten  Rose's avatar
Carsten Rose committed
371
        // Check 'session expire' happens quite late, cause it can be configured per form.
372
373
        Session::checkSessionExpired($this->formSpec[F_SESSION_TIMEOUT_SECONDS]);

374
375
376
377
378
379
380
381
382
383
        // Fill STORE_FORM: might need Form.fillStoreVar={{!SELECT ...}}) to provide STORE_VAR - therefore the FORM-definition should already been processed. #8058
        switch ($formMode) {
            case FORM_UPDATE:
            case FORM_SAVE:
            case FORM_REST:
                $fillStoreForm = new FillStoreForm();
                $fillStoreForm->process($formMode);
                break;
        }

384
        if ($formName !== false) {
Carsten  Rose's avatar
Carsten Rose committed
385
            // Validate (only if there is a 'real' form, not a FORM_DELETE with only a table name).
Carsten  Rose's avatar
Carsten Rose committed
386
387
            // Attention: $formModeNew will be set
            $sipFound = $this->validateForm($foundInStore, $formMode, $formModeNew);
388
389

        } else {
390
            // FORM_DELETE without a form definition: Fake the form with only a tableName.
391
392
            $table = $this->store->getVar(SIP_TABLE, STORE_SIP);
            if ($table === false) {
393
                throw new UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE);
394
            }
395

396
397
398
            $sipFound = true;
            $this->formSpec[F_NAME] = '';
            $this->formSpec[F_TABLE_NAME] = $table;
399
400
            $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS] = 1; // just indicate a timeout, the exact timeout is stored in the dirty record.
            $this->formSpec[F_DIRTY_MODE] = DIRTY_MODE_EXCLUSIVE; // just set a mode,, the exact mode is stored in the dirty record.
401
402
403
404
405
406
407
408
409
410

            $tmpDbIndexData = $this->store->getVar(PARAM_DB_INDEX_DATA, STORE_SIP);
            if (!empty($tmpDbIndexData)) {
                $this->formSpec[F_DB_INDEX] = $tmpDbIndexData;
                if ($tmpDbIndexData != $this->dbIndexData) {
                    if (!isset($this->dbArray[$tmpDbIndexData])) {
                        $this->dbArray[$tmpDbIndexData] = new Database($tmpDbIndexData);
                    }
                }
            }
411
412
        }

413
414
        // For 'new' record always create a new Browser TAB-uniq (for this current form, nowhere else used) SIP.
        // With such a Browser TAB-uniq SIP, multiple Browser TABs and following repeated NEWs are easily implemented.
415
416
417
418
        if ($formMode != FORM_REST) {
            if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) {
                $this->store->createSipAfterFormLoad($formName);
            }
419
        }
420

421
        // Fill STORE_BEFORE
422
        if ($this->store->getVar($this->formSpec[F_PRIMARY_KEY], STORE_BEFORE) === false) {
423
            $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId,
424
                $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY], STORE_BEFORE);
425
426
        }

427
        // Check (and release) dirtyRecord.
Carsten  Rose's avatar
Carsten Rose committed
428
        if ($formModeNew === FORM_DELETE || $formModeNew === FORM_SAVE) {
429
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
Carsten  Rose's avatar
Carsten Rose committed
430

Carsten  Rose's avatar
Carsten Rose committed
431
            $answer = $dirty->checkDirtyAndRelease($formModeNew, $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS],
432
                $this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $this->formSpec[F_PRIMARY_KEY], $recordId, true);
433

434
            // In case of a conflict, return immediately
Carsten  Rose's avatar
Carsten Rose committed
435
436
            if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
                $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
437

438
439
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
440
441
        }

442
        // FORM_LOAD: if there is a foreign exclusive record lock - show form in F_MODE_READONLY mode.
Carsten  Rose's avatar
Carsten Rose committed
443
        if ($formModeNew === FORM_LOAD) {
444
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
445
446
447
448
449
450
451
            $recordDirty = array();
            $rcLockFound = $dirty->getCheckDirty($this->formSpec[F_TABLE_NAME], $recordId, $recordDirty, $msg);
            if (($rcLockFound == LOCK_FOUND_CONFLICT || $rcLockFound == LOCK_FOUND_OWNER) && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) {
                $this->formSpec[F_MODE] = F_MODE_READONLY;
            }
        }

Carsten  Rose's avatar
Carsten Rose committed
452
        switch ($formModeNew) {
453
454
455
456
457
458
459
460
461
            case FORM_DELETE:
                $build = new Delete($this->dbIndexData);
                break;
            case FORM_REST:
                break;
            case FORM_LOAD:
            case FORM_SAVE:
            case FORM_UPDATE:
            case FORM_DRAG_AND_DROP:
462

463
464
465
                $tableDefinition = $this->dbArray[$this->dbIndexData]->getTableDefinition($this->formSpec[F_TABLE_NAME]);
                $this->store->fillStoreTableDefaultColumnType($tableDefinition);

466
467
468
469
470
                // Check if the defined column primary key exist.
                if ($this->store::getVar($this->formSpec[F_PRIMARY_KEY], STORE_TABLE_COLUMN_TYPES) === false) {
                    throw new UserFormException("Primary Key '" . $this->formSpec[F_PRIMARY_KEY] . "' not found in table " . $this->formSpec[F_TABLE_NAME], ERROR_INVALID_OR_MISSING_PARAMETER);
                }

471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
                switch ($this->formSpec['render']) {
                    case 'plain':
                        $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
                        break;
                    case 'table':
                        $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
                        break;
                    case 'bootstrap':
                        $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
                        break;
                    default:
                        throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
                }
                break;
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
487
488
        }

489
        $formAction = new FormAction($this->formSpec, $this->dbArray[$this->dbIndexData], $this->phpUnit);
Carsten  Rose's avatar
Carsten Rose committed
490
        switch ($formModeNew) {
491
            case FORM_LOAD:
492
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
Carsten  Rose's avatar
Carsten Rose committed
493
                $data = $build->process($formModeNew);
494
495
496
497
                $tmpClass = is_numeric($this->formSpec[F_BS_COLUMNS]) ? ('col-md-' . $this->formSpec[F_BS_COLUMNS]) : $this->formSpec[F_BS_COLUMNS];
//                $data = Support::wrapTag("<div class='" . 'col-md-' . $this->formSpec[F_BS_COLUMNS] . "'>", $data);
                $data = Support::wrapTag('<div class="' . $tmpClass . '">', $data);
                $data = Support::wrapTag('<div class="row">', $data);
498
499
500
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
                break;

Carsten  Rose's avatar
Carsten Rose committed
501
            case FORM_UPDATE:
502
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
503
                // data['form-update']=....
Carsten  Rose's avatar
Carsten Rose committed
504
                $data = $build->process($formModeNew);
505
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
506
                break;
Carsten  Rose's avatar
Carsten Rose committed
507

508
509
510
            case FORM_DELETE:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_DELETE);

511
                $build->process($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);
512
513
514
515

                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_DELETE);
                break;

516
            case FORM_SAVE:
517
518
                $this->logFormSubmitRequest();

Carsten  Rose's avatar
Carsten Rose committed
519
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3);
520

521
                // Action: Before
522
523
                $feTypeList = FE_TYPE_BEFORE_SAVE . ',' . ($recordId == 0 ? FE_TYPE_BEFORE_INSERT : FE_TYPE_BEFORE_UPDATE);
                $formAction->elements($recordId, $this->feSpecAction, $feTypeList);
524

525
                // If an old record exist: load it. Necessary to delete uploaded files which should be overwritten.
526
                $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId,
527
                    $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]);
528

529
530
                $this->ifPillIsHiddenSetChildFeToHidden();

531
                // SAVE
532
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw);
533
534

                $save->processAllImageCutFE();
535
                $save->checkRequiredHidden();
536

537
538
                $rc = $save->process();

539
                // Reload fresh saved record and fill STORE_RECORD with it.
540
                $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]);
541

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
542
543
                $save->processAllUploads($rc);

544
                // Action: After
545
546
                $feTypeList = FE_TYPE_AFTER_SAVE . ',' . ($recordId == 0 ? FE_TYPE_AFTER_INSERT : FE_TYPE_AFTER_UPDATE);
                $status = $formAction->elements($rc, $this->feSpecAction, $feTypeList);
547
                if ($status != ACTION_ELEMENT_NO_CHANGE) {
548
                    // Reload fresh saved record and fill STORE_RECORD with it.
549
                    $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]);
550
                }
551

Carsten  Rose's avatar
Carsten Rose committed
552
553
554
555
556
557
                // Action: Paste
                $this->pasteClipboard($this->formSpec[F_ID], $formAction);

                // Action: Sendmail
                $formAction->elements($rc, $this->feSpecAction, FE_TYPE_SENDMAIL);

Carsten  Rose's avatar
Carsten Rose committed
558
559
                if ($formMode == FORM_REST) {
                    $data = ['id' => $rc];
560
                    $flagApiStructureReGroup = false;
Carsten  Rose's avatar
Carsten Rose committed
561
562
                    break;
                }
563
564
565
566
567
568
569

                $customForward = $this->setForwardModePage();

                // Logic: If a) r=0 and
                //           b) User presses only 'save' (not save & close) and
                //           c) there is no forwardMode=='url...'
                // then the client should reload the current page with the newly created record. A new SIP is necessary!
570
                $getJson = true;
571
572
573
574
575
                if (0 == $this->store->getVar(SIP_RECORD_ID, STORE_SIP) &&
                    API_SUBMIT_REASON_SAVE == $this->store->getVar(API_SUBMIT_REASON, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX) &&
                    $customForward == false
                ) {
                    $this->formSpec = $this->buildNSetReloadUrl($this->formSpec, $rc);
576
                    $getJson = false;
577
                }
578

579
                if ($getJson) {
580

581
                    // Values of FormElements might be changed during 'afterSave': rebuild the form to load the new values. Especially for non primary template groups.
582
583
584
585
                    $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec[F_ID]], $this->formSpec);
                    $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);
                    $feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName);
                    $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($feSpecNative, $this->formSpec[F_ID], $recordId);
586

Carsten  Rose's avatar
Carsten Rose committed
587
                    $data = $build->process($formModeNew, false, $this->feSpecNative);
588
                }
589
                break;
590

591
592
593
            case FORM_DRAG_AND_DROP:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);

594
                $dragAndDrop = new DragAndDrop($this->formSpec);
595
                $data = $dragAndDrop->process();
596
                $flagApiStructureReGroup = false;
597
598

                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
599
                break;
Carsten  Rose's avatar
Carsten Rose committed
600

601
            case FORM_REST:
602
                $flagApiStructureReGroup = false;
603
                $data = $this->doRestGet();
604
605
                break;

606
607
608
609
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

610
        if ($flagApiStructureReGroup && is_array($data)) {
611
            // $data['element-update']=...
612
613
            $data = $this->groupElementUpdateEntries($data);
        }
Carsten  Rose's avatar
Carsten Rose committed
614

Carsten  Rose's avatar
Carsten Rose committed
615
        return $data;
616
617
    }

618
619
620
621
622
623
624
625
    /**
     * @param array $restIds
     * @return array
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     */
626
    private function doRestGet() {
627

Carsten  Rose's avatar
Carsten Rose committed
628
        $this->nameGenericRestParam();
629
630
631
632

        $r = $this->store::getVar(TYPO3_RECORD_ID, STORE_TYPO3);
        $key = empty($r) ? F_REST_SQL_LIST : F_REST_SQL_DATA;

633
        if (!isset($this->formSpec[$key])) {
634
635
636
637
638
639
640
            throw new UserFormException("Missing Parameter '$key'", ERROR_INVALID_VALUE);
        }

        return $this->evaluate->parse($this->formSpec[$key]);

    }

Carsten  Rose's avatar
Carsten Rose committed
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
    /**
     * Checks if $serverToken matches HTTP_HEADER_AUTHORIZATION,
     * If not: throw an exception.
     *
     * @param string|array $serverToken
     * @throws CodeException
     * @throws UserFormException
     */
    private function restCheckAuthToken($serverToken) {

        // No serverToken: no check necessary
        if ($serverToken === '') {
            return;
        }

656
657
        $clientToken = $this->store::getVar(HTTP_HEADER_AUTHORIZATION, STORE_CLIENT, SANITIZE_ALLOW_ALL);
        if ($serverToken === $clientToken) {
Carsten  Rose's avatar
Carsten Rose committed
658
659
660
661
662
663
664
            return;
        }

        // Delay before answering.
        $seconds = $this->store::getVar(SYSTEM_SECURITY_FAILED_AUTH_DELAY, STORE_SYSTEM);
        sleep($seconds);

665
        if ($clientToken == false) {
666
            throw new UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Missing authorization token',
Marc Egger's avatar
Marc Egger committed
667
                ERROR_MESSAGE_TO_DEVELOPER => "Missing HTTP Header: " . HTTP_HEADER_AUTHORIZATION,
668
                ERROR_MESSAGE_HTTP_STATUS => HTTP_401_UNAUTHORIZED
669
670
671
            ]), ERROR_REST_AUTHORIZATION);
        }

672
        throw new UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Authorization token not accepted',
Marc Egger's avatar
Marc Egger committed
673
            ERROR_MESSAGE_TO_DEVELOPER => "Missing HTTP Header: " . HTTP_HEADER_AUTHORIZATION,
674
            ERROR_MESSAGE_HTTP_STATUS => HTTP_401_UNAUTHORIZED
675
        ]), ERROR_REST_AUTHORIZATION);
Carsten  Rose's avatar
Carsten Rose committed
676
677
    }

678
679
680
681
682
683
684
    /**
     * STORE_CLIENT: copy parameter _id1,_id2,...,_idN to named variables, specified via $this->formSpec[F_REST_PARAM] (CSV list)
     *
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
Carsten  Rose's avatar
Carsten Rose committed
685
    private function nameGenericRestParam() {
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704

        $paramNames = explode(',', $this->formSpec[F_REST_PARAM] ?? '');

        $ii = 1;
        foreach ($paramNames as $key) {
            switch ($key) {
                case CLIENT_FORM:
                case CLIENT_RECORD_ID:
                    throw new UserFormException("Name '$key' is forbidden in " . F_REST_PARAM, ERROR_INVALID_VALUE);
                    break;
                default:
                    break;
            }
            $val = $this->store::getVar(CLIENT_REST_ID . $ii, STORE_CLIENT);
            $this->store::setVar($key, $val, STORE_CLIENT);
            $ii++;
        }
    }

705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
    /**
     * Copies state 'hidden' from a FE pill to all FE child elements of that pill.
     *
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     */
    private function ifPillIsHiddenSetChildFeToHidden() {

        $feFilter = OnArray::filter($this->feSpecNative, FE_TYPE, FE_TYPE_PILL);

        if (!empty($feFilter)) {
            foreach ($feFilter AS $feParent) {

720
                if ($feParent[FE_MODE_SQL]) {
721
                    $mode = $this->evaluate->parse($feParent[FE_MODE_SQL]);
722
723
                    if ($mode != '') {
                        $feParent[FE_MODE] = $mode;
724
725
726
727
728
729
730
731
                    }
                }

                if ($feParent[FE_MODE] == FE_MODE_HIDDEN) {
                    $feChild = OnArray::filter($this->feSpecNative, FE_ID_CONTAINER, $feParent[FE_ID]);
                    foreach ($feChild AS $fe) {

                        # Search for origin
732
733
                        foreach ($this->feSpecNative as $key => $value) {
                            if ($value[FE_ID] == $fe[FE_ID]) {
734
735
736
737
738
739
740
741
742
743
                                $this->feSpecNative[$key][FE_MODE] = FE_MODE_HIDDEN;
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
    /**
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     */
    private function logFormSubmitRequest() {
        $formSubmitLogMode = $this->formSpec[F_FORM_SUBMIT_LOG_MODE] ??
            $this->store->getVar(SYSTEM_FORM_SUBMIT_LOG_MODE, STORE_SYSTEM, SANITIZE_ALLOW_ALNUMX);
        if ($formSubmitLogMode === FORM_SUBMIT_LOG_MODE_NONE) {
            return;
        }

        $formData = $_POST;
        unset($formData[CLIENT_SIP]);
        $formData = json_encode($formData, JSON_UNESCAPED_UNICODE);
        $clientIp = $_SERVER[CLIENT_REMOTE_ADDRESS];
        $userAgent = $_SERVER[CLIENT_HTTP_USER_AGENT];
        $sipData = json_encode($this->store->getStore(STORE_SIP), JSON_UNESCAPED_UNICODE);
        $formId = $this->formSpec[F_ID];
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
        $feUser = $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX);
        $pageId = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX);
        $sessionId = session_id();

        $sql = "INSERT INTO FormSubmitLog (formData, sipData, clientIp, feUser, userAgent, formId, recordId, pageId, sessionId, created)" .
769
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())";
770
771
772
773
        $params = [$formData, $sipData, $clientIp, $feUser, $userAgent, $formId, $recordId, $pageId, $sessionId];
        $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_REGULAR, $params);
    }

774
775
776
777
778
779
780
781
782
783
784

    /**
     * Check if forwardMode='url...'.
     * yes: process 'forwardPage' and fill $this->formSpec[F_FORWARD_MODE] and $this->formSpec[F_FORWARD_PAGE]
     * no: do nothing
     *
     * '$this->formSpec[F_FORWARD_PAGE]' might give a new forwardMode. If so, set $this->formSpec[F_FORWARD_MODE] to
     * it.
     *
     * '$this->formSpec[F_FORWARD_PAGE]':
     * a) url     http://www.nzz.ch/index.html?a=123#bottom, website.html?a=123#bottom,
785
     *            ?[id=]<T3 Alias pageid>&a=123#bottom, ?id=<T3 page id>&a=123#bottom
786
787
788
789
790
     * b) mode      no|client|url|...
     * c) mode|url  combination of above
     *
     * @return bool  TRUE if F_FORWARD_MODE = 'url..', else FALSE
     *
Carsten  Rose's avatar
Carsten Rose committed
791
792
     * @throws CodeException
     * @throws DbException
793
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
794
     * @throws UserReportException
795
796
797
     */
    private function setForwardModePage() {

798
        if (F_FORWARD_MODE_URL != substr($this->formSpec[F_FORWARD_MODE], 0, 3)) {
799
800
801
            return false;
        }

802
        $forwardPageTmp = $this->evaluate->parse($this->formSpec[F_FORWARD_PAGE]);
803
804
805
806
807
808

        // Format: [mode/url][|url]
        $forwardArray = explode('|', $forwardPageTmp, 2);
        $forward = trim($forwardArray[0]);
        switch ($forward) {

809
            case F_FORWARD_MODE_AUTO:
810
            case F_FORWARD_MODE_CLOSE:
811
812
813
814
            case F_FORWARD_MODE_NO:
            case F_FORWARD_MODE_URL:
            case F_FORWARD_MODE_URL_SKIP_HISTORY:
            case F_FORWARD_MODE_URL_SIP:
815
            case F_FORWARD_MODE_URL_SIP_SKIP_HISTORY:
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
                $this->formSpec[F_FORWARD_MODE] = $forward;
                if (isset($forwardArray[1])) {
                    $this->formSpec[F_FORWARD_PAGE] = trim($forwardArray[1]);
                } else {
                    $this->formSpec[F_FORWARD_PAGE] = '';
                }
                break;

            default:
                $this->formSpec[F_FORWARD_PAGE] = $forward;
                break;
        }

        if ('url' == substr($this->formSpec[F_FORWARD_MODE], 0, 3)) {
            if ($this->formSpec[F_FORWARD_PAGE] == '') {
831
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_AUTO;
832
833
834
835
836
837
838
839
840
841
842
843
844
                $customForward = false;
            } else {
                $customForward = true;
            }

        } else {
            $customForward = false;
        }

        return $customForward;

    }

Carsten  Rose's avatar
Carsten Rose committed
845
846
847
    /**
     * Iterate over all Clipboard source records and fire for each all FE.type=paste records.
     *
Carsten  Rose's avatar
Carsten Rose committed
848
     * @param int $formId
Carsten  Rose's avatar
Carsten Rose committed
849
     * @param FormAction $formAction
850
     *
Carsten  Rose's avatar
Carsten Rose committed
851
852
853
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
854
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
855
856
857
     */
    private function pasteClipboard($formId, FormAction $formAction) {

858
859
860
861
        if (!$this->isPasteRecord()) {
            return;
        }

Carsten  Rose's avatar
Carsten Rose committed
862
863
864
865
866
867
868
        $cookieQfq = $this->store->getVar(CLIENT_COOKIE_QFQ, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
        if ($cookieQfq === false || $cookieQfq == '') {
            throw new UserFormException('Qfq Session missing', ERROR_QFQ_SESSION_MISSING);
        }

        # select clipboard records
        $sql = "SELECT c.idSrc as id, c.xId FROM Clipboard AS c WHERE c.cookie='$cookieQfq' AND c.formIdPaste=$formId ORDER BY c.id";
869
        $arrClipboard = $this->dbArray[$this->dbIndexQfq]->sql($sql);
Carsten  Rose's avatar
Carsten Rose committed
870
871
872
873
874
875
876
877

        // Process clipboard records.
        foreach ($arrClipboard AS $srcIdRecord) {
            $formAction->doAllFormElementPaste($this->feSpecAction, $this->formSpec[F_TABLE_NAME], $this->formSpec[F_TABLE_NAME], "", $srcIdRecord);
        }

    } # doClipboard()

878
879
880
881
882
883
884
885
886
887
    /**
     * @return bool  true if there is at least one paste record, else false.
     */
    private function isPasteRecord() {

        foreach ($this->feSpecAction as $formElement) {
            if ($formElement[FE_TYPE] == FE_TYPE_PASTE) {
                return true;
            }
        }
888

889
890
891
892
        return false;

    }

893
894
895
896
897
    /**
     * Set F_FORWARD_MODE to  F_FORWARD_MODE_PAGE and builds a redirection URL to the current page with the already
     * used parameters. Do this by building a new SIP with the new recordId.
     *
     * @param array $formSpec
Carsten  Rose's avatar
Carsten Rose committed
898
     * @param int $recordId
899
     *
900
901
902
903
904
905
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
    private function buildNSetReloadUrl(array $formSpec, $recordId) {

906
        $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923

        // Rebuild original URL
        $storeT3 = $this->store->getStore(STORE_TYPO3);
        $storeT3['id'] = $storeT3[TYPO3_PAGE_ID];
        $storeT3 = OnArray::getArrayItems($storeT3, ['id', TYPO3_PAGE_TYPE, TYPO3_PAGE_LANGUAGE], true, true);

        $arr = KeyValueStringParser::parse($this->store->getVar(SIP_URLPARAM, STORE_SIP), '=', '&');
        $arr[SIP_RECORD_ID] = $recordId;
        $arr = array_merge($storeT3, $arr);
        $queryString = KeyValueStringParser::unparse($arr, '=', '&');

        $formSpec[F_FORWARD_PAGE] = store::getSipInstance()->queryStringToSip($queryString, RETURN_URL);

        return $formSpec;

    }

924
    /**
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
     * Checks if there is formLog mode active for FORM_LOG_SESSION or FORM_LOG_ALL.
     * If yes, set $form[FORM_LOG_FILE_SESSION] resp.  $form[FORM_LOG_FILE_ALL].
     * If the last action is older FORM_LOG_FILE_EXPIRE, the file will be deleted and formLog mode stops (disabled).
     *
     * @param array $form
     * @return array
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
    private function checkFormLogMode(array $form) {

        $form[FORM_LOG_FILE_SESSION] = '';
        $form[FORM_LOG_FILE_ALL] = '';

        foreach ([FORM_LOG_SESSION, FORM_LOG_ALL] as $mode) {
            $file = Support::getFormLogFileName($form[F_NAME], $mode);
942
            if (file_exists($file) && false !== ($arr = stat($file))) {
943
944

                if (time() - $arr['mtime'] > FORM_LOG_FILE_EXPIRE) {
945
                    HelperFile::unlink($file);
946
947
                } else {
                    $form[FORM_LOG_FILE . '_' . $mode] = $file;
948
                    $form[FORM_LOG_ACTIVE] = 1;
949
950
951
952
953
954
955
                }
            }
        }

        return $form;
    }

956
    /**
957
958
     * Get form name
     * Check if the form is in log mode: set formLog and return
959
     * Load form. Evaluates form. Load FormElements.
960
     *
Carsten  Rose's avatar
Carsten Rose committed
961
     * After processing:
962
963
964
965
     * Loaded Form is in  $this->formSpec
     * Loaded 'action' FormElements are in $this->feSpecAction
     * Loaded 'native' FormElements are in $this->feSpecNative
     *
966
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE|FORM_REST
Carsten  Rose's avatar
Carsten Rose committed
967
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
968
     * @param string $foundInStore
969
     * @param string $formLogMode
Carsten  Rose's avatar
Carsten Rose committed
970
     * @return bool|string if found the formName, else 'false'.
971
     *
Carsten  Rose's avatar
Carsten Rose committed
972
     * @throws CodeException
973
     * @throws DbException
974
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
975
     * @throws UserReportException
976
     */
977
978
979
    private function loadFormSpecification($mode, $recordId, &$foundInStore = '', &$formLogMode = '') {

        $formLogMode = false; // Important: if no form is found, formLogMode needs also to be false.
Carsten  Rose's avatar
Carsten Rose committed
980

981
        // formName
Carsten  Rose's avatar
Carsten Rose committed
982
        if (false === ($formName = $this->getFormName($mode, $foundInStore))) {