QuickFormQuery.php 41.2 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
73
    /**
     * @var Database instantiated class
     */
74
    protected $db = null;
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

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

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

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

99
100
101
102
103
104
105
106
107
108
109
    /*
     * 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'
     */
110

111
112
113
    /**
     * Construct the Form Class and Store too. This is the base initialization moment.
     *
114
115
     * 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.
116
     *
117
     * @param array $t3data
118
119
     * @param bool  $phpUnit
     *
120
121
     * @throws CodeException
     * @throws UserFormException
122
     */
123
124
    public function __construct(array $t3data = array(), $phpUnit = false) {

125
126
        $this->phpUnit = $phpUnit;

127
        mb_internal_encoding("UTF-8");
128

129
130
        $this->session = Session::getInstance($phpUnit);

131
        // Refresh the session even if no new data saved.
132
        Session::set('LAST_ACTIVITY', time());
133

134
        set_error_handler("\\qfq\\ErrorHandler::exception_error_handler");
135

136
137
138
139
140
141
142
        if (!isset($t3data[T3DATA_BODYTEXT])) {
            $t3data[T3DATA_BODYTEXT] = '';
        }

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

144
        $btp = new BodytextParser();
145
        $t3data[T3DATA_BODYTEXT] = $btp->process($t3data[T3DATA_BODYTEXT]);
146

147
148
        $this->t3data = $t3data;

149
        $bodytext = $this->t3data[T3DATA_BODYTEXT];
150
151

        $this->store = Store::getInstance($bodytext, $phpUnit);
152
        $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data[T3DATA_UID], STORE_TYPO3);
153
        $this->db = new Database();
154

155
        $this->eval = new Evaluate($this->store, $this->db);
156

157
        $dbUpdate = $this->store->getVar(SYSTEM_DB_UPDATE, STORE_SYSTEM);
158
        $updateDb = new DatabaseUpdate($this->db);
159
        $updateDb->checkNupdate($dbUpdate);
160

161
        $this->store->systemStoreUpdate(); // Do this after the DB-update
Carsten  Rose's avatar
Carsten Rose committed
162
163
    }

164
    /**
165
     * Returns the defined forwardMode and set forwardPage
166
     *
167
     * @return array
168
     */
169
170
    public function getForwardMode() {

Carsten  Rose's avatar
Carsten Rose committed
171
172
173
174
175
        $forwardPage = $this->eval->parse($this->formSpec[F_FORWARD_PAGE]);
        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.
176
            // 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
177
178
179
            $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL_SKIP_HISTORY;
        }

180
181
        return ([
            API_REDIRECT => $this->formSpec[F_FORWARD_MODE],
Carsten  Rose's avatar
Carsten Rose committed
182
            API_REDIRECT_URL => $forwardPage,
183
        ]);
184
185
    }

186
    /**
Carsten  Rose's avatar
Carsten Rose committed
187
     * Main entrypoint for display content: a) form and/or b) report
188
     *
189
     * @return string
Carsten  Rose's avatar
Carsten Rose committed
190
     */
191
    public function process() {
192
        $html = '';
193

194
        if ($this->store->getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === 'yes') {
195
196
            $htmlId = HelperFormElement::buildFormElementId($this->formSpec[F_ID], 0, 0, 0);
            $html .= Support::doTooltip($htmlId . HTML_ID_EXTENSION_TOOLTIP, $this->t3data['bodytext']);
197
198
199
        }

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

202
        // 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
203
204
205
206
        if ($this->store->getVar(SYSTEM_DOWNLOAD_POPUP, STORE_SYSTEM) == DOWNLOAD_POPUP_REQUEST) {
            $html .= $this->getModalCode();
        }

207
        $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
208
        if ($class) {
209
            $html = Support::wrapTag("<div class='$class'>", $html);
Carsten  Rose's avatar
Carsten Rose committed
210
211
        }

212
        return $html;
213
214
    }

215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
    /**
     * 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) {
            $languageIdx = SYSTEM_FORM_LANGUAGE . "_$key" . "_ID";
            if ($this->store->getVar($languageIdx, STORE_SYSTEM) == $typo3PageLanguage) {
                $this->store->setVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, 'parameterLanguage' . $key, STORE_SYSTEM);
                break;
            }
        }
    }

234
    /**
235
     * Process form.
236
237
238
239
240
     * $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:
241
     *
242
     * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE
243
     *
244
     * @return array|string
245
     * @throws CodeException
246
     * @throws UserFormException
247
     */
248
    private function doForm($formMode) {
Carsten  Rose's avatar
Carsten Rose committed
249
        $data = '';
Carsten  Rose's avatar
Carsten Rose committed
250
        $foundInStore = '';
251

Carsten  Rose's avatar
Carsten Rose committed
252
        // Fill STORE_FORM
253
        if ($formMode === FORM_UPDATE || $formMode === FORM_SAVE) {
Carsten  Rose's avatar
Carsten Rose committed
254
255
256
            $fillStoreForm = new FillStoreForm();
            $fillStoreForm->process();
        }
257

258
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO);
259
        $this->setParameterLanguageFieldName();
260
261

        $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore);
262
        if ($formName === false && $formMode !== FORM_DELETE) {
263
            // No form found: do nothing
264
            return '';
265
        }
266

267
268
        if ($formName !== false) {
            // Validate only if there is a 'real' form (not a FORM_DELETE with only a tablename).
269
            $sipFound = $this->validateForm($foundInStore, $formMode);
270
271

        } else {
272
            // FORM_DELETE without a form definition: Fake the form with only a tableName.
273
274
            $table = $this->store->getVar(SIP_TABLE, STORE_SIP);
            if ($table === false) {
275
                throw new UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE);
276
277
278
279
280
281
            }
            $sipFound = true;
            $this->formSpec[F_NAME] = '';
            $this->formSpec[F_TABLE_NAME] = $table;
        }

282
283
        // 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.
284
        if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) {
285
286
            $this->store->createSipAfterFormLoad($formName);
        }
287

288
289
290
291
        if ($this->store->getVar('id', STORE_BEFORE) === false) {
            $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_BEFORE);
        }

292
        // Check (and release) dirtyRecord.
Carsten  Rose's avatar
Carsten Rose committed
293
        if ($formMode === FORM_DELETE || $formMode === FORM_SAVE) {
Carsten  Rose's avatar
Carsten Rose committed
294
            $dirty = new Dirty();
Carsten  Rose's avatar
Carsten Rose committed
295

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

299
            // In case of a conflict, return immediately
Carsten  Rose's avatar
Carsten Rose committed
300
301
            if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
                $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
302

303
304
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
305
306
        }

307
308
309
310
311
312
313
314
315
316
        // FORM_LOAD: if there is an foreign exclusive record lock - show form in F_MODE_READONLY mode.
        if ($formMode === FORM_LOAD) {
            $dirty = new Dirty();
            $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;
            }
        }

317
        if ($formMode === FORM_DELETE) {
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336

            $build = new Delete();

        } else {
            $this->store->fillStoreTableDefaultColumnType($this->formSpec[F_TABLE_NAME]);

            switch ($this->formSpec['render']) {
                case 'plain':
                    $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative);
                    break;
                case 'table':
                    $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative);
                    break;
                case 'bootstrap':
                    $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative);
                    break;
                default:
                    throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
            }
337
338
        }

Carsten  Rose's avatar
#2067    
Carsten Rose committed
339
        $formAction = new FormAction($this->formSpec, $this->db, $this->phpUnit);
340
        switch ($formMode) {
341
            case FORM_LOAD:
342
343
344
345
346
347
348
349
                $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
350
            case FORM_UPDATE:
351
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
352
353
                // data['form-update']=....
                $data = $build->process($formMode);
354
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
355
                break;
Carsten  Rose's avatar
Carsten Rose committed
356

357
358
359
360
361
362
363
364
            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;

365
            case FORM_SAVE:
366
367
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

368
                // Action: Before
369
370
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_INSERT . ',' . FE_TYPE_BEFORE_UPDATE . ',' . FE_TYPE_BEFORE_SAVE);

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

374
                // SAVE
375
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative);
376
377
                $rc = $save->process();

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

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

383
                // Action: After
384
385
                $status = $formAction->elements($rc, $this->feSpecAction, FE_TYPE_AFTER_INSERT . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_AFTER_SAVE);
                if ($status != ACTION_ELEMENT_NO_CHANGE) {
386
                    // Reload fresh saved record and fill STORE_RECORD with it.
387
                    $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, STORE_RECORD);
388
                }
389

Carsten  Rose's avatar
Carsten Rose committed
390
391
392
393
394
395
396
                // Action: Paste
                $this->pasteClipboard($this->formSpec[F_ID], $formAction);

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

                //                $htmlElementNameIdZero = false;
397
                $getJson = true;
398
399
                // Retrieve current STORE_SIP.
                $sipArray = $this->store->getStore(STORE_SIP);
400
                if ($sipArray[SIP_RECORD_ID] == 0 && API_SUBMIT_REASON_SAVE == $this->store->getVar(API_SUBMIT_REASON, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX)) {
401
//                if ($sipArray[SIP_RECORD_ID] == 0 ) {
402

Carsten  Rose's avatar
Carsten Rose committed
403
404
405
406
                    if ($this->formSpec[F_FORWARD_MODE] !== F_FORWARD_MODE_URL &&
                        $this->formSpec[F_FORWARD_MODE] !== F_FORWARD_MODE_URL_SKIP_HISTORY &&
                        $this->formSpec[F_FORWARD_MODE] !== F_FORWARD_MODE_URL_SIP
                    ) {
407
                        $this->formSpec = $this->buildNSetReloadUrl($this->formSpec, $rc);
408
409
                    }
                    $getJson = false;
410
                }
411

412
413
414
415
416
417
                if ($getJson) {
                    // Retrieve FE Values as JSON
                    // $data['form-update']=...
//                    $data = $build->process($formMode, $htmlElementNameIdZero);
                    $data = $build->process($formMode);
                }
418
                break;
Carsten  Rose's avatar
Carsten Rose committed
419

420
421
422
423
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

424
        if (is_array($data)) {
425
            // $data['element-update']=...
426
427
            $data = $this->groupElementUpdateEntries($data);
        }
Carsten  Rose's avatar
Carsten Rose committed
428

Carsten  Rose's avatar
Carsten Rose committed
429
        return $data;
430
431
    }

Carsten  Rose's avatar
Carsten Rose committed
432
433
434
    /**
     * Iterate over all Clipboard source records and fire for each all FE.type=paste records.
     *
435
     * @param int        $formId
Carsten  Rose's avatar
Carsten Rose committed
436
     * @param FormAction $formAction
437
     *
Carsten  Rose's avatar
Carsten Rose committed
438
439
440
441
442
443
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     */
    private function pasteClipboard($formId, FormAction $formAction) {

444
445
446
447
        if (!$this->isPasteRecord()) {
            return;
        }

Carsten  Rose's avatar
Carsten Rose committed
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
        $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";
        $arrClipboard = $this->db->sql($sql);

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

    } # doClipboard()

464
465
466
467
468
469
470
471
472
473
    /**
     * @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;
            }
        }
474

475
476
477
478
        return false;

    }

479
480
481
482
483
    /**
     * 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
484
485
     * @param int   $recordId
     *
486
487
488
489
490
491
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
    private function buildNSetReloadUrl(array $formSpec, $recordId) {

492
        $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509

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

    }

510
    /**
511
     * Load form. Evaluates form. Load FormElements.
512
     *
Carsten  Rose's avatar
Carsten Rose committed
513
     * After processing:
514
515
516
517
     * 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
518
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
519
     * @param int    $recordId
Carsten  Rose's avatar
Carsten Rose committed
520
     * @param string $foundInStore
521
     *
Carsten  Rose's avatar
Carsten Rose committed
522
523
     * @return bool|string if found the formName, else 'false'.
     * @throws CodeException
524
     * @throws DbException
525
     * @throws UserFormException
526
     */
527
    private function loadFormSpecification($mode, $recordId, &$foundInStore = '') {
Carsten  Rose's avatar
Carsten Rose committed
528

529
        // formName
Carsten  Rose's avatar
Carsten Rose committed
530
        if (false === ($formName = $this->getFormName($mode, $foundInStore))) {
531
532
            return false;
        }
533

534
535
536
537
        if (!$this->db->existTable('Form')) {
            throw new UserFormException("Table 'Form' not found", ERROR_MISSING_TABLE);
        }

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

541
542
543
544
545
546
547
        // 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);
        }

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

553
        $form = $this->modeCleanFormConfig($mode, $form);
554

Carsten  Rose's avatar
Carsten Rose committed
555
556
557
558
        // Save specific elements to be expanded later.
        $parseLater = OnArray::getArrayItems($form, [F_FORWARD_PAGE]);
        $form[F_FORWARD_PAGE] = '';

559
        $formSpec = $this->eval->parseArray($form);
560
        HelperFormElement::explodeParameter($formSpec, F_PARAMETER);
561
562
563

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

565
566
        $formSpec = $this->syncSystemFormConfig($formSpec);
        $formSpec = $this->initForm($formSpec);
567

Carsten  Rose's avatar
Carsten Rose committed
568
569
        $formSpec = array_merge($formSpec, $parseLater);

570
        // Set F_FINAL_DELETE_FORM
571
        $formSpec[F_FINAL_DELETE_FORM] = ($formSpec[F_EXTRA_DELETE_FORM] != '') ? $formSpec[F_EXTRA_DELETE_FORM] : $formSpec[F_NAME];
572

573
        $this->formSpec = $formSpec;
574

575
576
577
        // this is needed for filling templateGroup records with their default values
        $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_RECORD);

Carsten  Rose's avatar
Carsten Rose committed
578
        // Clear
579
580
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

581
        // FE: Action
582
        $this->feSpecAction = $this->db->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, ['no', $this->formSpec["id"], 'action']);
583
        HelperFormElement::explodeParameterInArrayElements($this->feSpecAction, FE_PARAMETER);
584
585

        // FE: Native & Container
586
        // "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";
587
        $feSpecNative = array();
588
589
        switch ($mode) {
            case FORM_LOAD:
590
                // Select all Native elements (native, pill, fieldset, templateGroup) which are NOT nested = Root level.
591
                $feSpecNative = $this->db->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER, ['no', $this->formSpec["id"], 'native,container', 0], $this->formSpec);
592
593
594
                break;

            case FORM_SAVE:
Carsten  Rose's avatar
Carsten Rose committed
595
            case FORM_UPDATE:
Carsten  Rose's avatar
Carsten Rose committed
596
//              $this->feSpecNative = $this->db->getNativeFormElements(SQL_FORM_ELEMENT_ALL_CONTAINER, ['no', $this->formSpec["id"], 'native'], $this->formSpec);
597
            $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec["id"]], $this->formSpec);
598
599
                break;

600
601
602
603
            case FORM_DELETE:
                $this->feSpecNative = array();
                break;

604
            default:
605
                break;
606
607
        }

608
        $this->feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName);
609

610
        return $formName;
Carsten  Rose's avatar
Carsten Rose committed
611
612
    }

613
614
    /**
     * Depending on $sql reads FormElements to a specific container or all. Preprocess all FormElements.
615
616
617
618
619
620
     * This code is dirty: the nearly same function exists in class 'Database' - the difference is only
     * 'explodeTemplateGroupElements()'.
     *
     * @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
621
622
623
624
625
626
627
628
629
     *
     * @return array|int
     * @throws \qfq\CodeException
     * @throws \qfq\DbException
     */
    public function getNativeFormElements($sql, array $param, $formSpec) {

        $feSpecNative = $this->db->sql($sql, ROW_REGULAR, $param);

630
631
        $feSpecNative = HelperFormElement::formElementSetDefault($feSpecNative);

632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
        // 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;
    }

    /**
648
649
650
     * 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.
651
     *
652
653
654
     * 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).
655
656
657
658
     *
     * Attention: The resulting order of the FormElements, is not the same as on the Form during FormLoad!
     *
     * @param array $elements
659
     *
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
     * @return array
     */
    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]);
                    $new[] = $tmpRow;
                }
            } else {
                $new[] = $row;
            }
        }
688

689
690
691
692
        return $new;
    }


Carsten  Rose's avatar
Carsten Rose committed
693
    /**
694
695
     * Get the formName from STORE_TYPO3 (bodytext), STORE_SIP or by STORE_CLIENT (URL).
     *
696
697
698
699
     * 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
700
701
702
     *   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
703
704
705
706
707
     *
     * FORM_SAVE:
     *   Specified in SIP
     *
     *
Carsten  Rose's avatar
Carsten Rose committed
708
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
709
     * @param string $foundInStore
710
     *
711
     * @return bool|string  Formname (Form.name) or FALSE (if no formname found)
Carsten  Rose's avatar
Carsten Rose committed
712
     * @throws CodeException
713
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
714
     */
715
    public function getFormName($mode, &$foundInStore = '') {
716
        $dummy = array();
Carsten  Rose's avatar
Carsten Rose committed
717

Carsten  Rose's avatar
Carsten Rose committed
718
719
720
721
722
723
        switch ($mode) {
            case FORM_LOAD:
                $store = STORE_TYPO3;
                break;
            case FORM_SAVE:
            case FORM_UPDATE:
724
725
            case FORM_DELETE:
                $store = STORE_SIP;
Carsten  Rose's avatar
Carsten Rose committed
726
727
728
729
730
                break;
            default:
                throw new CodeException("Unknown mode: $mode.", ERROR_UNKNOWN_MODE);
        }

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

734
735
736
737
//        if($mode===FORM_DELETE && $formName===false) {
//            return "";
//        }

738
        // If the formname is '': no formname name.
739
        if ($formName === '' || $foundInStore === '')
740
741
742
743
744
745
            return false;

        // If the formname is surrounded by single ticks: the token (typically 'form') has not been replaced by a value.
        if ($formName[0] === "'" && $formName[strlen($formName) - 1] === "'") {
            return false;
        }
746

747
        return $formName;
748
    }
Carsten  Rose's avatar
Carsten Rose committed
749

750
751
752
753
    /**
     * Depending on $mode various formSpec fields might be adjusted.
     * E.g.: the form title is not important during a delete.
     *
754
     * @param string $mode
755
756
     * @param array  $form
     *
757
758
     * @return array
     */
759
    private function modeCleanFormConfig($mode, array $form) {
760
761
762
763
764
765
766
767
768
769
770
771

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

        return $form;
    }

772
    /**
773
774
775
776
777
778
     * 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
     * corresponding inputfield.
779
     *
780
     * @param array $formSpec
781
     *
782
783
     * @return array
     */
784
785
    private function syncSystemFormConfig(array $formSpec) {

786
787
        $keys = [F_BS_COLUMNS,
            F_BS_LABEL_COLUMNS,
788
789
790
791
792
793
794
795
796
            F_BS_INPUT_COLUMNS,
            F_BS_NOTE_COLUMNS,
            F_FE_DATA_PATTERN_ERROR,
            F_FE_DATA_REQUIRED_ERROR,
            F_FE_DATA_MATCH_ERROR,
            F_FE_DATA_ERROR,
            F_CLASS,
            F_CLASS_PILL,
            F_CLASS_BODY,
797
            F_BUTTON_ON_CHANGE_CLASS,
798
            F_ESCAPE_TYPE_DEFAULT,
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
            F_SAVE_BUTTON_TEXT,
            F_SAVE_BUTTON_TOOLTIP,
            F_SAVE_BUTTON_CLASS,
            F_SAVE_BUTTON_GLYPH_ICON,

            F_CLOSE_BUTTON_TEXT,
            F_CLOSE_BUTTON_TOOLTIP,
            F_CLOSE_BUTTON_CLASS,
            F_CLOSE_BUTTON_GLYPH_ICON,

            F_DELETE_BUTTON_TEXT,
            F_DELETE_BUTTON_TOOLTIP,
            F_DELETE_BUTTON_CLASS,
            F_DELETE_BUTTON_GLYPH_ICON,

            F_NEW_BUTTON_TEXT,
            F_NEW_BUTTON_TOOLTIP,
            F_NEW_BUTTON_CLASS,
            F_NEW_BUTTON_GLYPH_ICON,

819
820
            F_RECORD_LOCK_TIMEOUT_SECONDS,

821
822
823
824
        ];

        // By definition: existing vars which are empty, means: EMPTY - do not use any default!
        // But: if these variables are table columns, they always exist. For those: empty value means 'not set' - unset those.
825
        foreach ([F_BS_LABEL_COLUMNS, F_BS_INPUT_COLUMNS, F_BS_NOTE_COLUMNS, F_ESCAPE_TYPE_DEFAULT] as $key) {
826
827
828
829
            if ($formSpec[$key] == '') {
                unset ($formSpec[$key]);
            }
        }
830

831
        foreach ($keys as $key) {
832

833
834
835
836
837
838
839
            if (isset($formSpec[$key])) {
                $this->store->setVar($key, $formSpec[$key], STORE_SYSTEM);
            } else {
                // if not found set ''
                $formSpec[$key] = $this->store->getVar($key, STORE_SYSTEM . STORE_EMPTY);
            }
        }
840

841
842
        return $formSpec;
    }
843

844
845
846
    /**
     * Set form parameter which are expected to exist.
     *
Carsten  Rose's avatar
Carsten Rose committed
847
     * @param array $formSpec
848
     *
849
850
     * @return array
     */
Carsten  Rose's avatar
Carsten Rose committed
851
852
853
854
855
856
857
    private function initForm(array $formSpec) {
        Support::setIfNotSet($formSpec, F_EXTRA_DELETE_FORM, '');
        Support::setIfNotSet($formSpec, F_SUBMIT_BUTTON_TEXT, '');
        Support::setIfNotSet($formSpec, F_BUTTON_ON_CHANGE_CLASS, '');
        Support::setIfNotSet($formSpec, F_LDAP_USE_BIND_CREDENTIALS, '');
        Support::setIfNotSet($formSpec, F_MODE, '');

858
859
860
861
862
863
        // In case there is no F_MODE defined on the form, check if there is one in STORE_SIP.
        if ($formSpec[F_MODE] == '') {
            $formModeGlobal = $this->store->getVar(F_MODE_GLOBAL, STORE_SIP);
            if ($formModeGlobal !== false) {
                $formSpec[F_MODE] = $formModeGlobal;
            }
864
865
        }

Carsten  Rose's avatar
Carsten Rose committed
866
867
868
869
        if ($formSpec[F_MODE] == F_MODE_READONLY) {
            $formSpec[F_SHOW_BUTTON] = FORM_BUTTON_CLOSE;
            $formSpec[F_SUBMIT_BUTTON_TEXT] = '';
        }
870

871
        if ($formSpec[F_ESCAPE_TYPE_DEFAULT] == TOKEN_ESCAPE_CONFIG) {
872
873
874
            $formSpec[F_ESCAPE_TYPE_DEFAULT] = $this->store->getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
        }

Carsten  Rose's avatar
Carsten Rose committed
875
        return $formSpec;
876
877
    }

878
    /**
Carsten  Rose's avatar
Carsten Rose committed
879
     * Check if loading of the given form is permitted. If not, throw an exception.
880
     *
Carsten  Rose's avatar
Carsten Rose committed
881
882
     * @param string $formNameFoundInStore
     * @param string $formMode
883
     *
Carsten  Rose's avatar
Carsten Rose committed
884
     * @return bool 'true' if SIP exists, else 'false'
Carsten  Rose's avatar
Carsten Rose committed
885
886
     * @throws \qfq\CodeException
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
887
     * @internal param $foundInStore
888
     */
889
    private function validateForm($formNameFoundInStore, $formMode) {
890
891

        // Retrieve record_id either from SIP (prefered) or via URL
892
        $r = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT, '', $recordIdFoundInStore);
893

894
        // If not found: Fake a definition in STORE_TYPO3.
895
        if ($r === false) {
896
897
898
            $r = 0;
            $this->store->setVar(TYPO3_RECORD_ID, $r, STORE_TYPO3);
            $recordIdFoundInStore = STORE_TYPO3;
899
900
        }

Carsten  Rose's avatar
Carsten Rose committed
901
        // If there is a record_id>0: EDIT else NEW: 'sip','logged_in','logged_out','always','never'
902
        $permitMode = ($r > 0) ? $this->formSpec['permitEdit'] : $this->formSpec['permitNew'];
903
904
905
906
907

        $feUserLoggedIn = isset($GLOBALS["TSFE"]->fe_user->user["uid"]) && $GLOBALS["TSFE"]->fe_user->user["uid"] > 0;

        $sipFound = $this->store->getVar(SIP_SIP, STORE_SIP) !== false;

Carsten  Rose's avatar
Carsten Rose committed
908
909
        if ($sipFound) {
            if (($formNameFoundInStore === STORE_CLIENT) || ($recordIdFoundInStore === STORE_CLIENT)) {
910
                throw new UserFormException("SIP exist but FORM or RECORD_ID are given by CLIENT.", ERROR_SIP_EXIST_BUT_OTHER_PARAM_GIVEN_BY_CLIENT);
Carsten  Rose's avatar
Carsten Rose committed
911
912
913
            }
        }

914
        switch ($permitMode) {
915
            case  FORM_PERMISSION_SIP:
Carsten  Rose's avatar
Carsten Rose committed
916
                if (!$sipFound || $formNameFoundInStore !== STORE_SIP || $recordIdFoundInStore !== STORE_SIP) {
917
                    throw new UserFormException("SIP Parameter needed for this form.", ERROR_SIP_NEEDED_FOR_THIS_FORM);
918
919
920
921
                }
                break;
            case  FORM_PERMISSION_LOGGED_IN:
                if (!$feUserLoggedIn) {
922
                    throw new UserFormException("User not logged in.", ERROR_USER_NOT_LOGGED_IN);
923
924
925
926
                }
                break;
            case FORM_PERMISSION_LOGGED_OUT:
                if ($feUserLoggedIn) {
927
                    throw new UserFormException("User logged in.", ERROR_USER_LOGGED_IN);
928
929
930
931
932
                }
                break;
            case FORM_PERMISSION_ALWAYS:
                break;
            case FORM_PERMISSION_NEVER:
933
                throw new UserFormException("Loading form forbidden.", ERROR_FORM_FORBIDDEN);
934
            default:
935
                throw new CodeException("Unknown permission mode: '" . $permitMode . "'", ERROR_FORM_UNKNOWN_PERMISSION_MODE);
936
        }
Carsten  Rose's avatar
Carsten Rose committed
937

Carsten  Rose's avatar
Carsten Rose committed
938
        // Form Definition valid?
939
        if ($this->formSpec['multiMode'] !== 'none' && $this->formSpec['multiSql'] === '') {
940
            throw new UserFormException("MultiMode selected, but MultiSQL missing", ERROR_MULTI_SQL_MISSING);
Carsten  Rose's avatar
Carsten Rose committed
941
942
        }

943
944
945
        if ($formMode !== FORM_DELETE) {
            $sipArray = $this->store->getStore(STORE_SIP);
            // Check: requiredParameter: '' or 'form' or 'form,grId' or 'form #formname for form,grId'
946
947
            $requiredParameter = ($r > 0) ? $this->formSpec[F_REQUIRED_PARAMETER_EDIT] : $this->formSpec[F_REQUIRED_PARAMETER_NEW];
            $param = explode(',', $requiredParameter);
948
            foreach ($param AS $name) {
949

950
951
                $name = explode('#', $name, 2);
                $name = trim($name[0]);
952

953
954
955
                if ($name === '') {
                    continue;
                }
956

957
958
959
                if (!isset($sipArray[$name])) {
                    throw new UserFormException("Missing required SIP parameter: $name", ERROR_MISSING_REQUIRED_PARAMETER);
                }
960
961
962
            }
        }

963
        return $sipFound;
964
    }
Carsten  Rose's avatar
Carsten Rose committed
965

966
    /**
967
     * Load record $id from $table and saves them in $store
Carsten  Rose's avatar
Carsten Rose committed
968
     *