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

namespace qfq;

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

//use qfq\Report;

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

Carsten  Rose's avatar
Carsten Rose committed
24

25
26
27
28
29
30
31
32
33
34
35
require_once(__DIR__ . '/store/Store.php');
require_once(__DIR__ . '/store/FillStoreForm.php');
require_once(__DIR__ . '/store/Session.php');
require_once(__DIR__ . '/Constants.php');
require_once(__DIR__ . '/Save.php');
require_once(__DIR__ . '/helper/KeyValueStringParser.php');
require_once(__DIR__ . '/helper/HelperFormElement.php');
require_once(__DIR__ . '/exceptions/UserFormException.php');
require_once(__DIR__ . '/exceptions/CodeException.php');
require_once(__DIR__ . '/exceptions/DbException.php');
require_once(__DIR__ . '/exceptions/ErrorHandler.php');
36
37
require_once(__DIR__ . '/database/Database.php');
require_once(__DIR__ . '/database/DatabaseUpdate.php');
38
39
40
41
42
43
44
require_once(__DIR__ . '/Evaluate.php');
require_once(__DIR__ . '/BuildFormPlain.php');
require_once(__DIR__ . '/BuildFormTable.php');
require_once(__DIR__ . '/BuildFormBootstrap.php');
require_once(__DIR__ . '/report/Report.php');
require_once(__DIR__ . '/BodytextParser.php');
require_once(__DIR__ . '/Delete.php');
45
require_once(__DIR__ . '/form/FormAction.php');
Carsten  Rose's avatar
Carsten Rose committed
46
require_once(__DIR__ . '/form/Dirty.php');
47
require_once(__DIR__ . '/form/DragAndDrop.php');
48

49
50
51
52
53
54
55
56
57
58
59
60
/*
 * Form will be called
 * a) with a SIP identifier, or
 * b) without a SIP identifier (form setting has to allow this) and will create on the fly a new SIP.
 *
 * The SIP-Store stores:
 *  form=<formname>
 *  r=<record id>  (table.id for a single record form)
 *  keySemId,keySemIduser
 *  <further individual variables>
 */

Carsten  Rose's avatar
Carsten Rose committed
61
/**
62
 * Class Qfq
Carsten  Rose's avatar
Carsten Rose committed
63
64
 * @package qfq
 */
65
66
class QuickFormQuery
{
67

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

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

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

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

92
93
94
    /**
     * @var bool
     */
95
96
    private $phpUnit = false;

97
98
99
100
101
    /**
     * @var Session
     */
    private $session = null;

102
103
104
    private $dbIndexData = false;
    private $dbIndexQfq = false;

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

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

134
135
        $this->phpUnit = $phpUnit;

136
        mb_internal_encoding("UTF-8");
137

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

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

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

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

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

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

156
157
        $this->t3data = $t3data;

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

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

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

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

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

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

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

178

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

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

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

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

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

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

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

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

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

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

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

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

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

252
        return $html;
253
254
    }

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

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

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

275

276
    /**
277
     * Process form.
278
279
280
281
282
     * $mode=
     *   FORM_LOAD: The whole form will be rendered as HTML Code, including the values of all form elements
     *   FORM_UPDATE: States and values of all form elements will be returned as JSON.
     *   FORM_SAVE: The submitted form will be saved. Return Failure or Success as JSON.
     *   FORM_DELETE:
283
     *
284
     * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE
285
     *
286
     * @return array|string
287
     * @throws CodeException
288
     * @throws DbException
289
     * @throws DownloadException
290
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
291
     * @throws UserReportException
292
     */
293
294
    private function doForm($formMode)
    {
Carsten  Rose's avatar
Carsten Rose committed
295
        $data = '';
Carsten  Rose's avatar
Carsten Rose committed
296
        $foundInStore = '';
297

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

304
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO);
305
        $this->setParameterLanguageFieldName();
306
307

        $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore);
308
309
310
311
312
313
314
315
316
        if ($formName === false) {
            switch ($formName) {
                case FORM_DELETE:
                    break;
                case FORM_DRAG_AND_DROP:
                    throw new CodeException('Missing form in SIP', ERROR_MISSING_FORM);
                default:
                    return '';// No form found: do nothing
            }
317
        }
318

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

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

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

            $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);
                    }
                }
            }
345
346
        }

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

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

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

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

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

368
369
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
370
371
        }

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

382
        if ($formMode === FORM_DELETE) {
383

384
            $build = new Delete($this->dbIndexData);
385
386

        } else {
387
            $tableDefinition = $this->dbArray[$this->dbIndexData]->getTableDefinition($this->formSpec[F_TABLE_NAME]);
388
            $this->store->fillStoreTableDefaultColumnType($tableDefinition);
389
390
391

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

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

422
423
424
425
426
427
428
429
            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;

430
            case FORM_SAVE:
431
432
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);

433
                // Action: Before
434
435
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_INSERT . ',' . FE_TYPE_BEFORE_UPDATE . ',' . FE_TYPE_BEFORE_SAVE);

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

439
                // SAVE
440
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw);
441
442
443

                $save->processAllImageCutFE();

444
445
                $rc = $save->process();

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

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

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

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

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

464
465
466
467
468
469
470

                $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!
471
                $getJson = true;
472
473
474
475
476
                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);
477
                    $getJson = false;
478
                }
479

480
                if ($getJson) {
481

482
483
                    // 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);
484
485
486
487
                    $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);
488

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

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

499
                $dragAndDrop = new DragAndDrop($this->formSpec);
500
                $dragAndDrop->process($this->formSpec);
501
502
503

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

505
506
507
508
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

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

Carsten  Rose's avatar
Carsten Rose committed
514
        return $data;
515
516
    }

517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533

    /**
     * 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
534
535
     * @throws CodeException
     * @throws DbException
536
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
537
     * @throws UserReportException
538
     */
539
540
    private function setForwardModePage()
    {
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586

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

601
602
603
604
        if (!$this->isPasteRecord()) {
            return;
        }

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

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

    } # doClipboard()

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

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

633
634
635
636
        return false;

    }

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

651
        $formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668

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

    }

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

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

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

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

702
703
704
705
706
707
708
        // 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);
        }

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

714
        $form = $this->modeCleanFormConfig($mode, $form);
715

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

720
721
722
723
724
725
726
        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]);
        }

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

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

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

745
746
747
748
749
750
        // 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);

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

754
755
        $formSpec = $this->syncSystemFormConfig($formSpec);
        $formSpec = $this->initForm($formSpec);
756

Carsten  Rose's avatar
Carsten Rose committed
757
758
        $formSpec = array_merge($formSpec, $parseLater);

759
        // Set F_FINAL_DELETE_FORM
760
        $formSpec[F_FINAL_DELETE_FORM] = ($formSpec[F_EXTRA_DELETE_FORM] != '') ? $formSpec[F_EXTRA_DELETE_FORM] : $formSpec[F_NAME];
761

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

Carsten  Rose's avatar
Carsten Rose committed
776
        // Clear
777
778
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

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

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

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

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

800
801
802
803
            case FORM_DELETE:
                $this->feSpecNative = array();
                break;

804
            default:
805
                break;
806
807
        }

808
        $this->feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName);
809
        $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($this->feSpecNative, $this->formSpec[F_ID], $recordId);
810

811
        return $formName;
Carsten  Rose's avatar
Carsten Rose committed
812
813
    }

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

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

834
835
        $feSpecNative = HelperFormElement::formElementSetDefault($feSpecNative);

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

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

899
900
901
902
        return $new;
    }


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

Carsten  Rose's avatar
Carsten Rose committed
931
932
933
934
935
936
        switch ($mode) {
            case FORM_LOAD:
                $store = STORE_TYPO3;
                break;
            case FORM_SAVE:
            case FORM_UPDATE:
937
            case FORM_DELETE:
938
            case FORM_DRAG_AND_DROP:
939
                $store = STORE_SIP;
Carsten  Rose's avatar
Carsten Rose committed
940
941
942