QuickFormQuery.php 49.7 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
<?php
/**
 * Created by PhpStorm.
 * User: ep
 * Date: 12/23/15
 * Time: 6:33 PM
 */

namespace qfq;

Carsten  Rose's avatar
Carsten Rose committed
11
use qfq;
12
13
14
15
16
17
18
19
20

//use qfq\Report;

//use qfq\BuildFormPlain;
//use qfq\BuildFormTable;
//use qfq\BuildFormBootstrap;
//use qfq\UserException;
//use qfq\CodeException;
//use qfq\DbException;
21
//use qfq\helper;
22
//use qfq\Store;
Carsten  Rose's avatar
Carsten Rose committed
23

Carsten  Rose's avatar
Carsten Rose committed
24

25
26
27
28
29
30
31
32
33
34
35
require_once(__DIR__ . '/store/Store.php');
require_once(__DIR__ . '/store/FillStoreForm.php');
require_once(__DIR__ . '/store/Session.php');
require_once(__DIR__ . '/Constants.php');
require_once(__DIR__ . '/Save.php');
require_once(__DIR__ . '/helper/KeyValueStringParser.php');
require_once(__DIR__ . '/helper/HelperFormElement.php');
require_once(__DIR__ . '/exceptions/UserFormException.php');
require_once(__DIR__ . '/exceptions/CodeException.php');
require_once(__DIR__ . '/exceptions/DbException.php');
require_once(__DIR__ . '/exceptions/ErrorHandler.php');
36
37
require_once(__DIR__ . '/database/Database.php');
require_once(__DIR__ . '/database/DatabaseUpdate.php');
38
39
40
41
42
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
class QuickFormQuery {
65

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

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

76
77
78
79
    /**
     * @var Evaluate instantiated class
     */
    protected $eval = null;
80
81
82
    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
83
    protected $feSpecNativeRaw = array(); // FormEelement Definition: all formElement.class='action' of the loaded form
84

85
86
87
88
    /**
     * @var array
     */
    private $t3data = array(); // FormEelement Definition: all formElement.class='native' of the loaded form
89

90
91
92
    /**
     * @var bool
     */
93
94
    private $phpUnit = false;

95
96
97
98
99
    /**
     * @var Session
     */
    private $session = null;

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

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

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

131
132
        $this->phpUnit = $phpUnit;

133
        mb_internal_encoding("UTF-8");
134

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

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

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

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

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

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

153
154
        $this->t3data = $t3data;

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

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

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

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

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

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

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

175

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

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

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

        // 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
189
190
    }

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

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

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

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

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

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

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

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

241
        $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
242
        if ($class) {
243
            $html = Support::wrapTag("<div class='$class'>", $html);
Carsten  Rose's avatar
Carsten Rose committed
244
245
        }

246
        return $html;
247
248
    }

249
250
251
252
253
254
255
256
257
258
259
    /**
     * Determine the name of the language parameter field, which has to be taken to fill language specific defintions.
     */
    private function setParameterLanguageFieldName() {

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

        foreach (['A', 'B', 'C', 'D'] as $key) {
260
            $languageIdx = SYSTEM_FORM_LANGUAGE . "$key" . "Id";
261
262
263
264
265
266
267
            if ($this->store->getVar($languageIdx, STORE_SYSTEM) == $typo3PageLanguage) {
                $this->store->setVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, 'parameterLanguage' . $key, STORE_SYSTEM);
                break;
            }
        }
    }

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

Carsten  Rose's avatar
Carsten Rose committed
288
        // Fill STORE_FORM
289
        if ($formMode === FORM_UPDATE || $formMode === FORM_SAVE) {
Carsten  Rose's avatar
Carsten Rose committed
290
            $fillStoreForm = new FillStoreForm();
291
            $fillStoreForm->process($formMode);
Carsten  Rose's avatar
Carsten Rose committed
292
        }
293

294
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO);
295
        $this->setParameterLanguageFieldName();
296
297

        $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore);
298
        if ($formName === false && $formMode !== FORM_DELETE) {
299
            // No form found: do nothing
300
            return '';
301
        }
302

303
304
        if ($formName !== false) {
            // Validate only if there is a 'real' form (not a FORM_DELETE with only a tablename).
305
            $sipFound = $this->validateForm($foundInStore, $formMode);
306
307

        } else {
308
            // FORM_DELETE without a form definition: Fake the form with only a tableName.
309
310
            $table = $this->store->getVar(SIP_TABLE, STORE_SIP);
            if ($table === false) {
311
                throw new UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE);
312
313
314
315
            }
            $sipFound = true;
            $this->formSpec[F_NAME] = '';
            $this->formSpec[F_TABLE_NAME] = $table;
316
317
            $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.
318
319
320
321
322
323
324
325
326
327

            $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);
                    }
                }
            }
328
329
        }

330
331
        // 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.
332
        if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) {
333
334
            $this->store->createSipAfterFormLoad($formName);
        }
335

336
337
338
339
        if ($this->store->getVar('id', STORE_BEFORE) === false) {
            $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_BEFORE);
        }

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

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

347
            // In case of a conflict, return immediately
Carsten  Rose's avatar
Carsten Rose committed
348
349
            if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
                $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
350

351
352
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
353
354
        }

355
356
        // FORM_LOAD: if there is an foreign exclusive record lock - show form in F_MODE_READONLY mode.
        if ($formMode === FORM_LOAD) {
357
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
358
359
360
361
362
363
364
            $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;
            }
        }

365
        if ($formMode === FORM_DELETE) {
366

367
            $build = new Delete($this->dbIndexData);
368
369

        } else {
370
            $tableDefinition = $this->dbArray[$this->dbIndexData]->getTableDefinition($this->formSpec[F_TABLE_NAME]);
371
            $this->store->fillStoreTableDefaultColumnType($tableDefinition);
372
373
374

            switch ($this->formSpec['render']) {
                case 'plain':
375
                    $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
376
377
                    break;
                case 'table':
378
                    $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
379
380
                    break;
                case 'bootstrap':
381
                    $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
382
383
384
385
                    break;
                default:
                    throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
            }
386
387
        }

388
        $formAction = new FormAction($this->formSpec, $this->dbArray[$this->dbIndexData], $this->phpUnit);
389
        switch ($formMode) {
390
            case FORM_LOAD:
391
392
393
394
395
396
397
                $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
398
            case FORM_UPDATE:
399
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
400
401
                // data['form-update']=....
                $data = $build->process($formMode);
402
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
403
                break;
Carsten  Rose's avatar
Carsten Rose committed
404

405
406
407
408
409
410
411
412
            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;

413
            case FORM_SAVE:
414
415
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

416
                // Action: Before
417
418
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_INSERT . ',' . FE_TYPE_BEFORE_UPDATE . ',' . FE_TYPE_BEFORE_SAVE);

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

422
                // SAVE
423
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw);
424
425
426

                $save->processAllImageCutFE();

427
428
                $rc = $save->process();

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

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

434
                // Action: After
435
436
                $status = $formAction->elements($rc, $this->feSpecAction, FE_TYPE_AFTER_INSERT . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_AFTER_SAVE);
                if ($status != ACTION_ELEMENT_NO_CHANGE) {
437
                    // Reload fresh saved record and fill STORE_RECORD with it.
438
                    $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, STORE_RECORD);
439
                }
440

Carsten  Rose's avatar
Carsten Rose committed
441
442
443
444
445
446
                // Action: Paste
                $this->pasteClipboard($this->formSpec[F_ID], $formAction);

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

447
448
449
450
451
452
453

                $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!
454
                $getJson = true;
455
456
457
458
459
                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);
460
                    $getJson = false;
461
                }
462

463
                if ($getJson) {
464

465
466
                    // 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);
467
468
469
470
                    $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);
471

472
473
                    // Retrieve FE Values as JSON
                    // $data['form-update']=...
474
                    // $data = $build->process($formMode, $htmlElementNameIdZero);
475
                    $data = $build->process($formMode, false, $this->feSpecNative);
476
                }
477

478
                break;
Carsten  Rose's avatar
Carsten Rose committed
479

480
481
482
483
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

484
        if (is_array($data)) {
485
            // $data['element-update']=...
486
487
            $data = $this->groupElementUpdateEntries($data);
        }
Carsten  Rose's avatar
Carsten Rose committed
488

Carsten  Rose's avatar
Carsten Rose committed
489
        return $data;
490
491
    }

492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508

    /**
     * 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
509
510
     * @throws CodeException
     * @throws DbException
511
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
512
     * @throws UserReportException
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
     */
    private function setForwardModePage() {

        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
561
562
563
    /**
     * Iterate over all Clipboard source records and fire for each all FE.type=paste records.
     *
Carsten  Rose's avatar
Carsten Rose committed
564
     * @param int $formId
Carsten  Rose's avatar
Carsten Rose committed
565
     * @param FormAction $formAction
566
     *
Carsten  Rose's avatar
Carsten Rose committed
567
568
569
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
570
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
571
572
573
     */
    private function pasteClipboard($formId, FormAction $formAction) {

574
575
576
577
        if (!$this->isPasteRecord()) {
            return;
        }

Carsten  Rose's avatar
Carsten Rose committed
578
579
580
581
582
583
584
        $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";
585
        $arrClipboard = $this->dbArray[$this->dbIndexQfq]->sql($sql);
Carsten  Rose's avatar
Carsten Rose committed
586
587
588
589
590
591
592
593

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

    } # doClipboard()

594
595
596
597
598
599
600
601
602
603
    /**
     * @return bool  true if there is at least one paste record, else false.
     */
    private function isPasteRecord() {

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

605
606
607
608
        return false;

    }

609
610
611
612
613
    /**
     * 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
614
     * @param int $recordId
615
     *
616
617
618
619
620
621
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
    private function buildNSetReloadUrl(array $formSpec, $recordId) {

622
        $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639

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

    }

640
    /**
641
     * Load form. Evaluates form. Load FormElements.
642
     *
Carsten  Rose's avatar
Carsten Rose committed
643
     * After processing:
644
645
646
647
     * 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
648
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
649
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
650
     * @param string $foundInStore
651
     *
Carsten  Rose's avatar
Carsten Rose committed
652
653
     * @return bool|string if found the formName, else 'false'.
     * @throws CodeException
654
     * @throws DbException
655
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
656
     * @throws UserReportException
657
     */
658
    private function loadFormSpecification($mode, $recordId, &$foundInStore = '') {
Carsten  Rose's avatar
Carsten Rose committed
659

660
        // formName
Carsten  Rose's avatar
Carsten Rose committed
661
        if (false === ($formName = $this->getFormName($mode, $foundInStore))) {
662
663
            return false;
        }
664

665
666
        if (!$this->dbArray[$this->dbIndexQfq]->existTable(TABLE_NAME_FORM)) {
            throw new UserFormException("Table '" . TABLE_NAME_FORM . "' not found", ERROR_MISSING_TABLE);
667
668
        }

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

672
673
674
675
676
677
678
        // 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);
        }

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

684
        $form = $this->modeCleanFormConfig($mode, $form);
685

Carsten  Rose's avatar
Carsten Rose committed
686
687
688
689
        // Save specific elements to be expanded later.
        $parseLater = OnArray::getArrayItems($form, [F_FORWARD_PAGE]);
        $form[F_FORWARD_PAGE] = '';

690
691
692
693
694
695
696
        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]);
        }

697
        // Setting defaults later is too late.
698
699
700
701
        if (empty($form[F_DB_INDEX])) {
            $form[F_DB_INDEX] = $this->dbIndexData;
        } else {
            $form[F_DB_INDEX] = $this->eval->parse($form[F_DB_INDEX]);
702
703
704
        }

        // Some forms load/save the form data on extra defined databases.
705
706
707
        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]);
708
            }
709
710
711
712
            $this->dbIndexData = $form[F_DB_INDEX];

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

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

721
722
        $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);
        $formSpec = HelperFormElement::setLanguage($formSpec, $parameterLanguageFieldName);
723

724
725
        $formSpec = $this->syncSystemFormConfig($formSpec);
        $formSpec = $this->initForm($formSpec);
726

Carsten  Rose's avatar
Carsten Rose committed
727
728
        $formSpec = array_merge($formSpec, $parseLater);

729
        // Set F_FINAL_DELETE_FORM
730
        $formSpec[F_FINAL_DELETE_FORM] = ($formSpec[F_EXTRA_DELETE_FORM] != '') ? $formSpec[F_EXTRA_DELETE_FORM] : $formSpec[F_NAME];
731

732
733
734
735
        // 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)) {
736
                $this->store->appendToStore($rows[0], STORE_VAR);
737
738
739
740
741
742
743
744
            } 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;
745

Carsten  Rose's avatar
Carsten Rose committed
746
        // Clear
747
748
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

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

752
        // FE: Action
753
        $this->feSpecAction = $this->dbArray[$this->dbIndexQfq]->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, ['no', $this->formSpec["id"], 'action']);
754
        HelperFormElement::explodeParameterInArrayElements($this->feSpecAction, FE_PARAMETER);
755
756

        // FE: Native & Container
757
        // "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";
758
        $feSpecNative = array();
759
760
        switch ($mode) {
            case FORM_LOAD:
761
                // Select all Native elements (native, pill, fieldset, templateGroup) which are NOT nested = Root level.
762
                $feSpecNative = $this->dbArray[$this->dbIndexQfq]->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER, ['no', $this->formSpec["id"], 'native,container', 0], $this->formSpec);
763
764
765
                break;

            case FORM_SAVE:
Carsten  Rose's avatar
Carsten Rose committed
766
            case FORM_UPDATE:
767
            $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec[F_ID]], $this->formSpec);
768
769
                break;

770
771
772
773
            case FORM_DELETE:
                $this->feSpecNative = array();
                break;

774
            default:
775
                break;
776
777
        }

778
        $this->feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName);
779
        $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($this->feSpecNative, $this->formSpec[F_ID], $recordId);
780

781
        return $formName;
Carsten  Rose's avatar
Carsten Rose committed
782
783
    }

784
785
    /**
     * Depending on $sql reads FormElements to a specific container or all. Preprocess all FormElements.
786
787
788
     * This code is dirty: the nearly same function exists in class 'Database' - the difference is only
     * 'explodeTemplateGroupElements()'.
     *
Carsten  Rose's avatar
Carsten Rose committed
789
790
791
     * @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
792
793
     *
     * @return array|int
794
795
796
797
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
798
799
800
     */
    public function getNativeFormElements($sql, array $param, $formSpec) {

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

803
804
        $feSpecNative = HelperFormElement::formElementSetDefault($feSpecNative);

805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
        // 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;
    }

    /**
821
822
823
     * 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.
824
     *
825
826
827
     * 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).
828
829
830
831
     *
     * Attention: The resulting order of the FormElements, is not the same as on the Form during FormLoad!
     *
     * @param array $elements
832
     *
833
     * @return array
834
835
836
837
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     * @throws UserReportException
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
     */
    private function explodeTemplateGroupElements(array $elements) {
        $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]);
859
                    $tmpRow[FE_TG_INDEX] = $ii;
860
861
862
863
864
865
                    $new[] = $tmpRow;
                }
            } else {
                $new[] = $row;
            }
        }
866

867
868
869
870
        return $new;
    }


Carsten  Rose's avatar
Carsten Rose committed
871
    /**
872
873
     * Get the formName from STORE_TYPO3 (bodytext), STORE_SIP or by STORE_CLIENT (URL).
     *
874
875
876
877
     * 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
878
879
880
     *   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
881
882
883
884
885
     *
     * FORM_SAVE:
     *   Specified in SIP
     *
     *
Carsten  Rose's avatar
Carsten Rose committed
886
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
887
     * @param string $foundInStore
888
     *
889
     * @return bool|string  Formname (Form.name) or FALSE (if no formname found)
Carsten  Rose's avatar
Carsten Rose committed
890
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
891
     * @throws DbException
892
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
893
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
894
     */
895
    public function getFormName($mode, &$foundInStore = '') {
896
        $dummy = array();
Carsten  Rose's avatar
Carsten Rose committed
897

Carsten  Rose's avatar
Carsten Rose committed
898
899
900
901
902
903
        switch ($mode) {
            case FORM_LOAD:
                $store = STORE_TYPO3;
                break;
            case FORM_SAVE:
            case FORM_UPDATE:
904
905
            case FORM_DELETE:
                $store = STORE_SIP;
Carsten  Rose's avatar
Carsten Rose committed
906
907
908
909
910
                break;
            default:
                throw new CodeException("Unknown mode: $mode.", ERROR_UNKNOWN_MODE);
        }

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

914
915
        // If the formname is empty or if 'form' has not been found in any store: no form.
        if ($formName === '' || $foundInStore === '') {
916
917
            return false;
        }
918

919
        return $formName;
920
    }
Carsten  Rose's avatar
Carsten Rose committed
921

922
923
924
925
    /**
     * Depending on $mode various formSpec fields might be adjusted.
     * E.g.: the form title is not important during a delete.
     *
926
     * @param string $mode
Carsten  Rose's avatar
Carsten Rose committed
927
     * @param array $form
928
     *
929
     * @return array
930
931
     * @throws CodeException
     * @throws UserFormException
932
     */
933
    private function modeCleanFormConfig($mode, array $form) {
934
935
936
937
938
939
940
941
942

        switch ($mode) {
            case FORM_DELETE:
                $form[F_TITLE] = '';
                break;
            default:
                break;
        }

943
944
945
946
        if (isset($form[F_ESCAPE_TYPE_DEFAULT]) && $form[F_ESCAPE_TYPE_DEFAULT] == TOKEN_ESCAPE_CONFIG) {
            $form[F_ESCAPE_TYPE_DEFAULT] = $this->store->getVar(SYSTEM_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
        }

947
948
949
        return $form;
    }

950
    /**
951
952
953
954
955
     * The named $keys will be synced between STORE_SYSTEM and $formSpec (both directions).
     * The per form definition has precedence over STORE_SYSTEM.
     * STORE_SYSTEM if filled with the default values (config.qfq.ini or if note exist than QFQ hardcoded)
     * Copying the 'Form' definition back to the system store helps to access the values
     * by '{{ ...:Y}}' (system store). E.g. the value of bs-*-columns might be displayed as placeholder in the
956
     * corresponding input field.