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

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

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

77
78
79
80
    /**
     * @var Evaluate instantiated class
     */
    protected $eval = 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
89
    /**
     * @var array
     */
    private $t3data = array(); // FormEelement 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 Session
     */
    private $session = null;

101
102
103
    private $dbIndexData = false;
    private $dbIndexQfq = false;

104
105
106
107
108
109
110
111
112
113
114
    /*
     * 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'
     */
115

116
117
118
    /**
     * Construct the Form Class and Store too. This is the base initialization moment.
     *
119
120
     * 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.
121
     *
122
     * @param array $t3data
Carsten  Rose's avatar
Carsten Rose committed
123
     * @param bool $phpUnit
124
     *
125
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
126
     * @throws DbException
127
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
128
     * @throws UserReportException
129
     */
130
    public function __construct(array $t3data = array(), $phpUnit = false) {
131

132
133
        $this->phpUnit = $phpUnit;

134
        mb_internal_encoding("UTF-8");
135

136
137
        $this->session = Session::getInstance($phpUnit);

138
        // Refresh the session even if no new data saved.
139
        Session::set('LAST_ACTIVITY', time());
140

141
        set_error_handler("\\qfq\\ErrorHandler::exception_error_handler");
142

143
144
145
146
147
148
149
        if (!isset($t3data[T3DATA_BODYTEXT])) {
            $t3data[T3DATA_BODYTEXT] = '';
        }

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

151
        $btp = new BodytextParser();
152
        $t3data[T3DATA_BODYTEXT] = $btp->process($t3data[T3DATA_BODYTEXT]);
153

154
155
        $this->t3data = $t3data;

156
        $bodytext = $this->t3data[T3DATA_BODYTEXT];
157
158

        $this->store = Store::getInstance($bodytext, $phpUnit);
159
        $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data[T3DATA_UID], STORE_TYPO3);
160

161
162
        $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM);
        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);
163

164
        $this->dbArray[$this->dbIndexData] = new Database($this->dbIndexData);
165
166
167
168

        $dbName = $this->dbArray[$this->dbIndexData]->getDbName();
        $this->store->setVar(SYSTEM_DB_NAME_DATA, $dbName, STORE_SYSTEM);

169
170
        if ($this->dbIndexData != $this->dbIndexQfq) {
            $this->dbArray[$this->dbIndexQfq] = new Database($this->dbIndexQfq);
171
172

            $dbName = $this->dbArray[$this->dbIndexQfq]->getDbName();
173
        }
174
175
        $this->store->setVar(SYSTEM_DB_NAME_QFQ, $dbName, STORE_SYSTEM);

176

177
        $this->eval = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
178

179
        $dbUpdate = $this->store->getVar(SYSTEM_DB_UPDATE, STORE_SYSTEM);
180
        $updateDb = new DatabaseUpdate($this->dbArray[$this->dbIndexQfq]);
181
        $updateDb->checkNupdate($dbUpdate);
182

183
        $this->store->StoreSystemUpdate(); // Do this after the DB-update
184
185
186
187
188
189

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

192
    /**
193
     * Returns the defined forwardMode and set forwardPage
194
     *
195
     * @return array
196
197
     * @throws CodeException
     * @throws UserFormException
198
     */
199
    public function getForwardMode() {
200

201
        $forwardPage = $this->formSpec[F_FORWARD_PAGE];
202

Carsten  Rose's avatar
Carsten Rose committed
203
204
205
206
        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.
207
            // 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
208
209
210
            $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL_SKIP_HISTORY;
        }

211
212
        return ([
            API_REDIRECT => $this->formSpec[F_FORWARD_MODE],
Carsten  Rose's avatar
Carsten Rose committed
213
            API_REDIRECT_URL => $forwardPage,
214
        ]);
215
216
    }

217
    /**
Carsten  Rose's avatar
Carsten Rose committed
218
     * Main entrypoint for display content: a) form and/or b) report
219
     *
220
     * @return string
221
222
     * @throws CodeException
     * @throws DbException
223
     * @throws DownloadException
224
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
225
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
226
     */
227
    public function process() {
228
        $html = '';
229

230
        if ($this->store->getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === 'yes') {
231
232
            $htmlId = HelperFormElement::buildFormElementId($this->formSpec[F_ID], 0, 0, 0);
            $html .= Support::doTooltip($htmlId . HTML_ID_EXTENSION_TOOLTIP, $this->t3data['bodytext']);
233
234
235
        }

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

238
        // 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
239
240
241
242
        if ($this->store->getVar(SYSTEM_DOWNLOAD_POPUP, STORE_SYSTEM) == DOWNLOAD_POPUP_REQUEST) {
            $html .= $this->getModalCode();
        }

243
244
245
246
247
        // Only needed if there are 'drag and drop' elements.
        if ($this->store->getVar(SYSTEM_DRAG_AND_DROP_JS, STORE_SYSTEM) == 'true') {
            $html .= $this->getDragAndDropCode();
        }

248
        $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
249
        if ($class) {
250
            $html = Support::wrapTag("<div class='$class'>", $html);
Carsten  Rose's avatar
Carsten Rose committed
251
252
        }

253
        return $html;
254
255
    }

256
257
    /**
     * Determine the name of the language parameter field, which has to be taken to fill language specific defintions.
258
259
260
     *
     * @throws CodeException
     * @throws UserFormException
261
     */
262
    private function setParameterLanguageFieldName() {
263
264
265
266
267
268
269

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

        foreach (['A', 'B', 'C', 'D'] as $key) {
270
            $languageIdx = SYSTEM_FORM_LANGUAGE . "$key" . "Id";
271
272
273
274
275
276
277
            if ($this->store->getVar($languageIdx, STORE_SYSTEM) == $typo3PageLanguage) {
                $this->store->setVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, 'parameterLanguage' . $key, STORE_SYSTEM);
                break;
            }
        }
    }

278
    /**
279
280
     * Creates an empty file. This indicates that the current form is in debug mode. Returns HTML element which will be
     * replaced by the logfile.
281
282
283
284
285
286
287
288
289
290
291
     *
     * @param $formName
     * @param $formLogMode
     * @return
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
    private function getFormLog($formName, $formLogMode) {

        $formLogFileName = Support::getFormLogFileName($formName, $formLogMode);
292
        file_put_contents($formLogFileName, '');
293

294
295
        $monitor = new Monitor();

296
297
        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]);
298
    }
299

300
    /**
301
     * Process form.
302
303
304
305
306
     * $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:
307
     *
308
     * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE
309
     *
310
     * @return array|string
311
     * @throws CodeException
312
     * @throws DbException
313
     * @throws DownloadException
314
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
315
     * @throws UserReportException
316
     */
317
    private function doForm($formMode) {
Carsten  Rose's avatar
Carsten Rose committed
318
        $data = '';
Carsten  Rose's avatar
Carsten Rose committed
319
        $foundInStore = '';
320

Carsten  Rose's avatar
Carsten Rose committed
321
        // Fill STORE_FORM
322
        if ($formMode === FORM_UPDATE || $formMode === FORM_SAVE || $formMode === FORM_DRAG_AND_DROP) {
Carsten  Rose's avatar
Carsten Rose committed
323
            $fillStoreForm = new FillStoreForm();
324
            $fillStoreForm->process($formMode);
Carsten  Rose's avatar
Carsten Rose committed
325
        }
326

327
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO);
328
        $this->setParameterLanguageFieldName();
329

330
331
332
        $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore, $formLogMode);
        if ($formName !== false && $formLogMode !== false) {
            return $this->getFormLog($formName, $formLogMode);
333
334
        }

335
        if ($formName === false) {
336
            switch ($formMode) {
337
338
339
340
341
342
343
                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
            }
344
        }
345

346
347
        if ($formName !== false) {
            // Validate only if there is a 'real' form (not a FORM_DELETE with only a tablename).
348
            $sipFound = $this->validateForm($foundInStore, $formMode);
349
350

        } else {
351
            // FORM_DELETE without a form definition: Fake the form with only a tableName.
352
353
            $table = $this->store->getVar(SIP_TABLE, STORE_SIP);
            if ($table === false) {
354
                throw new UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE);
355
            }
356

357
358
359
            $sipFound = true;
            $this->formSpec[F_NAME] = '';
            $this->formSpec[F_TABLE_NAME] = $table;
360
361
            $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.
362
363
364
365
366
367
368
369
370
371

            $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);
                    }
                }
            }
372
373
        }

374
375
        // 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.
376
        if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) {
377
378
            $this->store->createSipAfterFormLoad($formName);
        }
379

380
381
382
383
        if ($this->store->getVar('id', STORE_BEFORE) === false) {
            $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_BEFORE);
        }

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

388
            $answer = $dirty->checkDirtyAndRelease($formMode, $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS],
Carsten  Rose's avatar
Carsten Rose committed
389
                $this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $recordId, true);
390

391
            // In case of a conflict, return immediately
Carsten  Rose's avatar
Carsten Rose committed
392
393
            if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
                $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
394

395
396
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
397
398
        }

399
400
        // FORM_LOAD: if there is an foreign exclusive record lock - show form in F_MODE_READONLY mode.
        if ($formMode === FORM_LOAD) {
401
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
402
403
404
405
406
407
408
            $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;
            }
        }

409
        if ($formMode === FORM_DELETE) {
410

411
            $build = new Delete($this->dbIndexData);
412
413

        } else {
414
            $tableDefinition = $this->dbArray[$this->dbIndexData]->getTableDefinition($this->formSpec[F_TABLE_NAME]);
415
            $this->store->fillStoreTableDefaultColumnType($tableDefinition);
416
417
418

            switch ($this->formSpec['render']) {
                case 'plain':
419
                    $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
420
421
                    break;
                case 'table':
422
                    $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
423
424
                    break;
                case 'bootstrap':
425
                    $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
426
427
428
429
                    break;
                default:
                    throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
            }
430
431
        }

432
        $formAction = new FormAction($this->formSpec, $this->dbArray[$this->dbIndexData], $this->phpUnit);
433
        switch ($formMode) {
434
            case FORM_LOAD:
435
436
437
438
439
440
441
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
                $data = $build->process($formMode);
                $data = Support::wrapTag("<div class='" . 'col-md-' . $this->formSpec[F_BS_COLUMNS] . "'>", $data);
                $data = Support::wrapTag("<div class='row'>", $data);
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
                break;

Carsten  Rose's avatar
Carsten Rose committed
442
            case FORM_UPDATE:
443
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
444
445
                // data['form-update']=....
                $data = $build->process($formMode);
446
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
447
                break;
Carsten  Rose's avatar
Carsten Rose committed
448

449
450
451
452
453
454
455
456
            case FORM_DELETE:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_DELETE);

                $build->process($this->formSpec[F_TABLE_NAME], $recordId);

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

457
            case FORM_SAVE:
458
459
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

460
                // Action: Before
461
462
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_INSERT . ',' . FE_TYPE_BEFORE_UPDATE . ',' . FE_TYPE_BEFORE_SAVE);

463
                // If an old record exist: load it. Necessary to delete uploaded files which should be overwritten.
464
                $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_RECORD);
465

466
                // SAVE
467
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw);
468
469

                $save->processAllImageCutFE();
470
                $save->checkRequiredHidden();
471

472
473
                $rc = $save->process();

474
                // Reload fresh saved record and fill STORE_RECORD with it.
475
                $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, STORE_RECORD);
476

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

479
                // Action: After
480
481
                $status = $formAction->elements($rc, $this->feSpecAction, FE_TYPE_AFTER_INSERT . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_AFTER_SAVE);
                if ($status != ACTION_ELEMENT_NO_CHANGE) {
482
                    // Reload fresh saved record and fill STORE_RECORD with it.
483
                    $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, STORE_RECORD);
484
                }
485

Carsten  Rose's avatar
Carsten Rose committed
486
487
488
489
490
491
                // Action: Paste
                $this->pasteClipboard($this->formSpec[F_ID], $formAction);

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

492
493
494
495
496
497
498

                $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!
499
                $getJson = true;
500
501
502
503
504
                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);
505
                    $getJson = false;
506
                }
507

508
                if ($getJson) {
509

510
511
                    // 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);
512
513
514
515
                    $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);
516

517
518
                    // Retrieve FE Values as JSON
                    // $data['form-update']=...
519
                    // $data = $build->process($formMode, $htmlElementNameIdZero);
520
                    $data = $build->process($formMode, false, $this->feSpecNative);
521
                }
522
                break;
523

524
525
526
            case FORM_DRAG_AND_DROP:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);

527
                $dragAndDrop = new DragAndDrop($this->formSpec);
528
                $dragAndDrop->process();
529
530
531

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

533
534
535
536
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

537
        if (is_array($data)) {
538
            // $data['element-update']=...
539
540
            $data = $this->groupElementUpdateEntries($data);
        }
Carsten  Rose's avatar
Carsten Rose committed
541

Carsten  Rose's avatar
Carsten Rose committed
542
        return $data;
543
544
    }

545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561

    /**
     * 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
562
563
     * @throws CodeException
     * @throws DbException
564
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
565
     * @throws UserReportException
566
     */
567
    private function setForwardModePage() {
568
569
570
571
572
573
574
575
576
577
578
579

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

        $forwardPageTmp = $this->eval->parse($this->formSpec[F_FORWARD_PAGE]);

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

580
            case F_FORWARD_MODE_AUTO:
581
            case F_FORWARD_MODE_CLOSE:
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
            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] == '') {
601
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_AUTO;
602
603
604
605
606
607
608
609
610
611
612
613
614
                $customForward = false;
            } else {
                $customForward = true;
            }

        } else {
            $customForward = false;
        }

        return $customForward;

    }

Carsten  Rose's avatar
Carsten Rose committed
615
616
617
    /**
     * Iterate over all Clipboard source records and fire for each all FE.type=paste records.
     *
Carsten  Rose's avatar
Carsten Rose committed
618
     * @param int $formId
Carsten  Rose's avatar
Carsten Rose committed
619
     * @param FormAction $formAction
620
     *
Carsten  Rose's avatar
Carsten Rose committed
621
622
623
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
624
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
625
     */
626
    private function pasteClipboard($formId, FormAction $formAction) {
Carsten  Rose's avatar
Carsten Rose committed
627

628
629
630
631
        if (!$this->isPasteRecord()) {
            return;
        }

Carsten  Rose's avatar
Carsten Rose committed
632
633
634
635
636
637
638
        $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";
639
        $arrClipboard = $this->dbArray[$this->dbIndexQfq]->sql($sql);
Carsten  Rose's avatar
Carsten Rose committed
640
641
642
643
644
645
646
647

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

    } # doClipboard()

648
649
650
    /**
     * @return bool  true if there is at least one paste record, else false.
     */
651
    private function isPasteRecord() {
652
653
654
655
656
657

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

659
660
661
662
        return false;

    }

663
664
665
666
667
    /**
     * 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
668
     * @param int $recordId
669
     *
670
671
672
673
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
674
    private function buildNSetReloadUrl(array $formSpec, $recordId) {
675

676
        $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693

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

    }

694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
    /**
     * 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);
            if (false !== ($arr = stat($file))) {

                if (time() - $arr['mtime'] > FORM_LOG_FILE_EXPIRE) {
                    unlink($file);
                } else {
                    $form[FORM_LOG_FILE . '_' . $mode] = $file;
                }
            }
        }

        return $form;
    }

725
    /**
726
727
     * Get form name
     * Check if the form is in log mode: set formLog and return
728
     * Load form. Evaluates form. Load FormElements.
729
     *
Carsten  Rose's avatar
Carsten Rose committed
730
     * After processing:
731
732
733
734
     * 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
735
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
736
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
737
     * @param string $foundInStore
738
     * @param string $formLogMode
Carsten  Rose's avatar
Carsten Rose committed
739
     * @return bool|string if found the formName, else 'false'.
740
     *
Carsten  Rose's avatar
Carsten Rose committed
741
     * @throws CodeException
742
     * @throws DbException
743
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
744
     * @throws UserReportException
745
     */
746
747
748
    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
749

750
        // formName
Carsten  Rose's avatar
Carsten Rose committed
751
        if (false === ($formName = $this->getFormName($mode, $foundInStore))) {
752
753
            return false;
        }
754

755
756
757
758
        // 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.
759
760
        }

761
762
        if (!$this->dbArray[$this->dbIndexQfq]->existTable(TABLE_NAME_FORM)) {
            throw new UserFormException("Table '" . TABLE_NAME_FORM . "' not found", ERROR_MISSING_TABLE);
763
764
        }

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

768
769
770
771
772
773
774
        // 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)) {
            $rTmp = $this->eval->parse($rTmp);
            $this->store->setVar(CLIENT_RECORD_ID, $rTmp, STORE_TYPO3);
        }

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

780
        $form = $this->checkFormLogMode($form);
781
        $form = $this->modeCleanFormConfig($mode, $form);
782

Carsten  Rose's avatar
Carsten Rose committed
783
784
785
786
        // Save specific elements to be expanded later.
        $parseLater = OnArray::getArrayItems($form, [F_FORWARD_PAGE]);
        $form[F_FORWARD_PAGE] = '';

787
788
789
790
791
792
793
        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]);
        }

794
        // Setting defaults later is too late.
795
796
797
798
        if (empty($form[F_DB_INDEX])) {
            $form[F_DB_INDEX] = $this->dbIndexData;
        } else {
            $form[F_DB_INDEX] = $this->eval->parse($form[F_DB_INDEX]);
799
800
801
        }

        // Some forms load/save the form data on extra defined databases.
802
803
804
        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]);
805
            }
806
807
808
809
            $this->dbIndexData = $form[F_DB_INDEX];

            unset($this->eval);
            $this->eval = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
810
811
        }

812
813
814
815
816
817
        // This is needed for filling templateGroup records with their default values
        // and for evaluating variables in the Form title
        $this->fillStoreWithRecord($form[F_TABLE_NAME], $recordId, STORE_RECORD);

        $formSpec = $this->eval->parseArray($form);

818
819
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);
        $formSpec = HelperFormElement::setLanguage($formSpec, $parameterLanguageFieldName);
820

821
        $formSpec = $this->syncSystemFormConfig($formSpec);
822
        $formSpec = $this->initForm($formSpec, $recordId);
823

Carsten  Rose's avatar
Carsten Rose committed
824
825
        $formSpec = array_merge($formSpec, $parseLater);

826
        // Set F_FINAL_DELETE_FORM
827
        $formSpec[F_FINAL_DELETE_FORM] = ($formSpec[F_EXTRA_DELETE_FORM] != '') ? $formSpec[F_EXTRA_DELETE_FORM] : $formSpec[F_NAME];
828

829
830
831
832
        // Fire FE_FILL_STORE_VAR after the primary form record has been loaded
        if (!empty($fillStoreVar)) {
            $rows = $this->eval->parse($fillStoreVar);
            if (is_array($rows)) {
833
                $this->store->appendToStore($rows[0], STORE_VAR);
834
835
836
837
838
839
840
841
            } else {
                if (!empty($rows)) {
                    throw new UserFormException("Invalid statement for '" . FE_FILL_STORE_VAR . "': " . $formSpec[FE_FILL_STORE_VAR], ERROR_INVALID_OR_MISSING_PARAMETER);
                }
            }
        }

        $this->formSpec = $formSpec;
842

Carsten  Rose's avatar
Carsten Rose committed
843
        // Clear
844
845
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

846
847
848
        // Read all 'active' FE
        $this->feSpecNativeRaw = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_RAW, ROW_REGULAR, [$this->formSpec["id"]]);

849
        // FE: Action
850
        $this->feSpecAction = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, ['no', $this->formSpec["id"], 'action']);
851
        HelperFormElement::explodeParameterInArrayElements($this->feSpecAction, FE_PARAMETER);
852
853

        // FE: Native & Container
854
        // "SELECT *, ? AS 'nestedInFieldSet' FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND FIND_IN_SET(fe.class, ? ) AND fe.feIdContainer = ? AND fe.enabled='yes' ORDER BY fe.ord, fe.id";
855
        $feSpecNative = array();
856
857
        switch ($mode) {
            case FORM_LOAD:
858
                // Select all Native elements (native, pill, fieldset, templateGroup) which are NOT nested = Root level.
859
                $feSpecNative = $this->dbArray[$this->dbIndexQfq]->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER, ['no', $this->formSpec["id"], 'native,container', 0], $this->formSpec);
860
861
862
                break;

            case FORM_SAVE:
Carsten  Rose's avatar
Carsten Rose committed
863
            case FORM_UPDATE:
864
                $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec[F_ID]], $this->formSpec);
865
866
                break;

867
868
869
870
            case FORM_DELETE:
                $this->feSpecNative = array();
                break;

871
            default:
872
                break;
873
874
        }

875
        $this->feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName);
876
        $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($this->feSpecNative, $this->formSpec[F_ID], $recordId);
877

878
        return $formName;
Carsten  Rose's avatar
Carsten Rose committed
879
880
    }

881
882
    /**
     * Depending on $sql reads FormElements to a specific container or all. Preprocess all FormElements.
883
884
885
     * This code is dirty: the nearly same function exists in class 'Database' - the difference is only
     * 'explodeTemplateGroupElements()'.
     *
Carsten  Rose's avatar
Carsten Rose committed
886
887
888
     * @param string $sql SQL_FORM_ELEMENT_SPECIFIC_CONTAINER | SQL_FORM_ELEMENT_ALL_CONTAINER
     * @param array $param Parameter which matches the prepared statement in $sql
     * @param array $formSpec Main FormSpec to copy generic parameter to FormElements
889
890
     *
     * @return array|int
891
892
893
894
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
895
     */
896
    public function getNativeFormElements($sql, array $param, $formSpec) {
897

898
        $feSpecNative = $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_REGULAR, $param);
899

900
901
        $feSpecNative = HelperFormElement::formElementSetDefault($feSpecNative);

902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
        // Explode and Do $FormElement.parameter
        HelperFormElement::explodeParameterInArrayElements($feSpecNative, FE_PARAMETER);

        // Check for retype FormElements which have to duplicated.
        $feSpecNative = HelperFormElement::duplicateRetypeElements($feSpecNative);

        // Check for templateGroup Elements to explode them
        $feSpecNative = $this->explodeTemplateGroupElements($feSpecNative);

        // Copy Attributes to FormElements
        $feSpecNative = HelperFormElement::copyAttributesToFormElements($formSpec, $feSpecNative);

        return $feSpecNative;
    }

    /**
918
919
920
     * Iterate over all FormElements in $elements. If a row has a column NAME_TG_COPIES, copy those elements
     * NAME_TG_COPIES-times. Adjust FE_TEMPLATE_GROUP_NAME_PATTERN (='%d') with current count on column FE_NAME and
     * FE_LABEL.
921
     *
922
923
924
     * This code is dirty: only to get JSON value, we have to initialize the STORE_RECORD (done earlier) to be capable
     * to parse fe[FE_VALUE], which probably contains as string like '{{!SELECT value FROM table WHERE xId={{id}} ORDER
     * BY id}}' - the {{id}} needs to be replaced by the current recordId (primary record).
925
926
927
928
     *
     * Attention: The resulting order of the FormElements, is not the same as on the Form during FormLoad!
     *
     * @param array $elements
929
     *
Carsten  Rose's avatar