QuickFormQuery.php 50.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

48
49
50
51
52
53
54
55
56
57
58
59
/*
 * 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
60
/**
61
 * Class Qfq
Carsten  Rose's avatar
Carsten Rose committed
62
63
 * @package qfq
 */
64
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
131
    public function __construct(array $t3data = array(), $phpUnit = false)
    {
132

133
134
        $this->phpUnit = $phpUnit;

135
        mb_internal_encoding("UTF-8");
136

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

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

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

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

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

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

155
156
        $this->t3data = $t3data;

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

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

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

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

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

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

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

177

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

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

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

        // 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
191
192
    }

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

203
        $forwardPage = $this->formSpec[F_FORWARD_PAGE];
204

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

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

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

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

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

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

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

250
        return $html;
251
252
    }

253
254
255
    /**
     * Determine the name of the language parameter field, which has to be taken to fill language specific defintions.
     */
256
257
    private function setParameterLanguageFieldName()
    {
258
259
260
261
262
263
264

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

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

273

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

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

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

        $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore);
305
306
307
308
309
310
311
312
313
        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
            }
314
        }
315

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

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

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

            $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);
                    }
                }
            }
342
343
        }

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

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

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

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

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

365
366
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
367
368
        }

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

379
        if ($formMode === FORM_DELETE) {
380

381
            $build = new Delete($this->dbIndexData);
382
383

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

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

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

419
420
421
422
423
424
425
426
            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;

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

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

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

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

                $save->processAllImageCutFE();

441
442
                $rc = $save->process();

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

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

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

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

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

461
462
463
464
465
466
467

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

477
                if ($getJson) {
478

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

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

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

                $draganddrop = new DragAndDrop();
                $draganddrop->process($this->formSpec[F_TABLE_NAME], $this->formSpec[F_DRAG_AND_DROP_ORDER_SQL]);

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

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

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

Carsten  Rose's avatar
Carsten Rose committed
511
        return $data;
512
513
    }

514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530

    /**
     * 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
531
532
     * @throws CodeException
     * @throws DbException
533
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
534
     * @throws UserReportException
535
     */
536
537
    private function setForwardModePage()
    {
538
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

        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
584
585
586
    /**
     * Iterate over all Clipboard source records and fire for each all FE.type=paste records.
     *
Carsten  Rose's avatar
Carsten Rose committed
587
     * @param int $formId
Carsten  Rose's avatar
Carsten Rose committed
588
     * @param FormAction $formAction
589
     *
Carsten  Rose's avatar
Carsten Rose committed
590
591
592
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
593
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
594
     */
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
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
646
    private function buildNSetReloadUrl(array $formSpec, $recordId)
    {
647

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

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

    }

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

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

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

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

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

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

711
        $form = $this->modeCleanFormConfig($mode, $form);
712

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

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

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

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

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

742
743
744
745
746
747
        // 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);

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

751
752
        $formSpec = $this->syncSystemFormConfig($formSpec);
        $formSpec = $this->initForm($formSpec);
753

Carsten  Rose's avatar
Carsten Rose committed
754
755
        $formSpec = array_merge($formSpec, $parseLater);

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

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

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

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

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

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

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

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

801
            default:
802
                break;
803
804
        }

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

808
        return $formName;
Carsten  Rose's avatar
Carsten Rose committed
809
810
    }

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

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

831
832
        $feSpecNative = HelperFormElement::formElementSetDefault($feSpecNative);

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

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

896
897
898
899
        return $new;
    }


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

Carsten  Rose's avatar
Carsten Rose committed
928
929
930
931
932
933
        switch ($mode) {
            case FORM_LOAD:
                $store = STORE_TYPO3;
                break;
            case FORM_SAVE:
            case FORM_UPDATE:
934
935
            case FORM_DELETE:
                $store = STORE_SIP;
Carsten  Rose's avatar
Carsten Rose committed
936
937
938
939
940
                break;
            default:
                throw new CodeException("Unknown mode: $mode.", ERROR_UNKNOWN_MODE);
        }

941
942
        $storeFormName = $this->store->getVar(SIP_FORM, $store, '', $foundInStore);
        $formName = $this->eval->parse($storeFormName, 0, $dummy, $foundInStore);
Carsten  Rose's avatar
Carsten Rose committed
943

944
945
        // If the formname is empty or if 'form' has not been found in any store: no form.
        if ($formName === '' || $foundInStore === '') {
946
947
            return false;
        }
948

949
        return $formName;