QuickFormQuery.php 71.7 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;

Carsten  Rose's avatar
Carsten Rose committed
11
use qfq;
12
13
14
15
16
17
18
19
20

//use qfq\Report;

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

Carsten  Rose's avatar
Carsten Rose committed
24

25
require_once(__DIR__ . '/store/Store.php');
26
require_once(__DIR__ . '/store/Sip.php');
27
28
29
30
31
32
33
34
35
36
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');
37
38
require_once(__DIR__ . '/database/Database.php');
require_once(__DIR__ . '/database/DatabaseUpdate.php');
39
40
41
42
43
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');
44
require_once(__DIR__ . '/report/Monitor.php');
45
46
require_once(__DIR__ . '/BodytextParser.php');
require_once(__DIR__ . '/Delete.php');
47
require_once(__DIR__ . '/form/FormAction.php');
Carsten  Rose's avatar
Carsten Rose committed
48
require_once(__DIR__ . '/form/Dirty.php');
49
require_once(__DIR__ . '/form/DragAndDrop.php');
50
51
52
53
54
55
56
57
58
59
60
61
/*
 * 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
62
/**
63
 * Class Qfq
Carsten  Rose's avatar
Carsten Rose committed
64
65
 * @package qfq
 */
66
class QuickFormQuery {
67

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

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

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

83
84
85
    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
86
    protected $feSpecNativeRaw = array(); // FormEelement Definition: all formElement.class='action' of the loaded form
87

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

93
94
95
    /**
     * @var bool
     */
96
97
    private $phpUnit = false;

98
99
100
101
102
    /**
     * @var bool
     */
    private $inlineReport = false;

103
104
105
106
107
    /**
     * @var Session
     */
    private $session = null;

108
109
110
    private $dbIndexData = false;
    private $dbIndexQfq = false;

111
112
113
114
115
116
117
118
119
120
121
    /*
     * 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'
     */
122

123
124
125
    /**
     * Construct the Form Class and Store too. This is the base initialization moment.
     *
126
127
     * 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.
128
     *
129
     * @param array $t3data
Carsten  Rose's avatar
Carsten Rose committed
130
     * @param bool $phpUnit
131
     * @param bool $inlineReport
132
     *
133
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
134
     * @throws DbException
135
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
136
     * @throws UserReportException
137
     */
138
    public function __construct(array $t3data = array(), $phpUnit = false, $inlineReport = true) {
139
        $this->phpUnit = $phpUnit;
140
        $this->inlineReport = $inlineReport;
141

142
        mb_internal_encoding("UTF-8");
143

144
145
        $this->session = Session::getInstance($phpUnit);

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

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

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

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

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

165
166
        $this->t3data = $t3data;

167
        $bodytext = $this->t3data[T3DATA_BODYTEXT];
168
169

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

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

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

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

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

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

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

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

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

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

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

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

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

Carsten  Rose's avatar
Carsten Rose committed
218
219
220
221
        if ($this->formSpec[F_FORWARD_MODE] == F_FORWARD_MODE_URL_SIP) {
            $forwardPage = store::getSipInstance()->queryStringToSip($forwardPage, RETURN_URL);
            // 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.
222
            // An option for better implementing would be to separate SKIP History from ForwardMode. For API, it can be combined again.
Carsten  Rose's avatar
Carsten Rose committed
223
224
225
            $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL_SKIP_HISTORY;
        }

226
227
        return ([
            API_REDIRECT => $this->formSpec[F_FORWARD_MODE],
Carsten  Rose's avatar
Carsten Rose committed
228
            API_REDIRECT_URL => $forwardPage,
229
        ]);
230
231
    }

232
    /**
Carsten  Rose's avatar
Carsten Rose committed
233
     * Main entrypoint for display content: a) form and/or b) report
234
     *
235
     * @return string
236
237
     * @throws CodeException
     * @throws DbException
238
     * @throws DownloadException
239
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
240
     * @throws UserReportException
241
242
243
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
Carsten  Rose's avatar
Carsten Rose committed
244
     */
245
    public function process() {
246
        $html = '';
247

248
        if ($this->store->getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === 'yes') {
249
250
            $htmlId = HelperFormElement::buildFormElementId($this->formSpec[F_ID], 0, 0, 0);
            $html .= Support::doTooltip($htmlId . HTML_ID_EXTENSION_TOOLTIP, $this->t3data['bodytext']);
251
252
253
        }

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

256
        // 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
257
258
259
260
        if ($this->store->getVar(SYSTEM_DOWNLOAD_POPUP, STORE_SYSTEM) == DOWNLOAD_POPUP_REQUEST) {
            $html .= $this->getModalCode();
        }

261
262
263
264
265
        // Only needed if there are 'drag and drop' elements.
        if ($this->store->getVar(SYSTEM_DRAG_AND_DROP_JS, STORE_SYSTEM) == 'true') {
            $html .= $this->getDragAndDropCode();
        }

266
        $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
267
        if ($class) {
268
            $html = Support::wrapTag("<div class='$class'>", $html);
Carsten  Rose's avatar
Carsten Rose committed
269
270
        }

271
        return $html;
272
273
    }

274
275
    /**
     * Determine the name of the language parameter field, which has to be taken to fill language specific defintions.
276
277
278
     *
     * @throws CodeException
     * @throws UserFormException
279
280
281
282
283
284
285
286
287
     */
    private function setParameterLanguageFieldName() {

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

        foreach (['A', 'B', 'C', 'D'] as $key) {
288
            $languageIdx = SYSTEM_FORM_LANGUAGE . "$key" . "Id";
289
290
291
292
293
294
295
            if ($this->store->getVar($languageIdx, STORE_SYSTEM) == $typo3PageLanguage) {
                $this->store->setVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, 'parameterLanguage' . $key, STORE_SYSTEM);
                break;
            }
        }
    }

296
    /**
297
298
     * Creates an empty file. This indicates that the current form is in debug mode. Returns HTML element which will be
     * replaced by the logfile.
299
300
301
     *
     * @param $formName
     * @param $formLogMode
302
     * @return string
303
304
305
306
307
308
309
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
    private function getFormLog($formName, $formLogMode) {

        $formLogFileName = Support::getFormLogFileName($formName, $formLogMode);
310
        file_put_contents($formLogFileName, '');
311

312
313
        $monitor = new Monitor();

314
315
        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]);
316
    }
317

318
    /**
319
     * Process form.
320
321
322
323
324
     * $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:
325
     *
326
     * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE
327
     *
328
     * @return array|string
329
     * @throws CodeException
330
     * @throws DbException
331
     * @throws DownloadException
332
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
333
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
334
335
336
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
337
     */
338
    private function doForm($formMode) {
Carsten  Rose's avatar
Carsten Rose committed
339
        $data = '';
Carsten  Rose's avatar
Carsten Rose committed
340
        $foundInStore = '';
341
        $flagApiStructureReGroup = true;
342

Carsten  Rose's avatar
Carsten Rose committed
343
        // Fill STORE_FORM
344
        if ($formMode === FORM_UPDATE || $formMode === FORM_SAVE) {
Carsten  Rose's avatar
Carsten Rose committed
345
            $fillStoreForm = new FillStoreForm();
346
            $fillStoreForm->process($formMode);
Carsten  Rose's avatar
Carsten Rose committed
347
        }
348

349
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO);
350
        $this->setParameterLanguageFieldName();
351

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

357
        if ($formName === false) {
358
            switch ($formMode) {
359
360
361
362
363
364
365
                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
            }
366
        }
367

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

Carsten  Rose's avatar
Carsten Rose committed
371

372
373
        if ($formName !== false) {
            // Validate only if there is a 'real' form (not a FORM_DELETE with only a tablename).
374
            $sipFound = $this->validateForm($foundInStore, $formMode);
375
376

        } else {
377
            // FORM_DELETE without a form definition: Fake the form with only a tableName.
378
379
            $table = $this->store->getVar(SIP_TABLE, STORE_SIP);
            if ($table === false) {
380
                throw new UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE);
381
            }
382

383
384
385
            $sipFound = true;
            $this->formSpec[F_NAME] = '';
            $this->formSpec[F_TABLE_NAME] = $table;
386
387
            $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.
388
389
390
391
392
393
394
395
396
397

            $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);
                    }
                }
            }
398
399
        }

400
401
        // 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.
402
403
404
405
        if ($formMode != FORM_REST) {
            if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) {
                $this->store->createSipAfterFormLoad($formName);
            }
406
        }
407

408
        // Fill STORE_BEFORE
409
        if ($this->store->getVar($this->formSpec[F_PRIMARY_KEY], STORE_BEFORE) === false) {
410
            $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId,
411
                $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY], STORE_BEFORE);
412
413
        }

414
        // Check (and release) dirtyRecord.
Carsten  Rose's avatar
Carsten Rose committed
415
        if ($formMode === FORM_DELETE || $formMode === FORM_SAVE) {
416
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
Carsten  Rose's avatar
Carsten Rose committed
417

418
            $answer = $dirty->checkDirtyAndRelease($formMode, $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS],
419
                $this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $this->formSpec[F_PRIMARY_KEY], $recordId, true);
420

421
            // In case of a conflict, return immediately
Carsten  Rose's avatar
Carsten Rose committed
422
423
            if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
                $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
424

425
426
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
427
428
        }

429
        // FORM_LOAD: if there is a foreign exclusive record lock - show form in F_MODE_READONLY mode.
430
        if ($formMode === FORM_LOAD) {
431
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
432
433
434
435
436
437
438
            $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;
            }
        }

439
440
441
442
443
444
445
446
447
448
        switch ($formMode) {
            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:
449

450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
                $tableDefinition = $this->dbArray[$this->dbIndexData]->getTableDefinition($this->formSpec[F_TABLE_NAME]);
                $this->store->fillStoreTableDefaultColumnType($tableDefinition);

                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);
469
470
        }

471
        $formAction = new FormAction($this->formSpec, $this->dbArray[$this->dbIndexData], $this->phpUnit);
472
        switch ($formMode) {
473
            case FORM_LOAD:
474
475
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
                $data = $build->process($formMode);
476
477
478
479
                $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);
480
481
482
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
                break;

Carsten  Rose's avatar
Carsten Rose committed
483
            case FORM_UPDATE:
484
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
485
486
                // data['form-update']=....
                $data = $build->process($formMode);
487
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
488
                break;
Carsten  Rose's avatar
Carsten Rose committed
489

490
491
492
            case FORM_DELETE:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_DELETE);

493
                $build->process($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);
494
495
496
497

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

498
            case FORM_SAVE:
499
500
                $this->logFormSubmitRequest();

501
502
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

503
                // Action: Before
504
505
                $feTypeList = FE_TYPE_BEFORE_SAVE . ',' . ($recordId == 0 ? FE_TYPE_BEFORE_INSERT : FE_TYPE_BEFORE_UPDATE);
                $formAction->elements($recordId, $this->feSpecAction, $feTypeList);
506

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

511
512
                $this->ifPillIsHiddenSetChildFeToHidden();

513
                // SAVE
514
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw);
515
516

                $save->processAllImageCutFE();
517
                $save->checkRequiredHidden();
518

519
520
                $rc = $save->process();

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

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

526
                // Action: After
527
528
                $feTypeList = FE_TYPE_AFTER_SAVE . ',' . ($recordId == 0 ? FE_TYPE_AFTER_INSERT : FE_TYPE_AFTER_UPDATE);
                $status = $formAction->elements($rc, $this->feSpecAction, $feTypeList);
529
                if ($status != ACTION_ELEMENT_NO_CHANGE) {
530
                    // Reload fresh saved record and fill STORE_RECORD with it.
531
                    $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]);
532
                }
533

Carsten  Rose's avatar
Carsten Rose committed
534
535
536
537
538
539
                // Action: Paste
                $this->pasteClipboard($this->formSpec[F_ID], $formAction);

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

540
541
542
543
544
545
546

                $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!
547
                $getJson = true;
548
549
550
551
552
                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);
553
                    $getJson = false;
554
                }
555

556
                if ($getJson) {
557

558
                    // Values of FormElements might be changed during 'afterSave': rebuild the form to load the new values. Especially for non primary template groups.
559
560
561
562
                    $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);
563

564
565
                    // Retrieve FE Values as JSON
                    // $data['form-update']=...
566
                    // $data = $build->process($formMode, $htmlElementNameIdZero);
567
                    $data = $build->process($formMode, false, $this->feSpecNative);
568
                }
569
                break;
570

571
572
573
            case FORM_DRAG_AND_DROP:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);

574
                $dragAndDrop = new DragAndDrop($this->formSpec);
575
                $data = $dragAndDrop->process();
576
                $flagApiStructureReGroup = false;
577
578

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

581
            case FORM_REST:
582
                $data = $this->doRestGet();
583
584
585
                $flagApiStructureReGroup = false;
                break;

586
587
588
589
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

590
        if ($flagApiStructureReGroup && is_array($data)) {
591
            // $data['element-update']=...
592
593
            $data = $this->groupElementUpdateEntries($data);
        }
Carsten  Rose's avatar
Carsten Rose committed
594

Carsten  Rose's avatar
Carsten Rose committed
595
        return $data;
596
597
    }

598
599
600
601
602
603
604
605
    /**
     * @param array $restIds
     * @return array
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
     */
606
    private function doRestGet() {
607

Carsten  Rose's avatar
Carsten Rose committed
608
        $this->nameGenericRestParam();
609
610
611
612

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

613
        if (!isset($this->formSpec[$key])) {
614
615
616
617
618
619
620
            throw new UserFormException("Missing Parameter '$key'", ERROR_INVALID_VALUE);
        }

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

    }

Carsten  Rose's avatar
Carsten Rose committed
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
    /**
     * 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;
        }

        if ($serverToken === $this->store::getVar(HTTP_HEADER_AUTHORIZATION, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALL)) {
            return;
        }

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

        throw new UserFormException('Missing or wrong authorization token', ERROR_REST_AUTHORIZATION);
    }

647
648
649
650
651
652
653
    /**
     * 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
654
    private function nameGenericRestParam() {
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673

        $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++;
        }
    }

674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
    /**
     * 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) {

689
                if ($feParent[FE_MODE_SQL]) {
690
                    $mode = $this->evaluate->parse($feParent[FE_MODE_SQL]);
691
692
                    if ($mode != '') {
                        $feParent[FE_MODE] = $mode;
693
694
695
696
697
698
699
700
                    }
                }

                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
701
702
                        foreach ($this->feSpecNative as $key => $value) {
                            if ($value[FE_ID] == $fe[FE_ID]) {
703
704
705
706
707
708
709
710
711
712
                                $this->feSpecNative[$key][FE_MODE] = FE_MODE_HIDDEN;
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
    /**
     * @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)" .
738
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())";
739
740
741
742
        $params = [$formData, $sipData, $clientIp, $feUser, $userAgent, $formId, $recordId, $pageId, $sessionId];
        $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_REGULAR, $params);
    }

743
744
745
746
747
748
749
750
751
752
753

    /**
     * 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,
754
     *            ?[id=]<T3 Alias pageid>&a=123#bottom, ?id=<T3 page id>&a=123#bottom
755
756
757
758
759
     * 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
760
761
     * @throws CodeException
     * @throws DbException
762
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
763
     * @throws UserReportException
764
765
766
     */
    private function setForwardModePage() {

767
        if (F_FORWARD_MODE_URL != substr($this->formSpec[F_FORWARD_MODE], 0, 3)) {
768
769
770
            return false;
        }

771
        $forwardPageTmp = $this->evaluate->parse($this->formSpec[F_FORWARD_PAGE]);
772
773
774
775
776
777

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

778
            case F_FORWARD_MODE_AUTO:
779
            case F_FORWARD_MODE_CLOSE:
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
            case F_FORWARD_MODE_NO:
            case F_FORWARD_MODE_URL:
            case F_FORWARD_MODE_URL_SKIP_HISTORY:
            case F_FORWARD_MODE_URL_SIP:
                $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] == '') {
799
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_AUTO;
800
801
802
803
804
805
806
807
808
809
810
811
812
                $customForward = false;
            } else {
                $customForward = true;
            }

        } else {
            $customForward = false;
        }

        return $customForward;

    }

Carsten  Rose's avatar
Carsten Rose committed
813
814
815
    /**
     * Iterate over all Clipboard source records and fire for each all FE.type=paste records.
     *
Carsten  Rose's avatar
Carsten Rose committed
816
     * @param int $formId
Carsten  Rose's avatar
Carsten Rose committed
817
     * @param FormAction $formAction
818
     *
Carsten  Rose's avatar
Carsten Rose committed
819
820
821
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
822
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
823
824
825
     */
    private function pasteClipboard($formId, FormAction $formAction) {

826
827
828
829
        if (!$this->isPasteRecord()) {
            return;
        }

Carsten  Rose's avatar
Carsten Rose committed
830
831
832
833
834
835
836
        $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";
837
        $arrClipboard = $this->dbArray[$this->dbIndexQfq]->sql($sql);
Carsten  Rose's avatar
Carsten Rose committed
838
839
840
841
842
843
844
845

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

    } # doClipboard()

846
847
848
849
850
851
852
853
854
855
    /**
     * @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;
            }
        }
856

857
858
859
860
        return false;

    }

861
862
863
864
865
    /**
     * 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
866
     * @param int $recordId
867
     *
868
869
870
871
872
873
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
    private function buildNSetReloadUrl(array $formSpec, $recordId) {

874
        $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891

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

    }

892
    /**
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
     * 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);
910
            if (file_exists($file) && false !== ($arr = stat($file))) {
911
912

                if (time() - $arr['mtime'] > FORM_LOG_FILE_EXPIRE) {
913
                    HelperFile::unlink($file);
914
915
                } else {
                    $form[FORM_LOG_FILE . '_' . $mode] = $file;
916
                    $form[FORM_LOG_ACTIVE] = 1;
917
918
919
920
921
922
923
                }
            }
        }

        return $form;
    }

924
    /**
925
926
     * Get form name
     * Check if the form is in log mode: set formLog and return
927
     * Load form. Evaluates form. Load FormElements.
928
     *
Carsten  Rose's avatar
Carsten Rose committed
929
     * After processing:
930
931
932
933
     * Loaded Form is in  $this->formSpec
     * Loaded 'action' FormElements are in $this->feSpecAction
     * Loaded 'native' FormElements are in $this->feSpecNative
     *
934
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE|FORM_REST
Carsten  Rose's avatar
Carsten Rose committed
935
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
936
     * @param string $foundInStore
937
     * @param string $formLogMode
Carsten  Rose's avatar
Carsten Rose committed
938
     * @return bool|string if found the formName, else 'false'.
939
     *
Carsten  Rose's avatar
Carsten Rose committed
940
     * @throws CodeException
941
     * @throws DbException
942
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
943
     * @throws UserReportException
944
     */
945
946
947
    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
948

949
        // formName
Carsten  Rose's avatar
Carsten Rose committed
950
        if (false === ($formName = $this->getFormName($mode, $foundInStore))) {
951
952
            return false;
        }
953

954
955
956
957
        // Check for '_formLogMode'=logSession|logAll
        $formLogMode = $this->store::getVar(FORM_LOG_MODE, STORE_SIP);
        if ($formLogMode !== false) {
            return $formName; // fomLog: getting the formName is sufficient.
958
959
        }

960
961
        if (!$this->dbArray[$this->dbIndexQfq]->existTable(TABLE_NAME_FORM)) {
            throw new UserFormException("Table '" . TABLE_NAME_FORM . "' not found", ERROR_MISSING_TABLE);
962
963
        }

964
        // Preparation for Log, Debug
965
        $this->store->setVar(SYSTEM_FORM, $formName, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
966

967
968
        // Check if there is a recordId specified in Bodytext - as variable or query.
        $rTmp = $this->store->getVar(CLIENT_RECORD_ID, STORE_TYPO3, SANITIZE_ALLOW_ALL);
969
        if (false !== $rTmp && !ctype_digit($rTmp)) {
970
            $rTmp = $this->evaluate->parse($rTmp);
971
972
973
            $this->store->setVar(CLIENT_RECORD_ID, $rTmp, STORE_TYPO3);
        }

974
        // Load form
975
        $constant = F_NAME; // PhpStorm complains if the constant is directly defined in the string below
976
        $form = $this->dbArray[$this->dbIndexQfq]->sql("SELECT * FROM Form AS f WHERE f.$constant LIKE ? AND f.deleted='no'", ROW_EXPECT_1,
977
            [$formName], 'Form "' . $formName . '" not found or multiple forms with the same name.');
978

979
        $form = $this->checkFormLogMode($form);