QuickFormQuery.php 41.6 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
        $forwardPage = $this->eval->parse($this->formSpec[F_FORWARD_PAGE]);
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186

        // Format:   [mode/url][|url]
        $forwardArray = explode('|', $forwardPage, 2);
        switch ($forwardArray[0]) {
            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] = $forwardArray[0];
                if (isset($forwardArray[1])) {
                    $forwardPage = $forwardArray[1];
                }
        }

Carsten  Rose's avatar
Carsten Rose committed
187
188
189
190
        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.
191
            // 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
192
193
194
            $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL_SKIP_HISTORY;
        }

195
196
        return ([
            API_REDIRECT => $this->formSpec[F_FORWARD_MODE],
Carsten  Rose's avatar
Carsten Rose committed
197
            API_REDIRECT_URL => $forwardPage,
198
        ]);
199
200
    }

201
    /**
Carsten  Rose's avatar
Carsten Rose committed
202
     * Main entrypoint for display content: a) form and/or b) report
203
     *
204
     * @return string
Carsten  Rose's avatar
Carsten Rose committed
205
     */
206
    public function process() {
207
        $html = '';
208

209
        if ($this->store->getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === 'yes') {
210
211
            $htmlId = HelperFormElement::buildFormElementId($this->formSpec[F_ID], 0, 0, 0);
            $html .= Support::doTooltip($htmlId . HTML_ID_EXTENSION_TOOLTIP, $this->t3data['bodytext']);
212
213
214
        }

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

217
        // 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
218
219
220
221
        if ($this->store->getVar(SYSTEM_DOWNLOAD_POPUP, STORE_SYSTEM) == DOWNLOAD_POPUP_REQUEST) {
            $html .= $this->getModalCode();
        }

222
        $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
223
        if ($class) {
224
            $html = Support::wrapTag("<div class='$class'>", $html);
Carsten  Rose's avatar
Carsten Rose committed
225
226
        }

227
        return $html;
228
229
    }

230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
    /**
     * 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;
            }
        }
    }

249
    /**
250
     * Process form.
251
252
253
254
255
     * $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:
256
     *
257
     * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE
258
     *
259
     * @return array|string
260
     * @throws CodeException
261
     * @throws UserFormException
262
     */
263
    private function doForm($formMode) {
Carsten  Rose's avatar
Carsten Rose committed
264
        $data = '';
Carsten  Rose's avatar
Carsten Rose committed
265
        $foundInStore = '';
266

Carsten  Rose's avatar
Carsten Rose committed
267
        // Fill STORE_FORM
268
        if ($formMode === FORM_UPDATE || $formMode === FORM_SAVE) {
Carsten  Rose's avatar
Carsten Rose committed
269
270
271
            $fillStoreForm = new FillStoreForm();
            $fillStoreForm->process();
        }
272

273
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO);
274
        $this->setParameterLanguageFieldName();
275
276

        $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore);
277
        if ($formName === false && $formMode !== FORM_DELETE) {
278
            // No form found: do nothing
279
            return '';
280
        }
281

282
283
        if ($formName !== false) {
            // Validate only if there is a 'real' form (not a FORM_DELETE with only a tablename).
284
            $sipFound = $this->validateForm($foundInStore, $formMode);
285
286

        } else {
287
            // FORM_DELETE without a form definition: Fake the form with only a tableName.
288
289
            $table = $this->store->getVar(SIP_TABLE, STORE_SIP);
            if ($table === false) {
290
                throw new UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE);
291
292
293
294
295
296
            }
            $sipFound = true;
            $this->formSpec[F_NAME] = '';
            $this->formSpec[F_TABLE_NAME] = $table;
        }

297
298
        // 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.
299
        if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) {
300
301
            $this->store->createSipAfterFormLoad($formName);
        }
302

303
304
305
306
        if ($this->store->getVar('id', STORE_BEFORE) === false) {
            $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId, STORE_BEFORE);
        }

307
        // Check (and release) dirtyRecord.
Carsten  Rose's avatar
Carsten Rose committed
308
        if ($formMode === FORM_DELETE || $formMode === FORM_SAVE) {
Carsten  Rose's avatar
Carsten Rose committed
309
            $dirty = new Dirty();
Carsten  Rose's avatar
Carsten Rose committed
310

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

314
            // In case of a conflict, return immediately
Carsten  Rose's avatar
Carsten Rose committed
315
316
            if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
                $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
317

318
319
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
320
321
        }

322
323
324
325
326
327
328
329
330
331
        // 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;
            }
        }

332
        if ($formMode === FORM_DELETE) {
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351

            $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);
            }
352
353
        }

Carsten  Rose's avatar
#2067    
Carsten Rose committed
354
        $formAction = new FormAction($this->formSpec, $this->db, $this->phpUnit);
355
        switch ($formMode) {
356
            case FORM_LOAD:
357
358
359
360
361
362
363
364
                $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
365
            case FORM_UPDATE:
366
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
367
368
                // data['form-update']=....
                $data = $build->process($formMode);
369
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
370
                break;
Carsten  Rose's avatar
Carsten Rose committed
371

372
373
374
375
376
377
378
379
            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;

380
            case FORM_SAVE:
381
382
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

383
                // Action: Before
384
385
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_INSERT . ',' . FE_TYPE_BEFORE_UPDATE . ',' . FE_TYPE_BEFORE_SAVE);

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

389
                // SAVE
390
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative);
391
392
                $rc = $save->process();

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

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

398
                // Action: After
399
400
                $status = $formAction->elements($rc, $this->feSpecAction, FE_TYPE_AFTER_INSERT . ',' . FE_TYPE_AFTER_UPDATE . ',' . FE_TYPE_AFTER_SAVE);
                if ($status != ACTION_ELEMENT_NO_CHANGE) {
401
                    // Reload fresh saved record and fill STORE_RECORD with it.
402
                    $this->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, STORE_RECORD);
403
                }
404

Carsten  Rose's avatar
Carsten Rose committed
405
406
407
408
409
410
                // Action: Paste
                $this->pasteClipboard($this->formSpec[F_ID], $formAction);

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

411
                $getJson = true;
412
413
                // Retrieve current STORE_SIP.
                $sipArray = $this->store->getStore(STORE_SIP);
414
                if ($sipArray[SIP_RECORD_ID] == 0 && API_SUBMIT_REASON_SAVE == $this->store->getVar(API_SUBMIT_REASON, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX)) {
415

Carsten  Rose's avatar
Carsten Rose committed
416
417
418
419
                    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
                    ) {
420
                        $this->formSpec = $this->buildNSetReloadUrl($this->formSpec, $rc);
421
422
                    }
                    $getJson = false;
423
                }
424

425
426
427
428
429
430
                if ($getJson) {
                    // Retrieve FE Values as JSON
                    // $data['form-update']=...
//                    $data = $build->process($formMode, $htmlElementNameIdZero);
                    $data = $build->process($formMode);
                }
431
                break;
Carsten  Rose's avatar
Carsten Rose committed
432

433
434
435
436
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

437
        if (is_array($data)) {
438
            // $data['element-update']=...
439
440
            $data = $this->groupElementUpdateEntries($data);
        }
Carsten  Rose's avatar
Carsten Rose committed
441

Carsten  Rose's avatar
Carsten Rose committed
442
        return $data;
443
444
    }

Carsten  Rose's avatar
Carsten Rose committed
445
446
447
    /**
     * Iterate over all Clipboard source records and fire for each all FE.type=paste records.
     *
448
     * @param int        $formId
Carsten  Rose's avatar
Carsten Rose committed
449
     * @param FormAction $formAction
450
     *
Carsten  Rose's avatar
Carsten Rose committed
451
452
453
454
455
456
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     */
    private function pasteClipboard($formId, FormAction $formAction) {

457
458
459
460
        if (!$this->isPasteRecord()) {
            return;
        }

Carsten  Rose's avatar
Carsten Rose committed
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
        $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()

477
478
479
480
481
482
483
484
485
486
    /**
     * @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;
            }
        }
487

488
489
490
491
        return false;

    }

492
493
494
495
496
    /**
     * 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
497
498
     * @param int   $recordId
     *
499
500
501
502
503
504
     * @return array
     * @throws CodeException
     * @throws UserFormException
     */
    private function buildNSetReloadUrl(array $formSpec, $recordId) {

505
        $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522

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

    }

523
    /**
524
     * Load form. Evaluates form. Load FormElements.
525
     *
Carsten  Rose's avatar
Carsten Rose committed
526
     * After processing:
527
528
529
530
     * 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
531
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
532
     * @param int    $recordId
Carsten  Rose's avatar
Carsten Rose committed
533
     * @param string $foundInStore
534
     *
Carsten  Rose's avatar
Carsten Rose committed
535
536
     * @return bool|string if found the formName, else 'false'.
     * @throws CodeException
537
     * @throws DbException
538
     * @throws UserFormException
539
     */
540
    private function loadFormSpecification($mode, $recordId, &$foundInStore = '') {
Carsten  Rose's avatar
Carsten Rose committed
541

542
        // formName
Carsten  Rose's avatar
Carsten Rose committed
543
        if (false === ($formName = $this->getFormName($mode, $foundInStore))) {
544
545
            return false;
        }
546

547
548
549
550
        if (!$this->db->existTable('Form')) {
            throw new UserFormException("Table 'Form' not found", ERROR_MISSING_TABLE);
        }

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

554
555
556
557
558
559
560
        // 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);
        }

561
        // Load form
562
563
        $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,
564
            [$formName], 'Form not found or multiple forms with the same name.');
565

566
        $form = $this->modeCleanFormConfig($mode, $form);
567

Carsten  Rose's avatar
Carsten Rose committed
568
569
570
571
        // Save specific elements to be expanded later.
        $parseLater = OnArray::getArrayItems($form, [F_FORWARD_PAGE]);
        $form[F_FORWARD_PAGE] = '';

572
        $formSpec = $this->eval->parseArray($form);
573
        HelperFormElement::explodeParameter($formSpec, F_PARAMETER);
574
575
576

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

578
579
        $formSpec = $this->syncSystemFormConfig($formSpec);
        $formSpec = $this->initForm($formSpec);
580

Carsten  Rose's avatar
Carsten Rose committed
581
582
        $formSpec = array_merge($formSpec, $parseLater);

583
        // Set F_FINAL_DELETE_FORM
584
        $formSpec[F_FINAL_DELETE_FORM] = ($formSpec[F_EXTRA_DELETE_FORM] != '') ? $formSpec[F_EXTRA_DELETE_FORM] : $formSpec[F_NAME];
585

586
        $this->formSpec = $formSpec;
587

588
589
590
        // 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
591
        // Clear
592
593
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

594
        // FE: Action
595
        $this->feSpecAction = $this->db->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR, ['no', $this->formSpec["id"], 'action']);
596
        HelperFormElement::explodeParameterInArrayElements($this->feSpecAction, FE_PARAMETER);
597
598

        // FE: Native & Container
599
        // "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";
600
        $feSpecNative = array();
601
602
        switch ($mode) {
            case FORM_LOAD:
603
                // Select all Native elements (native, pill, fieldset, templateGroup) which are NOT nested = Root level.
604
                $feSpecNative = $this->db->getNativeFormElements(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER, ['no', $this->formSpec["id"], 'native,container', 0], $this->formSpec);
605
606
607
                break;

            case FORM_SAVE:
Carsten  Rose's avatar
Carsten Rose committed
608
            case FORM_UPDATE:
Carsten  Rose's avatar
Carsten Rose committed
609
//              $this->feSpecNative = $this->db->getNativeFormElements(SQL_FORM_ELEMENT_ALL_CONTAINER, ['no', $this->formSpec["id"], 'native'], $this->formSpec);
610
            $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec["id"]], $this->formSpec);
611
612
                break;

613
614
615
616
            case FORM_DELETE:
                $this->feSpecNative = array();
                break;

617
            default:
618
                break;
619
620
        }

621
        $this->feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName);
622

623
        return $formName;
Carsten  Rose's avatar
Carsten Rose committed
624
625
    }

626
627
    /**
     * Depending on $sql reads FormElements to a specific container or all. Preprocess all FormElements.
628
629
630
631
632
633
     * 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
634
635
636
637
638
639
640
641
642
     *
     * @return array|int
     * @throws \qfq\CodeException
     * @throws \qfq\DbException
     */
    public function getNativeFormElements($sql, array $param, $formSpec) {

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

643
644
        $feSpecNative = HelperFormElement::formElementSetDefault($feSpecNative);

645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
        // 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;
    }

    /**
661
662
663
     * 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.
664
     *
665
666
667
     * 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).
668
669
670
671
     *
     * Attention: The resulting order of the FormElements, is not the same as on the Form during FormLoad!
     *
     * @param array $elements
672
     *
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
     * @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;
            }
        }
701

702
703
704
705
        return $new;
    }


Carsten  Rose's avatar
Carsten Rose committed
706
    /**
707
708
     * Get the formName from STORE_TYPO3 (bodytext), STORE_SIP or by STORE_CLIENT (URL).
     *
709
710
711
712
     * 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
713
714
715
     *   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
716
717
718
719
720
     *
     * FORM_SAVE:
     *   Specified in SIP
     *
     *
Carsten  Rose's avatar
Carsten Rose committed
721
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
722
     * @param string $foundInStore
723
     *
724
     * @return bool|string  Formname (Form.name) or FALSE (if no formname found)
Carsten  Rose's avatar
Carsten Rose committed
725
     * @throws CodeException
726
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
727
     */
728
    public function getFormName($mode, &$foundInStore = '') {
729
        $dummy = array();
Carsten  Rose's avatar
Carsten Rose committed
730

Carsten  Rose's avatar
Carsten Rose committed
731
732
733
734
735
736
        switch ($mode) {
            case FORM_LOAD:
                $store = STORE_TYPO3;
                break;
            case FORM_SAVE:
            case FORM_UPDATE:
737
738
            case FORM_DELETE:
                $store = STORE_SIP;
Carsten  Rose's avatar
Carsten Rose committed
739
740
741
742
743
                break;
            default:
                throw new CodeException("Unknown mode: $mode.", ERROR_UNKNOWN_MODE);
        }

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

747
748
749
750
//        if($mode===FORM_DELETE && $formName===false) {
//            return "";
//        }

751
        // If the formname is '': no formname name.
752
        if ($formName === '' || $foundInStore === '')
753
754
755
756
757
758
            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;
        }
759

760
        return $formName;
761
    }
Carsten  Rose's avatar
Carsten Rose committed
762

763
764
765
766
    /**
     * Depending on $mode various formSpec fields might be adjusted.
     * E.g.: the form title is not important during a delete.
     *
767
     * @param string $mode
768
769
     * @param array  $form
     *
770
771
     * @return array
     */
772
    private function modeCleanFormConfig($mode, array $form) {
773
774
775
776
777
778
779
780
781
782
783
784

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

        return $form;
    }

785
    /**
786
787
788
789
790
     * 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
791
     * corresponding input field.
792
     *
793
     * @param array $formSpec
794
     *
795
796
     * @return array
     */
797
798
    private function syncSystemFormConfig(array $formSpec) {

799
800
        $keys = [F_BS_COLUMNS,
            F_BS_LABEL_COLUMNS,
801
802
803
804
805
806
807
808
809
            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,
810
            F_BUTTON_ON_CHANGE_CLASS,
811
            F_ESCAPE_TYPE_DEFAULT,
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
            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,

832
833
            F_RECORD_LOCK_TIMEOUT_SECONDS,

834
            FE_INPUT_EXTRA_BUTTON_INFO_CLASS,
835
836
837
838
        ];

        // 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.
839
        foreach ([F_BS_LABEL_COLUMNS, F_BS_INPUT_COLUMNS, F_BS_NOTE_COLUMNS, F_ESCAPE_TYPE_DEFAULT] as $key) {
840
841
842
843
            if ($formSpec[$key] == '') {
                unset ($formSpec[$key]);
            }
        }
844

845
        foreach ($keys as $key) {
846

847
848
849
850
851
852
853
            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);
            }
        }
854

855
856
        return $formSpec;
    }
857

858
859
860
    /**
     * Set form parameter which are expected to exist.
     *
Carsten  Rose's avatar
Carsten Rose committed
861
     * @param array $formSpec
862
     *
863
864
     * @return array
     */
Carsten  Rose's avatar
Carsten Rose committed
865
866
867
868
869
870
871
    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, '');

872
873
874
875
876
877
        // 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;
            }
878
879
        }

Carsten  Rose's avatar
Carsten Rose committed
880
881
882
883
        if ($formSpec[F_MODE] == F_MODE_READONLY) {
            $formSpec[F_SHOW_BUTTON] = FORM_BUTTON_CLOSE;
            $formSpec[F_SUBMIT_BUTTON_TEXT] = '';
        }
884

885
        if ($formSpec[F_ESCAPE_TYPE_DEFAULT] == TOKEN_ESCAPE_CONFIG) {
886
887
888
            $formSpec[F_ESCAPE_TYPE_DEFAULT] = $this->store->getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
        }

Carsten  Rose's avatar
Carsten Rose committed
889
        return $formSpec;
890
891
    }

892
    /**
Carsten  Rose's avatar
Carsten Rose committed
893
     * Check if loading of the given form is permitted. If not, throw an exception.
894
     *
Carsten  Rose's avatar
Carsten Rose committed
895
896
     * @param string $formNameFoundInStore
     * @param string $formMode
897
     *
Carsten  Rose's avatar
Carsten Rose committed
898
     * @return bool 'true' if SIP exists, else 'false'
Carsten  Rose's avatar
Carsten Rose committed
899
900
     * @throws \qfq\CodeException
     * @throws \qfq\UserFormException
Carsten  Rose's avatar
Carsten Rose committed
901
     * @internal param $foundInStore
902
     */
903
    private function validateForm($formNameFoundInStore, $formMode) {
904
905

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

908
        // If not found: Fake a definition in STORE_TYPO3.
909
        if ($r === false) {
910
911
912
            $r = 0;
            $this->store->setVar(TYPO3_RECORD_ID, $r, STORE_TYPO3);
            $recordIdFoundInStore = STORE_TYPO3;
913
914
        }

Carsten  Rose's avatar
Carsten Rose committed
915
        // If there is a record_id>0: EDIT else NEW: 'sip','logged_in','logged_out','always','never'
916
        $permitMode = ($r > 0) ? $this->formSpec['permitEdit'] : $this->formSpec['permitNew'];
917
918
919
920
921

        $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
922
923
        if ($sipFound) {
            if (($formNameFoundInStore === STORE_CLIENT) || ($recordIdFoundInStore === STORE_CLIENT)) {
924
                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
925
926
927
            }
        }

928
        switch ($permitMode) {
929
            case  FORM_PERMISSION_SIP:
Carsten  Rose's avatar
Carsten Rose committed
930
                if (!$sipFound || $formNameFoundInStore !== STORE_SIP || $recordIdFoundInStore !== STORE_SIP) {
931
                    throw new UserFormException("SIP Parameter needed for this form.", ERROR_SIP_NEEDED_FOR_THIS_FORM);
932
933
934
935
                }
                break;
            case  FORM_PERMISSION_LOGGED_IN:
                if (!$feUserLoggedIn) {
936
                    throw new UserFormException("User not logged in.", ERROR_USER_NOT_LOGGED_IN);
937
938
939
940
                }
                break;
            case FORM_PERMISSION_LOGGED_OUT:
                if ($feUserLoggedIn) {
941
                    throw new UserFormException("User logged in.", ERROR_USER_LOGGED_IN);
942
943
944
945
946
                }
                break;
            case FORM_PERMISSION_ALWAYS:
                break;
            case FORM_PERMISSION_NEVER:
947
                throw new UserFormException("Loading form forbidden.", ERROR_FORM_FORBIDDEN);
948
            default:
949
                throw new CodeException("Unknown permission mode: '" . $permitMode . "'", ERROR_FORM_UNKNOWN_PERMISSION_MODE);
950
        }
Carsten  Rose's avatar
Carsten Rose committed
951

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

957
958
959
        if ($formMode !== FORM_DELETE) {
            $sipArray = $this->store->getStore(STORE_SIP);
            // Check: requiredParameter: '' or 'form' or 'form,grId' or 'form #formname for form,grId'
960
961
            $requiredParameter = ($r > 0) ? $this->formSpec[F_REQUIRED_PARAMETER_EDIT] : $this->formSpec[F_REQUIRED_PARAMETER_NEW];
            $param = explode(',', $requiredParameter);
962
            foreach ($param AS $name) {
963

964
965
                $name = explode('#', $name, 2);
                $name = trim($name[0]);
966

967
968
969
                if ($name === '') {
                    continue;
                }
970

971
972
973
                if (!isset($sipArray[$name])) {
                    throw new UserFormException("Missing required SIP parameter: $name", ERROR_MISSING_REQUIRED_PARAMETER);
                }
974
975
976
            }
        }