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

Carsten  Rose's avatar
Carsten Rose committed
9

10
11
namespace qfq;

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

//use qfq\Report;

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

Carsten  Rose's avatar
Carsten Rose committed
25

26
require_once(__DIR__ . '/../qfq/store/Store.php');
Carsten  Rose's avatar
Carsten Rose committed
27
require_once(__DIR__ . '/../qfq/store/FillStoreForm.php');
Carsten  Rose's avatar
Carsten Rose committed
28
require_once(__DIR__ . '/../qfq/Constants.php');
29
require_once(__DIR__ . '/../qfq/Save.php');
Carsten  Rose's avatar
Carsten Rose committed
30
require_once(__DIR__ . '/../qfq/helper/KeyValueStringParser.php');
31
require_once(__DIR__ . '/../qfq/helper/HelperFormElement.php');
32
require_once(__DIR__ . '/../qfq/exceptions/UserFormException.php');
33
34
require_once(__DIR__ . '/../qfq/exceptions/CodeException.php');
require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
35
require_once(__DIR__ . '/../qfq/exceptions/ErrorHandler.php');
36
require_once(__DIR__ . '/../qfq/Database.php');
Carsten  Rose's avatar
Carsten Rose committed
37
require_once(__DIR__ . '/../qfq/Evaluate.php');
38
39
40
require_once(__DIR__ . '/../qfq/BuildFormPlain.php');
require_once(__DIR__ . '/../qfq/BuildFormTable.php');
require_once(__DIR__ . '/../qfq/BuildFormBootstrap.php');
41
require_once(__DIR__ . '/../qfq/report/Report.php');
42
require_once(__DIR__ . '/../qfq/BodytextParser.php');
43

44
45
46
47
48
49
50
51
52
53
54
55
/*
 * 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
56
/**
57
 * Class Qfq
Carsten  Rose's avatar
Carsten Rose committed
58
59
 * @package qfq
 */
60
class QuickFormQuery {
61
    /**
62
     * @var \qfq\Store instantiated class
63
     */
Carsten  Rose's avatar
Carsten Rose committed
64
    protected $store = null;
65
66
67
    /**
     * @var Database instantiated class
     */
68
    protected $db = null;
69
70
71
72
    /**
     * @var Evaluate instantiated class
     */
    protected $eval = null;
73
74
75
76
77
78
79
    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
    /**
     * @var array
     */
    private $t3data = array(); // FormEelement Definition: all formElement.class='native' of the loaded form
80

81
82
    private $phpUnit = false;

83
84
85
86
87
88
89
90
91
92
93
    /*
     * 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'
     */
94

95
96
97
98
99
    /**
     * Construct the Form Class and Store too. This is the base initialization moment.
     *
     * 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.
100
     *
101
     * @param string $bodytext
102
     */
103
104
    public function __construct(array $t3data = array(), $phpUnit = false) {

105
106
        $this->phpUnit = $phpUnit;

107
        mb_internal_encoding("UTF-8");
108

Carsten  Rose's avatar
Carsten Rose committed
109
110
111
112
113
114
115
116
117
118
        // session.cache_expire
        // session.cookie_lifetime
        // session.gc_maxlifetime

//        $arr1['sessionname'] =   session_name();
//        $arr1['session.auto_start']  = ini_get('session.auto_start');
//        $arr1['session.gc_maxlifetime']  = ini_get('session.gc_maxlifetime');
//        $arr1['session.cookie_lifetime']  = ini_get('session.cookie_lifetime');
//        $arr1['session.name']  = ini_get('session.name');

119
120
121
        // Refresh the session even if no new data saved.
        $_SESSION['LAST_ACTIVITY'] = time();

122
        set_error_handler("\\qfq\\ErrorHandler::exception_error_handler");
123

124
125
126
127
128
        if (!isset($t3data['bodytext']))
            $t3data['bodytext'] = '';
        if (!isset($t3data['uid']))
            $t3data['uid'] = 0;

129
130
131
        $btp = new BodytextParser();
        $t3data['bodytext'] = $btp->process($t3data['bodytext']);

132
133
        $this->t3data = $t3data;

134
135
136
        $bodytext = $this->t3data['bodytext'];

        $this->store = Store::getInstance($bodytext, $phpUnit);
137
        $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data['uid'], STORE_TYPO3);
138
139
        $this->db = new Database();
        $this->eval = new Evaluate($this->store, $this->db);
Carsten  Rose's avatar
Carsten Rose committed
140
141
    }

142
    /**
143
144
     * Returns the defined forwardMode and set, if necessary, $forwardPage
     *
145
146
147
148
149
150
151
152
     * @param $forwardPage
     * @return mixed
     */
    public function getForwardMode(&$forwardPage) {
        $forwardPage = $this->formSpec['forwardPage'];
        return $this->formSpec['forwardMode'];
    }

153
    /**
154
155
     * Main entrypoint for display content: form or report
     *
156
     * @return string
Carsten  Rose's avatar
Carsten Rose committed
157
     */
158
    public function process() {
159
        $html = '';
160

161
        if ($this->store->getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === '1') {
162
163
164
165
            $html .= Support::appendTooltip('', $this->t3data['bodytext']);
        }

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

168
169
170
171
        $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
        if ($class)
            $html = Support::wrapTag("<div class='$class'>", $html);

172
        return $html;
173
174
    }

175
    /**
176
177
178
179
     * Process form.
     * $mode=FORM_LOAD: The whole form will be rendered as HTML Code, including the values of all form elements
     * $mode=FORM_UPDATE: States and values of all form elements will be returned as JSON.
     * $mode=FORM_SAVE: The submitted form will be saved. Return Failure or Success as JSON.
180
     *
181
     * @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
182
     * @return array|string
183
     * @throws CodeException
184
     * @throws UserFormException
185
     */
Carsten  Rose's avatar
Carsten Rose committed
186
187
    private function doForm($mode) {
        $data = '';
Carsten  Rose's avatar
Carsten Rose committed
188
        $foundInStore = '';
189

Carsten  Rose's avatar
Carsten Rose committed
190
191
192
193
194
        // Fill STORE_FORM
        if ($mode === FORM_UPDATE || $mode === FORM_SAVE) {
            $fillStoreForm = new FillStoreForm();
            $fillStoreForm->process();
        }
195

Carsten  Rose's avatar
Carsten Rose committed
196
        $formName = $this->loadFormSpecification($mode, $foundInStore);
197
198
199
        if ($formName === false)
            return '';

Carsten  Rose's avatar
Carsten Rose committed
200
        $sipFound = $this->validateForm($foundInStore);
201
202
203
204
205
        if (!$sipFound) {
            $this->store->createSipAfterFormLoad($formName);
        }
        $this->store->fillStoreTableDefaultColumnType($this->formSpec['tableName']);

206
207
208
209
210
211
212
213
214
215
216
217
218
219
        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);
        }

220
221
        switch ($mode) {
            case FORM_LOAD:
Carsten  Rose's avatar
Carsten Rose committed
222
223
224
            case FORM_UPDATE:

                $data = $build->process($mode);
225
                break;
Carsten  Rose's avatar
Carsten Rose committed
226

227
228
229
            case FORM_SAVE:
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative);
                $save->process();
230
231
232

                // Retrieve FE Values as JSON
                $data = $build->process($mode);
233
                break;
Carsten  Rose's avatar
Carsten Rose committed
234

235
236
237
238
            default:
                throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

Carsten  Rose's avatar
Carsten Rose committed
239
        return $data;
240
241
    }

242
    /**
243
     * Load form. Evaluates form. Load FormElements.
244
     *
Carsten  Rose's avatar
Carsten Rose committed
245
     * After processing:
246
247
248
249
     * 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
250
251
252
253
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
     * @param string $foundInStore
     * @return bool|string if found the formName, else 'false'.
     * @throws CodeException
254
     * @throws DbException
255
     * @throws UserFormException
256
     */
Carsten  Rose's avatar
Carsten Rose committed
257
    private function loadFormSpecification($mode, &$foundInStore = '') {
Carsten  Rose's avatar
Carsten Rose committed
258

259
        // formName
Carsten  Rose's avatar
Carsten Rose committed
260
        if (false === ($formName = $this->getFormName($mode, $foundInStore))) {
261
262
            return false;
        }
263
        $this->store->setVar(SYSTEM_FORM, $formName, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
264

265
266
267
268
269
270
271
        // 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);
        }

272
273
274
        // Load form
        $form = $this->db->sql("SELECT * FROM Form AS f WHERE f.name LIKE ? AND f.deleted='no'", ROW_EXPECT_1,
            [$formName], 'Form not found or multiple forms with the same name.');
275
276

        $this->formSpec = $this->eval->parseArray($form);
277
        HelperFormElement::explodeParameter($this->formSpec);
278

Carsten  Rose's avatar
Carsten Rose committed
279
280
281
282
        # Set defaults:
        if (!isset($this->formSpec['class']))
            $this->formSpec['class'] = '';

Carsten  Rose's avatar
Carsten Rose committed
283
        // Clear
284
285
        $this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);

286
        // FE: Action
287
288
        $this->feSpecAction = $this->eval->parseArray($this->db->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR,
            ['no', $this->formSpec["id"], 'action']));
289
        HelperFormElement::explodeParameterInArrayElements($this->feSpecAction);
290
291

        // FE: Native & Container
292
293
294
        // "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";
        switch ($mode) {
            case FORM_LOAD:
295
296
                $this->feSpecNative = $this->db->sql(SQL_FORM_ELEMENT_SPECIFIC_CONTAINER, ROW_REGULAR,
                    ['no', $this->formSpec["id"], 'native,container', 0]);
297
298
299
                break;

            case FORM_SAVE:
Carsten  Rose's avatar
Carsten Rose committed
300
            case FORM_UPDATE:
301
302
                $this->feSpecNative = $this->db->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR,
                    ['no', $this->formSpec["id"], 'native']);
303
304
305
306
307
                break;

            default:
        }

308
        HelperFormElement::explodeParameterInArrayElements($this->feSpecNative);
309
310

        return $formName;
Carsten  Rose's avatar
Carsten Rose committed
311
312
    }

Carsten  Rose's avatar
Carsten Rose committed
313
    /**
314
315
     * Get the formName from STORE_TYPO3 (bodytext), STORE_SIP or by STORE_CLIENT (URL).
     *
316
317
318
319
320
321
322
323
324
325
326
327
     * 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
     *   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
     *
     * FORM_SAVE:
     *   Specified in SIP
     *
     *
Carsten  Rose's avatar
Carsten Rose committed
328
     * @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
Carsten  Rose's avatar
Carsten Rose committed
329
     * @param string $foundInStore
Carsten  Rose's avatar
Carsten Rose committed
330
331
     * @return array|bool|mixed|null|string  Formname (Form.name) or FALSE, if no formname found.
     * @throws CodeException
332
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
333
     */
Carsten  Rose's avatar
Carsten Rose committed
334
    private function getFormName($mode, &$foundInStore = '') {
335
        $dummy = array();
Carsten  Rose's avatar
Carsten Rose committed
336

Carsten  Rose's avatar
Carsten Rose committed
337
338
339
340
341
342
343
344
345
346
347
348
        switch ($mode) {
            case FORM_LOAD:
                $store = STORE_TYPO3;
                break;
            case FORM_SAVE:
            case FORM_UPDATE:
                $store = STORE_SIP;
                break;
            default:
                throw new CodeException("Unknown mode: $mode.", ERROR_UNKNOWN_MODE);
        }

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

352
353
354
355
356
357
358
359
        // If the formname is '': no formname name.
        if ($formName === '')
            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;
        }
360

361
        return $formName;
362
    }
Carsten  Rose's avatar
Carsten Rose committed
363

364
    /**
Carsten  Rose's avatar
Carsten Rose committed
365
     * Check if loading of the given form is permitted. If not, throw an exception.
366
     *
Carsten  Rose's avatar
Carsten Rose committed
367
368
     * @param $formNameFoundInStore
     * @return bool 'true' if SIP exists, else 'false'
369
     * @throws CodeException
370
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
371
     * @internal param $foundInStore
372
     */
Carsten  Rose's avatar
Carsten Rose committed
373
    private function validateForm($formNameFoundInStore) {
374
375

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

Carsten  Rose's avatar
Carsten Rose committed
378
        // If there is a record_id>0: EDIT else NEW: 'sip','logged_in','logged_out','always','never'
379
        $permitMode = ($r > 0) ? $this->formSpec['permitEdit'] : $this->formSpec['permitNew'];
380
381
382
383
384

        $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
385
386
        if ($sipFound) {
            if (($formNameFoundInStore === STORE_CLIENT) || ($recordIdFoundInStore === STORE_CLIENT)) {
387
                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
388
389
390
            }
        }

391
        switch ($permitMode) {
392
            case  FORM_PERMISSION_SIP:
Carsten  Rose's avatar
Carsten Rose committed
393
                if (!$sipFound || $formNameFoundInStore !== STORE_SIP || $recordIdFoundInStore !== STORE_SIP) {
394
                    throw new UserFormException("SIP Parameter needed for this form.", ERROR_SIP_NEEDED_FOR_THIS_FORM);
395
396
397
398
                }
                break;
            case  FORM_PERMISSION_LOGGED_IN:
                if (!$feUserLoggedIn) {
399
                    throw new UserFormException("User not logged in.", ERROR_USER_NOT_LOGGED_IN);
400
401
402
403
                }
                break;
            case FORM_PERMISSION_LOGGED_OUT:
                if ($feUserLoggedIn) {
404
                    throw new UserFormException("User logged in.", ERROR_USER_LOGGED_IN);
405
406
407
408
409
                }
                break;
            case FORM_PERMISSION_ALWAYS:
                break;
            case FORM_PERMISSION_NEVER:
410
                throw new UserFormException("Loading form forbidden.", ERROR_FORM_FORBIDDEN);
411
            default:
412
                throw new CodeException("Unknown permission mode: '" . $permitMode . "'", ERROR_FORM_UNKNOWN_PERMISSION_MODE);
413
        }
Carsten  Rose's avatar
Carsten Rose committed
414

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

420
        return $sipFound;
421
    }
Carsten  Rose's avatar
Carsten Rose committed
422

423
    /**
424
425
     * Process the SQL Queries from bodytext. Return the output.
     *
426
427
     * @return string
     */
428
    private function doReport() {
429
        $report = new Report($this->t3data, $this->store->getVar(SYSTEM_SESSION_NAME, STORE_SYSTEM), $this->eval, $this->phpUnit);
430

431
        $html = $report->process();
432
433
434
435
436

        return $html;

    }

437
    /**
438
439
     * Save the current form.
     *
440
441
442
443
     * @return string
     */
    public function saveForm() {

444
        $json = $this->doForm(FORM_SAVE);
445

446
        return $json;
447
448
    }

Carsten  Rose's avatar
Carsten Rose committed
449
450
451
452
453
454
455
456
457
458
459
460
461
    /**
     * Update FormElements and form values. Receives the current form values via POST.
     *
     * @return array
     * @throws CodeException
     */
    public function updateForm() {

        $json = $this->doForm(FORM_UPDATE);

        return $json;
    }

462
463
464
465
466
    /**
     * Delete a record (tablename and recordid are given) or process a 'delete form'
     *
     * @throws CodeException
     * @throws DbException
467
     * @throws UserFormException
468
469
470
471
472
473
474
475
476
     */
    public function delete() {

        #TODO: implement 'delete form'

        // simple delete: table and recordId are given
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
        $table = $this->store->getVar(SIP_TABLE, STORE_SIP);

Carsten  Rose's avatar
Carsten Rose committed
477
        if ($recordId === false || $recordId < 1 || $table === false || $table === '') {
478
            throw new UserFormException("Invalid or missing parameter: recordId=$recordId, table=$table", ERROR_INVALID_OR_MISSING_PARAMETER);
479
480
        }

Carsten  Rose's avatar
Carsten Rose committed
481
        $this->db->sql("DELETE FROM $table WHERE id = ? LIMIT 1", ROW_REGULAR, [$recordId]);
482
483
    }

484
}