QuickFormQuery.php 51.8 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
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584

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

            case F_FORWARD_MODE_CLIENT:
            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] == '') {
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_CLIENT;
                $customForward = false;
            } else {
                $customForward = true;
            }

        } else {
            $customForward = false;
        }

        return $customForward;

    }

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

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

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

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

    } # doClipboard()

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

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

629
630
631
632
        return false;

    }

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

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

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

    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

798
            default:
799
                break;
800
801
        }

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

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

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

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

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

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

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

891
892
893
894
        return $new;
    }


Carsten  Rose's avatar
Carsten Rose committed
895
    /**
896
897
     * Get the formName from STORE_TYPO3 (bodytext), STORE_SIP or by STORE_CLIENT (URL).
     *
898
899
900
901
     * 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
902
903
904
     *   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
905
906
907
908
909
     *
     * FORM_SAVE:
     *   Specified in SIP
     *
     *
Carsten  Rose's avatar
Carsten Rose committed
910
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
911
     * @param string $foundInStore
912
     *
913
     * @return bool|string  Formname (Form.name) or FALSE (if no formname found)
Carsten  Rose's avatar
Carsten Rose committed
914
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
915
     * @throws DbException
916
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
917
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
918
     */
919
    public function getFormName($mode, &$foundInStore = '') {
920
        $dummy = array();
Carsten  Rose's avatar
Carsten Rose committed
921

Carsten  Rose's avatar
Carsten Rose committed
922
923
924
925
926
927
        switch ($mode) {
            case FORM_LOAD:
                $store = STORE_TYPO3;
                break;
            case FORM_SAVE:
            case FORM_UPDATE:
928
            case FORM_DELETE:
929
            case FORM_DRAG_AND_DROP:
930
                $store = STORE_SIP;
Carsten  Rose's avatar
Carsten Rose committed