QuickFormQuery.php 51.9 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
43
44
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');
require_once(__DIR__ . '/BodytextParser.php');
require_once(__DIR__ . '/Delete.php');
45
require_once(__DIR__ . '/form/FormAction.php');
Carsten  Rose's avatar
Carsten Rose committed
46
require_once(__DIR__ . '/form/Dirty.php');
47
require_once(__DIR__ . '/form/DragAndDrop.php');
48

49
50
51
52
53
54
55
56
57
58
59
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
258
    /**
     * Determine the name of the language parameter field, which has to be taken to fill language specific defintions.
     */
259
    private function setParameterLanguageFieldName() {
260
261
262
263
264
265
266

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

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

275

276
    /**
277
     * Process form.
278
279
280
281
282
     * $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:
283
     *
284
     * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE
285
     *
286
     * @return array|string
287
     * @throws CodeException
288
     * @throws DbException
289
     * @throws DownloadException
290
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
291
     * @throws UserReportException
292
     */
293
    private function doForm($formMode) {
Carsten  Rose's avatar
Carsten Rose committed
294
        $data = '';
Carsten  Rose's avatar
Carsten Rose committed
295
        $foundInStore = '';
296

Carsten  Rose's avatar
Carsten Rose committed
297
        // Fill STORE_FORM
298
        if ($formMode === FORM_UPDATE || $formMode === FORM_SAVE || $formMode === FORM_DRAG_AND_DROP) {
Carsten  Rose's avatar
Carsten Rose committed
299
            $fillStoreForm = new FillStoreForm();
300
            $fillStoreForm->process($formMode);
Carsten  Rose's avatar
Carsten Rose committed
301
        }
302

303
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO);
304
        $this->setParameterLanguageFieldName();
305
306

        $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore);
307
308
309
310
311
312
313
314
315
        if ($formName === false) {
            switch ($formName) {
                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
            }
316
        }
317

318
319
        if ($formName !== false) {
            // Validate only if there is a 'real' form (not a FORM_DELETE with only a tablename).
320
            $sipFound = $this->validateForm($foundInStore, $formMode);
321
322

        } else {
323
            // FORM_DELETE without a form definition: Fake the form with only a tableName.
324
325
            $table = $this->store->getVar(SIP_TABLE, STORE_SIP);
            if ($table === false) {
326
                throw new UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE);
327
            }
328

329
330
331
            $sipFound = true;
            $this->formSpec[F_NAME] = '';
            $this->formSpec[F_TABLE_NAME] = $table;
332
333
            $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.
334
335
336
337
338
339
340
341
342
343

            $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);
                    }
                }
            }
344
345
        }

346
347
        // 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.
348
        if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) {
349
350
            $this->store->createSipAfterFormLoad($formName);
        }
351

352
353
354
355
        if ($this->store->getVar('id', STORE_BEFORE) === false) {
            $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_BEFORE);
        }

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

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

363
            // In case of a conflict, return immediately
Carsten  Rose's avatar
Carsten Rose committed
364
365
            if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
                $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
366

367
368
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
369
370
        }

371
372
        // FORM_LOAD: if there is an foreign exclusive record lock - show form in F_MODE_READONLY mode.
        if ($formMode === FORM_LOAD) {
373
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
374
375
376
377
378
379
380
            $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;
            }
        }

381
        if ($formMode === FORM_DELETE) {
382

383
            $build = new Delete($this->dbIndexData);
384
385

        } else {
386
            $tableDefinition = $this->dbArray[$this->dbIndexData]->getTableDefinition($this->formSpec[F_TABLE_NAME]);
387
            $this->store->fillStoreTableDefaultColumnType($tableDefinition);
388
389
390

            switch ($this->formSpec['render']) {
                case 'plain':
391
                    $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
392
393
                    break;
                case 'table':
394
                    $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
395
396
                    break;
                case 'bootstrap':
397
                    $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
398
399
400
401
                    break;
                default:
                    throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
            }
402
403
        }

404
        $formAction = new FormAction($this->formSpec, $this->dbArray[$this->dbIndexData], $this->phpUnit);
405
        switch ($formMode) {
406
            case FORM_LOAD:
407
408
409
410
411
412
413
                $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
414
            case FORM_UPDATE:
415
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
416
417
                // data['form-update']=....
                $data = $build->process($formMode);
418
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
419
                break;
Carsten  Rose's avatar
Carsten Rose committed
420

421
422
423
424
425
426
427
428
            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;

429
            case FORM_SAVE:
430
431
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

432
                // Action: Before
433
434
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_INSERT . ',' . FE_TYPE_BEFORE_UPDATE . ',' . FE_TYPE_BEFORE_SAVE);

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

438
                // SAVE
439
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw);
440
441
442

                $save->processAllImageCutFE();

443
444
                $rc = $save->process();

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

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

450
                // Action: After
451
452
                $status = $formAction->elements($rc, $this->feSpecAction, FE_TYPE_AFTER_INSERT . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_AFTER_SAVE);
                if ($status != ACTION_ELEMENT_NO_CHANGE) {
453
                    // Reload fresh saved record and fill STORE_RECORD with it.
454
                    $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, STORE_RECORD);
455
                }
456

Carsten  Rose's avatar
Carsten Rose committed
457
458
459
460
461
462
                // Action: Paste
                $this->pasteClipboard($this->formSpec[F_ID], $formAction);

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

463
464
465
466
467
468
469

                $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!
470
                $getJson = true;
471
472
473
474
475
                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);
476
                    $getJson = false;
477
                }
478

479
                if ($getJson) {
480

481
482
                    // 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);
483
484
485
486
                    $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);
487

488
489
                    // Retrieve FE Values as JSON
                    // $data['form-update']=...
490
                    // $data = $build->process($formMode, $htmlElementNameIdZero);
491
                    $data = $build->process($formMode, false, $this->feSpecNative);
492
                }
493

494
                break;
495
496
497
            case FORM_DRAG_AND_DROP:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);

498
                $dragAndDrop = new DragAndDrop($this->formSpec);
499
                $dragAndDrop->process($this->formSpec);
500
501
502

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

504
505
506
507
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

508
        if (is_array($data)) {
509
            // $data['element-update']=...
510
511
            $data = $this->groupElementUpdateEntries($data);
        }
Carsten  Rose's avatar
Carsten Rose committed
512

Carsten  Rose's avatar
Carsten Rose committed
513
        return $data;
514
515
    }

516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532

    /**
     * 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
533
534
     * @throws CodeException
     * @throws DbException
535
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
536
     * @throws UserReportException
537
     */
538
    private function setForwardModePage() {
539
540
541
542
543
544
545
546
547
548
549
550

        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) {

551
            case F_FORWARD_MODE_AUTO:
552
            case F_FORWARD_MODE_CLOSE:
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
            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] == '') {
572
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_AUTO;
573
574
575
576
577
578
579
580
581
582
583
584
585
                $customForward = false;
            } else {
                $customForward = true;
            }

        } else {
            $customForward = false;
        }

        return $customForward;

    }

Carsten  Rose's avatar
Carsten Rose committed
586
587
588
    /**
     * Iterate over all Clipboard source records and fire for each all FE.type=paste records.
     *
Carsten  Rose's avatar
Carsten Rose committed
589
     * @param int $formId
Carsten  Rose's avatar
Carsten Rose committed
590
     * @param FormAction $formAction
591
     *
Carsten  Rose's avatar
Carsten Rose committed
592
593
594
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
595
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
596
     */
597
    private function pasteClipboard($formId, FormAction $formAction) {
Carsten  Rose's avatar
Carsten Rose committed
598

599
600
601
602
        if (!$this->isPasteRecord()) {
            return;
        }

Carsten  Rose's avatar
Carsten Rose committed
603
604
605
606
607
608
609
        $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";
610
        $arrClipboard = $this->dbArray[$this->dbIndexQfq]->sql($sql);
Carsten  Rose's avatar
Carsten Rose committed
611
612
613
614
615
616
617
618

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

    } # doClipboard()

619
620
621
    /**
     * @return bool  true if there is at least one paste record, else false.
     */
622
    private function isPasteRecord() {
623
624
625
626
627
628

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

630
631
632
633
        return false;

    }

634
635
636
637
638
    /**
     * 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
639
     * @param int $recordId
640
     *
641
642
643
644
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
645
    private function buildNSetReloadUrl(array $formSpec, $recordId) {
646

647
        $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664

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

    }

665
    /**
666
     * Load form. Evaluates form. Load FormElements.
667
     *
Carsten  Rose's avatar
Carsten Rose committed
668
     * After processing:
669
670
671
672
     * 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
673
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
674
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
675
     * @param string $foundInStore
676
     *
Carsten  Rose's avatar
Carsten Rose committed
677
678
     * @return bool|string if found the formName, else 'false'.
     * @throws CodeException
679
     * @throws DbException
680
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
681
     * @throws UserReportException
682
     */
683
    private function loadFormSpecification($mode, $recordId, &$foundInStore = '') {
Carsten  Rose's avatar
Carsten Rose committed
684

685
        // formName
Carsten  Rose's avatar
Carsten Rose committed
686
        if (false === ($formName = $this->getFormName($mode, $foundInStore))) {
687
688
            return false;
        }
689

690
691
        if (!$this->dbArray[$this->dbIndexQfq]->existTable(TABLE_NAME_FORM)) {
            throw new UserFormException("Table '" . TABLE_NAME_FORM . "' not found", ERROR_MISSING_TABLE);
692
693
        }

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

697
698
699
700
701
702
703
        // 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);
        }

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

709
        $form = $this->modeCleanFormConfig($mode, $form);
710

Carsten  Rose's avatar
Carsten Rose committed
711
712
713
714
        // Save specific elements to be expanded later.
        $parseLater = OnArray::getArrayItems($form, [F_FORWARD_PAGE]);
        $form[F_FORWARD_PAGE] = '';

715
716
717
718
719
720
721
        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]);
        }

722
        // Setting defaults later is too late.
723
724
725
726
        if (empty($form[F_DB_INDEX])) {
            $form[F_DB_INDEX] = $this->dbIndexData;
        } else {
            $form[F_DB_INDEX] = $this->eval->parse($form[F_DB_INDEX]);
727
728
729
        }

        // Some forms load/save the form data on extra defined databases.
730
731
732
        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]);
733
            }
734
735
736
737
            $this->dbIndexData = $form[F_DB_INDEX];

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

740
741
742
743
744
745
        // 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);

746
747
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);
        $formSpec = HelperFormElement::setLanguage($formSpec, $parameterLanguageFieldName);
748

749
750
        $formSpec = $this->syncSystemFormConfig($formSpec);
        $formSpec = $this->initForm($formSpec);
751

Carsten  Rose's avatar
Carsten Rose committed
752
753
        $formSpec = array_merge($formSpec, $parseLater);

754
        // Set F_FINAL_DELETE_FORM
755
        $formSpec[F_FINAL_DELETE_FORM] = ($formSpec[F_EXTRA_DELETE_FORM] != '') ? $formSpec[F_EXTRA_DELETE_FORM] : $formSpec[F_NAME];
756

757
758
759
760
        // 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)) {
761
                $this->store->appendToStore($rows[0], STORE_VAR);
762
763
764
765
766
767
768
769
            } 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;
770

Carsten  Rose's avatar
Carsten Rose committed
771
        // Clear
772
773
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

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

777
        // FE: Action
778
        $this->feSpecAction = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, ['no', $this->formSpec["id"], 'action']);
779
        HelperFormElement::explodeParameterInArrayElements($this->feSpecAction, FE_PARAMETER);
780
781

        // FE: Native & Container
782
        // "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";
783
        $feSpecNative = array();
784
785
        switch ($mode) {
            case FORM_LOAD:
786
                // Select all Native elements (native, pill, fieldset, templateGroup) which are NOT nested = Root level.
787
                $feSpecNative = $this->dbArray[$this->dbIndexQfq]->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER, ['no', $this->formSpec["id"], 'native,container', 0], $this->formSpec);
788
789
790
                break;

            case FORM_SAVE:
Carsten  Rose's avatar
Carsten Rose committed
791
            case FORM_UPDATE:
792
                $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec[F_ID]], $this->formSpec);
793
794
                break;

795
796
797
798
            case FORM_DELETE:
                $this->feSpecNative = array();
                break;

799
            default:
800
                break;
801
802
        }

803
        $this->feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName);
804
        $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($this->feSpecNative, $this->formSpec[F_ID], $recordId);
805

806
        return $formName;
Carsten  Rose's avatar
Carsten Rose committed
807
808
    }

809
810
    /**
     * Depending on $sql reads FormElements to a specific container or all. Preprocess all FormElements.
811
812
813
     * This code is dirty: the nearly same function exists in class 'Database' - the difference is only
     * 'explodeTemplateGroupElements()'.
     *
Carsten  Rose's avatar
Carsten Rose committed
814
815
816
     * @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
817
818
     *
     * @return array|int
819
820
821
822
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
823
     */
824
    public function getNativeFormElements($sql, array $param, $formSpec) {
825

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

828
829
        $feSpecNative = HelperFormElement::formElementSetDefault($feSpecNative);

830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
        // 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;
    }

    /**
846
847
848
     * 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.
849
     *
850
851
852
     * 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).
853
854
855
856
     *
     * Attention: The resulting order of the FormElements, is not the same as on the Form during FormLoad!
     *
     * @param array $elements
857
     *
858
     * @return array
859
860
861
862
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
863
     */
864
    private function explodeTemplateGroupElements(array $elements) {
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
        $new = array();

        // No FormElements or no NAME_TG_COPIES column: nothing to do, return.
        if ($elements == array() || count($elements) == 0 || !isset($elements[0][NAME_TG_COPIES])) {
            return $elements;
        }

        // Iterate over all
        foreach ($elements as $row) {
            if (isset($row[NAME_TG_COPIES]) && $row[NAME_TG_COPIES] > 0) {
                $row[FE_VALUE] = $this->eval->parse($row[FE_VALUE]);
                for ($ii = 1; $ii <= $row[NAME_TG_COPIES]; $ii++) {
                    $tmpRow = $row;
                    if (is_array($row[FE_VALUE])) {
                        $tmpRow[FE_VALUE] = ($ii <= count($row[FE_VALUE])) ? current($row[FE_VALUE][$ii - 1]) : '';
                    }
                    unset($tmpRow[NAME_TG_COPIES]);
                    $tmpRow[FE_NAME] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $tmpRow[FE_NAME]);
                    $tmpRow[FE_LABEL] = str_replace(FE_TEMPLATE_GROUP_NAME_PATTERN, $ii, $tmpRow[FE_LABEL]);
884
                    $tmpRow[FE_TG_INDEX] = $ii;
885
886
887
888
889
890
                    $new[] = $tmpRow;
                }
            } else {
                $new[] = $row;
            }
        }
891

892
893
894
895
        return $new;
    }


Carsten  Rose's avatar
Carsten Rose committed
896
    /**
897
898
     * Get the formName from STORE_TYPO3 (bodytext), STORE_SIP or by STORE_CLIENT (URL).
     *
899
900
901
902
     * FORM_LOAD:
     *   Specified in T3 body text with form=<formname>            Returned Store:Typo3
     *   Specified in T3 body text with form={{form}} ':FSRD'      Returned Store:SIP
     *   Specified in T3 body text with form={{form:C:ALNUMX}}     Returned Store:Client
903
904
905
     *   Specified in T3 body text with form={{SELECT registrationFormName FROM Conference WHERE id={{conferenceId:S0}}
     *   }} Specified in T3 body text with form={{SELECT registrationFormName FROM Conference WHERE
     *   id={{conferenceId:C0:DIGIT}} }} Specified in SIP
906
907
908
909
910
     *
     * FORM_SAVE:
     *   Specified in SIP
     *
     *
Carsten  Rose's avatar
Carsten Rose committed
911
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
912
     * @param string $foundInStore
913
     *
914
     * @return bool|string  Formname (Form.name) or FALSE (if no formname found)
Carsten  Rose's avatar
Carsten Rose committed
915
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
916
     * @throws DbException
917
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
918
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
919
     */
920
    public function getFormName($mode, &$foundInStore = '') {
921
        $dummy = array();
Carsten  Rose's avatar
Carsten Rose committed
922

Carsten  Rose's avatar
Carsten Rose committed
923
924
925
926