QuickFormQuery.php 66 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
173
174

        if (Session::getAndDestroyFlagFeUserHasChanged()) {
            $this->store->unsetStore(STORE_USER);
        }

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

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

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

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

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

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

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

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

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

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

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

Carsten  Rose's avatar
Carsten Rose committed
217
218
219
220
        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.
221
            // 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
222
223
224
            $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL_SKIP_HISTORY;
        }

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

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

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

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

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

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

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

270
        return $html;
271
272
    }

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

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

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

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

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

311
312
        $monitor = new Monitor();

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

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

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

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

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

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

367
368
        Session::checkSessionExpired($this->formSpec[F_SESSION_TIMEOUT_SECONDS]);

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

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

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

            $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);
                    }
                }
            }
395
396
        }

397
398
        // 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.
399
        if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) {
400
401
            $this->store->createSipAfterFormLoad($formName);
        }
402

403
        if ($this->store->getVar($this->formSpec[F_PRIMARY_KEY], STORE_BEFORE) === false) {
404
            $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId,
405
                $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY], STORE_BEFORE);
406
407
        }

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

412
            $answer = $dirty->checkDirtyAndRelease($formMode, $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS],
413
                $this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $this->formSpec[F_PRIMARY_KEY], $recordId, true);
414

415
            // In case of a conflict, return immediately
Carsten  Rose's avatar
Carsten Rose committed
416
417
            if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
                $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
418

419
420
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
421
422
        }

423
424
        // FORM_LOAD: if there is an foreign exclusive record lock - show form in F_MODE_READONLY mode.
        if ($formMode === FORM_LOAD) {
425
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
426
427
428
429
430
431
432
            $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;
            }
        }

433
        if ($formMode === FORM_DELETE) {
434

435
            $build = new Delete($this->dbIndexData);
436
437

        } else {
438
            $tableDefinition = $this->dbArray[$this->dbIndexData]->getTableDefinition($this->formSpec[F_TABLE_NAME]);
439
            $this->store->fillStoreTableDefaultColumnType($tableDefinition);
440
441
442

            switch ($this->formSpec['render']) {
                case 'plain':
443
                    $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
444
445
                    break;
                case 'table':
446
                    $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
447
448
                    break;
                case 'bootstrap':
449
                    $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
450
451
452
453
                    break;
                default:
                    throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
            }
454
455
        }

456
        $formAction = new FormAction($this->formSpec, $this->dbArray[$this->dbIndexData], $this->phpUnit);
457
        switch ($formMode) {
458
            case FORM_LOAD:
459
460
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
                $data = $build->process($formMode);
461
462
463
464
                $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);
465
466
467
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
                break;

Carsten  Rose's avatar
Carsten Rose committed
468
            case FORM_UPDATE:
469
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
470
471
                // data['form-update']=....
                $data = $build->process($formMode);
472
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
473
                break;
Carsten  Rose's avatar
Carsten Rose committed
474

475
476
477
            case FORM_DELETE:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_DELETE);

478
                $build->process($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);
479
480
481
482

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

483
            case FORM_SAVE:
484
485
                $this->logFormSubmitRequest();

486
487
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

488
                // Action: Before
489
490
                $feTypeList = FE_TYPE_BEFORE_SAVE . ',' . ($recordId == 0 ? FE_TYPE_BEFORE_INSERT : FE_TYPE_BEFORE_UPDATE);
                $formAction->elements($recordId, $this->feSpecAction, $feTypeList);
491

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

496
497
                $this->ifPillIsHiddenSetChildFeToHidden();

498
                // SAVE
499
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw);
500
501

                $save->processAllImageCutFE();
502
                $save->checkRequiredHidden();
503

504
505
                $rc = $save->process();

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

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

511
                // Action: After
512
513
                $feTypeList = FE_TYPE_AFTER_SAVE . ',' . ($recordId == 0 ? FE_TYPE_AFTER_INSERT : FE_TYPE_AFTER_UPDATE);
                $status = $formAction->elements($rc, $this->feSpecAction, $feTypeList);
514
                if ($status != ACTION_ELEMENT_NO_CHANGE) {
515
                    // Reload fresh saved record and fill STORE_RECORD with it.
516
                    $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]);
517
                }
518

Carsten  Rose's avatar
Carsten Rose committed
519
520
521
522
523
524
                // Action: Paste
                $this->pasteClipboard($this->formSpec[F_ID], $formAction);

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

525
526
527
528
529
530
531

                $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!
532
                $getJson = true;
533
534
535
536
537
                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);
538
                    $getJson = false;
539
                }
540

541
                if ($getJson) {
542

543
544
                    // Values of FormElements might be changed during 'afterSave': rebuild the form to load the new values. Especially for non primary template groups.
//                    $this->loadFormSpecification($formMode, $recordId, $foundInStore);
545
546
547
548
                    $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);
549

550
551
                    // Retrieve FE Values as JSON
                    // $data['form-update']=...
552
                    // $data = $build->process($formMode, $htmlElementNameIdZero);
553
                    $data = $build->process($formMode, false, $this->feSpecNative);
554
                }
555
                break;
556

557
558
559
            case FORM_DRAG_AND_DROP:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);

560
                $dragAndDrop = new DragAndDrop($this->formSpec);
561
                $data = $dragAndDrop->process();
562
                $flagApiStructureReGroup = false;
563
564

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

567
568
569
570
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

571
        if ($flagApiStructureReGroup && is_array($data)) {
572
            // $data['element-update']=...
573
574
            $data = $this->groupElementUpdateEntries($data);
        }
Carsten  Rose's avatar
Carsten Rose committed
575

Carsten  Rose's avatar
Carsten Rose committed
576
        return $data;
577
578
    }

579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
    /**
     * 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) {

                if($feParent[FE_MODE_SQL]){
                    $mode = $this->evaluate->parse($feParent[FE_MODE_SQL]);
                    if($mode!=''){
                        $feParent[FE_MODE]=$mode;
                    }
                }

                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
                        foreach($this->feSpecNative as $key => $value){
                            if($value[FE_ID]==$fe[FE_ID]){
                                $this->feSpecNative[$key][FE_MODE] = FE_MODE_HIDDEN;
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
    /**
     * @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)" .
643
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())";
644
645
646
647
        $params = [$formData, $sipData, $clientIp, $feUser, $userAgent, $formId, $recordId, $pageId, $sessionId];
        $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_REGULAR, $params);
    }

648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664

    /**
     * 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,
     *            ?<T3 Alias pageid>&a=123#bottom, ?id=<T3 page id>&a=123#bottom
     * 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
665
666
     * @throws CodeException
     * @throws DbException
667
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
668
     * @throws UserReportException
669
670
671
672
673
674
675
     */
    private function setForwardModePage() {

        if ('url' != substr($this->formSpec[F_FORWARD_MODE], 0, 3)) {
            return false;
        }

676
        $forwardPageTmp = $this->evaluate->parse($this->formSpec[F_FORWARD_PAGE]);
677
678
679
680
681
682

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

683
            case F_FORWARD_MODE_AUTO:
684
            case F_FORWARD_MODE_CLOSE:
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
            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] == '') {
704
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_AUTO;
705
706
707
708
709
710
711
712
713
714
715
716
717
                $customForward = false;
            } else {
                $customForward = true;
            }

        } else {
            $customForward = false;
        }

        return $customForward;

    }

Carsten  Rose's avatar
Carsten Rose committed
718
719
720
    /**
     * Iterate over all Clipboard source records and fire for each all FE.type=paste records.
     *
Carsten  Rose's avatar
Carsten Rose committed
721
     * @param int $formId
Carsten  Rose's avatar
Carsten Rose committed
722
     * @param FormAction $formAction
723
     *
Carsten  Rose's avatar
Carsten Rose committed
724
725
726
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
727
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
728
729
730
     */
    private function pasteClipboard($formId, FormAction $formAction) {

731
732
733
734
        if (!$this->isPasteRecord()) {
            return;
        }

Carsten  Rose's avatar
Carsten Rose committed
735
736
737
738
739
740
741
        $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";
742
        $arrClipboard = $this->dbArray[$this->dbIndexQfq]->sql($sql);
Carsten  Rose's avatar
Carsten Rose committed
743
744
745
746
747
748
749
750

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

    } # doClipboard()

751
752
753
754
755
756
757
758
759
760
    /**
     * @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;
            }
        }
761

762
763
764
765
        return false;

    }

766
767
768
769
770
    /**
     * 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
771
     * @param int $recordId
772
     *
773
774
775
776
777
778
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
    private function buildNSetReloadUrl(array $formSpec, $recordId) {

779
        $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796

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

    }

797
    /**
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
     * 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);
815
            if (file_exists($file) && false !== ($arr = stat($file))) {
816
817

                if (time() - $arr['mtime'] > FORM_LOG_FILE_EXPIRE) {
818
                    HelperFile::unlink($file);
819
820
                } else {
                    $form[FORM_LOG_FILE . '_' . $mode] = $file;
821
                    $form[FORM_LOG_ACTIVE] = 1;
822
823
824
825
826
827
828
                }
            }
        }

        return $form;
    }

829
    /**
830
831
     * Get form name
     * Check if the form is in log mode: set formLog and return
832
     * Load form. Evaluates form. Load FormElements.
833
     *
Carsten  Rose's avatar
Carsten Rose committed
834
     * After processing:
835
836
837
838
     * Loaded Form is in  $this->formSpec
     * Loaded 'action' FormElements are in $this->feSpecAction
     * Loaded 'native' FormElements are in $this->feSpecNative
     *
Carsten  Rose's avatar
Carsten Rose committed
839
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
840
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
841
     * @param string $foundInStore
842
     * @param string $formLogMode
Carsten  Rose's avatar
Carsten Rose committed
843
     * @return bool|string if found the formName, else 'false'.
844
     *
Carsten  Rose's avatar
Carsten Rose committed
845
     * @throws CodeException
846
     * @throws DbException
847
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
848
     * @throws UserReportException
849
     */
850
851
852
    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
853

854
        // formName
Carsten  Rose's avatar
Carsten Rose committed
855
        if (false === ($formName = $this->getFormName($mode, $foundInStore))) {
856
857
            return false;
        }
858

859
860
861
862
        // 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.
863
864
        }

865
866
        if (!$this->dbArray[$this->dbIndexQfq]->existTable(TABLE_NAME_FORM)) {
            throw new UserFormException("Table '" . TABLE_NAME_FORM . "' not found", ERROR_MISSING_TABLE);
867
868
        }

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

872
873
874
        // 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);
        if (false !== $rTmp && !is_int($rTmp)) {
875
            $rTmp = $this->evaluate->parse($rTmp);
876
877
878
            $this->store->setVar(CLIENT_RECORD_ID, $rTmp, STORE_TYPO3);
        }

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

884
        $form = $this->checkFormLogMode($form);
885
        $form = $this->modeCleanFormConfig($mode, $form);
886

Carsten  Rose's avatar
Carsten Rose committed
887
888
889
890
        // Save specific elements to be expanded later.
        $parseLater = OnArray::getArrayItems($form, [F_FORWARD_PAGE]);
        $form[F_FORWARD_PAGE] = '';

891
892
893
894
895
896
897
        HelperFormElement::explodeParameter($form, F_PARAMETER);
        unset($form[F_PARAMETER]);
        if (isset($form[FE_FILL_STORE_VAR])) {
            $fillStoreVar = $form[FE_FILL_STORE_VAR];
            unset($form[FE_FILL_STORE_VAR]);
        }

898
        // Setting defaults later is too late.
899
900
901
        if (empty($form[F_DB_INDEX])) {
            $form[F_DB_INDEX] = $this->dbIndexData;
        } else {
902
            $form[F_DB_INDEX] = $this->evaluate->parse($form[F_DB_INDEX]);
903
        }
904
905
906
        if (empty($form[F_PRIMARY_KEY])) {
            $form[F_PRIMARY_KEY] = F_PRIMARY_KEY_DEFAULT;
        }
907
908

        // Some forms load/save the form data on extra defined databases.
909
910
911
        if ($this->dbIndexData != $form[F_DB_INDEX]) {
            if (!isset($this->dbArray[$form[F_DB_INDEX]])) {
                $this->dbArray[$form[F_DB_INDEX]] = new Database($form[F_DB_INDEX]);
912
            }
913
914
            $this->dbIndexData = $form[F_DB_INDEX];

915
916
            unset($this->evaluate);
            $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
917
918
        }

919
920
        // This is needed for filling templateGroup records with their default values
        // and for evaluating variables in the Form title
921
        $this->store->fillStoreWithRecord($form[F_TABLE_NAME], $recordId, $this->dbArray[$this->dbIndexData], $form[F_PRIMARY_KEY]);
922

923
        $formSpec = $this->evaluate->parseArray($form);
924

925
926
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);
        $formSpec = HelperFormElement::setLanguage($formSpec, $parameterLanguageFieldName);
927

928
929
930
931
932
933
934
        if (!empty($formSpec[F_SUBMIT_BUTTON_TEXT])) {
            // set defaults for submit button (different from save button defaults)
            $formSpec[F_SUBMIT_BUTTON_CLASS] = $formSpec[F_SAVE_BUTTON_CLASS] ?? 'btn btn-default';
            $formSpec[F_SUBMIT_BUTTON_GLYPH_ICON] = $formSpec[F_SAVE_BUTTON_GLYPH_ICON] ?? '';
            $formSpec[F_SUBMIT_BUTTON_TOOLTIP] = $formSpec[F_SAVE_BUTTON_TOOLTIP] ?? $formSpec[F_SUBMIT_BUTTON_TEXT];
        }

935
        $formSpec = $this->syncSystemFormConfig($formSpec);
936
        $formSpec = $this->initForm($formSpec, $recordId);