Store.php 33.5 KB
Newer Older
1
2
3
4
5
6
7
8
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/1/16
 * Time: 6:51 PM
 */

Marc Egger's avatar
Marc Egger committed
9
namespace IMATHUZH\Qfq\Core\Store;
10

11
use IMATHUZH\Qfq\Core\Database\Database;
Marc Egger's avatar
Marc Egger committed
12
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
13
use IMATHUZH\Qfq\Core\Helper\Logger;
Marc Egger's avatar
Marc Egger committed
14
15
16
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
17
18
19
20
21
22
23
24
25
26

/*
 * Stores:
 * - SIP
 * - webVar
 * - record
 * - form
 * - formElement
 */

Carsten  Rose's avatar
Carsten Rose committed
27
28
/**
 * Class Store
29
 * @package qfq
Carsten  Rose's avatar
Carsten Rose committed
30
 */
31
32
class Store {

Carsten  Rose's avatar
Carsten Rose committed
33
34
35
    /**
     * @var Store Instance of class Store. There should only be one class 'Store' at a time.
     */
36
37
    private static $instance = null;

Carsten  Rose's avatar
Carsten Rose committed
38
39
40
    /**
     * @var Sip Instance of class SIP
     */
41
42
    private static $sip = null;

43
44
45
    /**
     * @var Session Instance of class Session
     */
46
//    private static $session = null;
47

Carsten  Rose's avatar
Carsten Rose committed
48
49
50
51
52
53
54
55
56
57
    /**
     * @var array Stores all indiviudal stores with the variable raw values
     *
     * $raw['D']['id'] = 0  - Defaultvalues from Tabledefinition
     * ...
     * $raw['S']['r'] = 1234 - record ID from current SIP identifier
     * ...
     * $raw['C']['HTTP_SERVER'] = 'qfq' - Servername
     * $raw['C']['s'] = 'badcaffee1234' - recent SIP
     */
58
    private static $raw = array();
Carsten  Rose's avatar
Carsten Rose committed
59
60

    /**
Carsten  Rose's avatar
Carsten Rose committed
61
     * @var array Default sanitize classes.
Carsten  Rose's avatar
Carsten Rose committed
62
     */
Carsten  Rose's avatar
Carsten Rose committed
63
    private static $sanitizeClass = array();
Carsten  Rose's avatar
Carsten Rose committed
64
65

    /**
Carsten  Rose's avatar
Carsten Rose committed
66
67
     * $sanitizeClass['S'] = false
     * $sanitizeClass['C'] = true
Carsten  Rose's avatar
Carsten Rose committed
68
69
     * ...
     *
Carsten  Rose's avatar
Carsten Rose committed
70
     * @var array each entry with true/false - depending if store needs to be sanitized.
Carsten  Rose's avatar
Carsten Rose committed
71
     */
Carsten  Rose's avatar
Carsten Rose committed
72
    private static $sanitizeStore = array();
73

Carsten  Rose's avatar
Carsten Rose committed
74
    private static $phpUnit = false;
75

76

77
    /**
78
     * @param string $bodytext
79
     * @param string $fileConfigPhp
Carsten  Rose's avatar
Carsten Rose committed
80
     *
Marc Egger's avatar
Marc Egger committed
81
82
83
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
84
     */
85
    private function __construct($bodytext = '', $fileConfigPhp = '') {
86

87
//        self::$session = Session::getInstance(self::$phpUnit);
88

89
        // This check is critical for some unwanted exception recursion during startup.
Carsten  Rose's avatar
Carsten Rose committed
90
        if (!function_exists('normalizer_normalize')) {
Marc Egger's avatar
Marc Egger committed
91
            throw new \CodeException("Function normalizer_normalize() not found - Please install 'php-intl'", ERROR_MISSING_INTL);
92
93
        }

Carsten  Rose's avatar
Carsten Rose committed
94
95
96
97
98
99
100
101
        self::$sanitizeClass = [
//            TYPO3_DEBUG_LOAD => SANITIZE_ALLOW_DIGIT,
//            TYPO3_DEBUG_SAVE => SANITIZE_ALLOW_DIGIT,
//            TYPO3_FORM => SANITIZE_ALLOW_ALNUMX,
//            TYPO3_FE_USER => SANITIZE_ALLOW_ALNUMX,
//            TYPO3_FE_USER_UID => SANITIZE_ALLOW_DIGIT,
//            TYPO3_FE_USER_GROUP => SANITIZE_ALLOW_ALNUMX,

Carsten  Rose's avatar
Carsten Rose committed
102
103
104
105
            CLIENT_SIP => SANITIZE_ALLOW_ALNUMX,
            CLIENT_TYPO3VARS => SANITIZE_ALLOW_ALNUMX,
            CLIENT_RECORD_ID => SANITIZE_ALLOW_DIGIT,
            CLIENT_KEY_SEM_ID => SANITIZE_ALLOW_DIGIT,
Carsten  Rose's avatar
Carsten Rose committed
106
            CLIENT_KEY_SEM_ID_USER => SANITIZE_ALLOW_DIGIT,
Carsten  Rose's avatar
Carsten Rose committed
107
108
109
110
            CLIENT_PAGE_ID => SANITIZE_ALLOW_DIGIT,
            CLIENT_PAGE_TYPE => SANITIZE_ALLOW_DIGIT,
            CLIENT_PAGE_LANGUAGE => SANITIZE_ALLOW_DIGIT,
            CLIENT_FORM => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
111
112

            // Part of $_SERVER. Missing vars must be requested individual with the needed sanitize class.
Carsten  Rose's avatar
Carsten Rose committed
113
114
115
            CLIENT_SCRIPT_URL => SANITIZE_ALLOW_ALNUMX,
            CLIENT_SCRIPT_URI => SANITIZE_ALLOW_ALNUMX,
            CLIENT_HTTP_HOST => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
116
            CLIENT_HTTP_USER_AGENT => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
117
            CLIENT_SERVER_NAME => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
118
            CLIENT_SERVER_ADDRESS => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
119
            CLIENT_SERVER_PORT => SANITIZE_ALLOW_DIGIT,
Carsten  Rose's avatar
Carsten Rose committed
120
121
            CLIENT_REMOTE_ADDRESS => SANITIZE_ALLOW_ALNUMX,
            CLIENT_REQUEST_SCHEME => SANITIZE_ALLOW_ALNUMX,
122
            CLIENT_REQUEST_METHOD => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
123
            CLIENT_SCRIPT_FILENAME => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
124
125
126
127
            CLIENT_QUERY_STRING => SANITIZE_ALLOW_ALL,
            CLIENT_REQUEST_URI => SANITIZE_ALLOW_ALL,
            CLIENT_SCRIPT_NAME => SANITIZE_ALLOW_ALNUMX,
            CLIENT_PHP_SELF => SANITIZE_ALLOW_ALNUMX,
128
//            CLIENT_UPLOAD_FILENAME => SANITIZE_ALLOW_ALLBUT,
Carsten  Rose's avatar
Carsten Rose committed
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

//            SYSTEM_DBUSER => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_DBSERVER => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_DBPW => SANITIZE_ALLOW_ALL,
//            SYSTEM_DB => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_TESTDB => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_SESSIONNAME => SANITIZE_ALLOW_ALNUMX,
//            SYSTEM_DBH => SANITIZE_ALLOW_ALL,

//            SYSTEM_SQL_RAW => SANITIZE_ALLOW_ALL,
//            SYSTEM_SQL_FINAL => SANITIZE_ALLOW_ALL,
//            SYSTEM_SQL_COUNT => SANITIZE_ALLOW_DIGIT,
//            SYSTEM_SQL_PARAM_ARRAY => SANITIZE_ALLOW_ALL,

//            SIP_SIP => SANITIZE_ALLOW_ALNUMX,
//            SIP_RECORD_ID => SANITIZE_ALLOW_DIGIT,
//            SIP_FORM => SANITIZE_ALLOW_ALNUMX,
//            SIP_URLPARAM => SANITIZE_ALLOW_ALL
147

148
149
        ];

Carsten  Rose's avatar
Carsten Rose committed
150
        self::$sanitizeStore = [
Carsten  Rose's avatar
Carsten Rose committed
151
152
153
154
155
156
157
158
159
160
161
162
163
164
            STORE_FORM => true,
            STORE_SIP => false,
            STORE_RECORD => false,
            STORE_BEFORE => false,
            STORE_PARENT_RECORD => false,
            STORE_TABLE_DEFAULT => false,
            STORE_TABLE_COLUMN_TYPES => false,
            STORE_CLIENT => true,
            STORE_TYPO3 => false,
            STORE_VAR => false,
            STORE_ZERO => false,
            STORE_EMPTY => false,
            STORE_SYSTEM => false,
            STORE_EXTRA => false,
165
            STORE_USER => false,
Carsten  Rose's avatar
Carsten Rose committed
166
            STORE_LDAP => false,
Carsten  Rose's avatar
Carsten Rose committed
167
            STORE_ADDITIONAL_FORM_ELEMENTS => false,
168
169
        ];

170
        self::fillStoreTypo3($bodytext);  // should be filled before fillStoreSystem() to offer T3 variables, especially 'feUser'
171
        self::fillStoreClient();  // should be filled before fillStoreSystem() to offer Client variables
172
173
        self::fillStorePhpSession(STORE_EXTRA); // should be filled before fillStoreSystem() to restore variables like SYSTEM_SHOW_DEBUG_INFO
        self::fillStorePhpSession(STORE_USER); // should be filled before fillStoreSystem() to restore variables like SYSTEM_SHOW_DEBUG_INFO
174
        self::fillStoreSystem($fileConfigPhp);
175
        self::fillStoreSip();
176
    }
177

178
    /**
179
     * Returns a pointer to this Class.
Carsten  Rose's avatar
Carsten Rose committed
180
     *
181
182
     * @param string $bodytext
     * @param bool|false $phpUnit
183
     * @param string $qfqConfigPhpUnit
184
185
     *
     * @return null|Store
Marc Egger's avatar
Marc Egger committed
186
187
188
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
189
     */
190
    public static function getInstance($bodytext = '', $phpUnit = false, $qfqConfigPhpUnit = '') {
191

192
193
194
195
196
        if (defined('PHPUNIT_QFQ')) {
            self::$phpUnit = true;
        }

        if (self::$phpUnit) {
197

198
199
200
            // If $qfqConfigPhpUnit is given: clean STORE
//            if (self::$instance !== null && $qfqConfigPhpUnit != '') {
            if ($qfqConfigPhpUnit != '') {
Carsten  Rose's avatar
Carsten Rose committed
201

202
203
                if ($qfqConfigPhpUnit == 'init') {
                    $qfqConfigPhpUnit = '';
Carsten  Rose's avatar
Carsten Rose committed
204
205
206
207
208
209
210
211
                }
                // fake to have a clean environment for the next test.
                self::unsetStore(STORE_TYPO3);
                self::fillStoreTypo3($bodytext);

                self::unsetStore(STORE_CLIENT);
                self::fillStoreClient();
            }
212

213
            // Testing different config files means initialize completely
214
            if ($qfqConfigPhpUnit != '') {
215
216
217
218
219
220
                self::$instance = null;
            }
        }

        // Design Pattern: Singleton
        if (self::$instance === null) {
221
//            self::$phpUnit = $phpUnit;
222

223
            self::$instance = new self($bodytext, $qfqConfigPhpUnit);
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
        } else {
            // Class Store seems to be persistent over multiple QFQ instantiation. Set bodytext again, with every new request (if bodytext is given).
            if ($bodytext !== '') {
                self::fillStoreTypo3($bodytext);
                self::unsetStore(STORE_RECORD); // At least in Multi DB Setup: STORE_RECORD of former  tt-content QFQ Records interfere with loading a form.
            }
        }

        // Disable TYPO3_DEBUG_SHOW_BODY_TEXT=1 if SYSTEM_SHOW_DEBUG_INFO!='yes'
        if (self::getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === '1' &&
            !Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, self::getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))
        ) {
            self::setVar(TYPO3_DEBUG_SHOW_BODY_TEXT, '0', STORE_TYPO3);
        }

        return self::$instance;
240
241
242
    }

    /**
243
     * QFQ might be called via Typo3 (index.php) or directly via AJAX (directory: api). The
Carsten  Rose's avatar
Carsten Rose committed
244
     *
245
     * @param array $config
Carsten  Rose's avatar
Carsten Rose committed
246
     *
247
248
     * @return array
     */
Carsten  Rose's avatar
Carsten Rose committed
249
    private static function doSystemPath(array $config) {
250

251
        // SYSTEM_PATH_EXT: compute only if not already defined.
252
        if (!isset($config[SYSTEM_EXT_PATH]) || $config[SYSTEM_EXT_PATH] === '' || $config[SYSTEM_EXT_PATH][0] !== '/') {
253
254
            $relExtDir = '/typo3conf/ext/' . EXT_KEY;

255
256
257
            if (defined('PHPUNIT_QFQ')) {
                $cwd = getcwd();
                $pos = strpos($cwd, '/typo3conf/');
258

259
260
                // this means phpUnit.
                $config[SYSTEM_SITE_PATH] = substr($cwd, 0, $pos);
261
                $config[SYSTEM_EXT_PATH] = $config[SYSTEM_SITE_PATH] . $relExtDir;
262
263
264

            } else {
                // If we are called through AJAX API (e.g. api/save.php), there is no TYPO3 environment.
Carsten  Rose's avatar
Carsten Rose committed
265
                $pos = strpos($_SERVER['SCRIPT_FILENAME'] ?? '', $relExtDir);
266
267
                if ($pos === false && isset($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php'])) {

268
                    // Typo3 extension: probably index.php
269
                    $config[SYSTEM_EXT_PATH] = dirname($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php']);
Carsten  Rose's avatar
Carsten Rose committed
270
                    $config[SYSTEM_SITE_PATH] = dirname($_SERVER['SCRIPT_FILENAME'] ?? '');
271
                } else {
272
                    // API
Carsten  Rose's avatar
Carsten Rose committed
273
274
                    $config[SYSTEM_EXT_PATH] = substr($_SERVER['SCRIPT_FILENAME'] ?? '', 0, $pos + strlen($relExtDir));
                    $config[SYSTEM_SITE_PATH] = substr($_SERVER['SCRIPT_FILENAME'] ?? '', 0, $pos);
275
276
277
                }
            }
        }
278

279
280
        Logger::setSystemSitePath($config[SYSTEM_SITE_PATH]);

281
282
283
284
285
286
287
        return $config;
    }

    /**
     * Depending on some configuration value, update corresponding values.
     *
     * @param array $config
Carsten  Rose's avatar
Carsten Rose committed
288
     *
289
290
291
     * @return array
     */
    private static function adjustConfig(array $config) {
292
293

        $config[SYSTEM_SHOW_DEBUG_INFO] = self::adjustConfigShowDebugInfo($config[SYSTEM_SHOW_DEBUG_INFO], self::beUserLoggdIn());
294

Marc Egger's avatar
Marc Egger committed
295
        $config[SYSTEM_SEND_E_MAIL] = $config[SYSTEM_EXT_PATH] . '/Classes/External/sendEmail';
296

297
298
299
300
301
302
303
        // Make path absolute
        foreach ([SYSTEM_MAIL_LOG, SYSTEM_QFQ_LOG, SYSTEM_SQL_LOG] AS $key) {
            if (!empty($config[$key]) && $config[$key][0] != '/') {
                $config[$key] = $config[SYSTEM_SITE_PATH] . '/' . $config[$key];
            }
        }

304
305
306
307
308
309
310
311
        // In case the database credentials are given in the old style: copy them to the new style
        if (!isset($config[SYSTEM_DB_1_USER]) && isset($config[SYSTEM_DB_USER])) {
            $config[SYSTEM_DB_1_USER] = $config[SYSTEM_DB_USER];
            $config[SYSTEM_DB_1_SERVER] = $config[SYSTEM_DB_SERVER];
            $config[SYSTEM_DB_1_PASSWORD] = $config[SYSTEM_DB_PASSWORD];
            $config[SYSTEM_DB_1_NAME] = $config[SYSTEM_DB_NAME];
        }

312
313
314
315
        if ($config[SYSTEM_THROW_GENERAL_ERROR] == 'auto') {
            $config[SYSTEM_THROW_GENERAL_ERROR] = $config[SYSTEM_FLAG_PRODUCTION] == 'yes' ? 'no' : 'yes';
        }

316
        return $config;
317
318
    }

319
    /**
320
     * @param string $value
Carsten  Rose's avatar
Carsten Rose committed
321
     *
322
     * @param $flag
323
     * @return string
324
     */
325
    private static function adjustConfigShowDebugInfo($value, $flag) {
326

327
        // Check if SHOW_DEBUG_INFO contains 'auto'. Replace with appropriate.
328
329
        if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_AUTO, $value) && $flag) {
            $value = str_replace(SYSTEM_SHOW_DEBUG_INFO_AUTO, SYSTEM_SHOW_DEBUG_INFO_YES, $value);
330
331
        }

332
333
334
335
336
337
338
339
340
341
342
        return $value;
    }

    /**
     * Check if there is a Typo3 beUser is logged in. This is only possible if QFQ is called via T3, not via API
     *
     * @return bool  true: current Browser session is a logged in BE User.
     */
    private static function beUserLoggdIn() {

        return (!empty($GLOBALS["TSFE"]->beUserLogin) && $GLOBALS["TSFE"]->beUserLogin === true);
343
344
    }

345
346
347
348
    /**
     * Iterate over all Parameter which have to exist in the config. Throw an array if any is missing.
     *
     * @param array $config
Carsten  Rose's avatar
Carsten Rose committed
349
     *
Marc Egger's avatar
Marc Egger committed
350
     * @throws \UserFormException
351
352
     */
    private static function checkMandatoryParameter(array $config) {
353

354
        // Check mandatory config vars.
355
        $names = array_merge([SYSTEM_SQL_LOG, SYSTEM_SQL_LOG_MODE],
356
357
358
            self::dbCredentialName($config[SYSTEM_DB_INDEX_DATA]),
            self::dbCredentialName($config[SYSTEM_DB_INDEX_QFQ]));

359
360
        foreach ($names as $name) {
            if (!isset($config[$name])) {
Marc Egger's avatar
Marc Egger committed
361
                throw new \UserFormException ("Missing configuration in `" . CONFIG_QFQ_PHP . "`: $name", ERROR_MISSING_CONFIG_INI_VALUE);
362
363
364
365
            }
        }
    }

366
    /**
367
368
     * @param $index
     * @return array
369
370
371
372
373
374
375
376
377
378
379
     */
    private static function dbCredentialName($index) {
        $names = array();
        $names[] = 'DB_' . $index . '_USER';
        $names[] = 'DB_' . $index . '_SERVER';
        $names[] = 'DB_' . $index . '_PASSWORD';
        $names[] = 'DB_' . $index . '_NAME';

        return $names;
    }

380
    /**
Carsten  Rose's avatar
Carsten Rose committed
381
382
     * Set or overwrite a complete store.
     *
Carsten  Rose's avatar
Carsten Rose committed
383
     * @param array $dataArray
Carsten  Rose's avatar
Carsten Rose committed
384
     * @param string $store
385
     * @param bool|false $flagOverwrite
Carsten  Rose's avatar
Carsten Rose committed
386
     *
Marc Egger's avatar
Marc Egger committed
387
388
     * @throws \UserFormException
     * @throws \CodeException
389
     */
390
    public static function setStore(array $dataArray, $store, $flagOverwrite = false) {
391

392
        // Check valid store name
393
        if (!isset(self::$sanitizeStore)) {
Marc Egger's avatar
Marc Egger committed
394
            throw new \UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE);
395
        }
Carsten  Rose's avatar
Carsten Rose committed
396

397
        if ($store === STORE_ZERO || $store === STORE_EMPTY) {
Marc Egger's avatar
Marc Egger committed
398
            throw new \CodeException("setVarArray() for STORE_ZERO/STORE_EMPTY is impossible - there are no values.", ERROR_SET_STORE_ZERO);
399
        }
400

Carsten  Rose's avatar
Carsten Rose committed
401
        if (!$flagOverwrite && isset(self::$raw[$store]) && count(self::$raw[$store]) > 0) {
Marc Egger's avatar
Marc Egger committed
402
            throw new \CodeException("Raw values already been copied to store '$store'. Do this only one time.", ERROR_STORE_VALUE_ALREADY_CODPIED);
403
        }
404

405
406
407
408
        if ($store === STORE_EXTRA || $store === STORE_USER) {
            Session::set($store, $dataArray);
        }

409
410
        self::$raw[$store] = $dataArray;
    }
411

412
413
414
415
416
    /**
     * Deletes a store assigning a new empty array to it.
     *
     * @param $store
     *
Marc Egger's avatar
Marc Egger committed
417
418
     * @throws \UserFormException
     * @throws \CodeException
419
420
421
422
423
424
425
426
427
     */
    public static function unsetStore($store) {

        self::setStore(array(), $store, true);
    }

    /**
     * Returns a complete $store.
     *
428
     * @param string $store STORE_SYSTEM, ...
429
430
     *
     * @return array
Marc Egger's avatar
Marc Egger committed
431
432
     * @throws \UserFormException
     * @throws \CodeException
433
434
435
436
     */
    public static function getStore($store) {
        $vars = array();

437
        // Check valid store name
438
        if (!isset(self::$sanitizeStore[$store])) {
Marc Egger's avatar
Marc Egger committed
439
            throw new \UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE);
440
441
        }

442
        if ($store === STORE_ZERO || $store === STORE_EMPTY) {
Marc Egger's avatar
Marc Egger committed
443
            throw new \CodeException("getStore() for $store is impossible - there are no values saved.", ERROR_GET_INVALID_STORE);
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
        }

        if (isset(self::$raw[$store])) {
            $vars = self::$raw[$store];
            if ($store == STORE_SIP) {
                $vars = self::checkDecodeBase64Arr($vars);
            }
        }

        return $vars;
    }


    /**
     * Fill the system store by reading config.qfq.ini. Also setup config defaults.
     *
460
     * @param string $fileConfigPhp
Marc Egger's avatar
Marc Egger committed
461
462
463
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
464
     */
465
    private static function fillStoreSystem($fileConfigPhp = '') {
466

467
        $config = Config::readConfig($fileConfigPhp);
468
469
470

        $config = self::doSystemPath($config);
        $config = self::adjustConfig($config);
471
        $config = self::setAutoConfigValue($config);
472
473
474
475
476
477

        self::checkMandatoryParameter($config);

        self::setStore($config, STORE_SYSTEM, true);
    }

478
479
480
481
482
483
484
485
    /**
     * Set automatic filled values
     *
     * @param array $config
     * @return array
     */
    public static function setAutoConfigValue(array $config) {

486
487
        $config[SYSTEM_DB_NAME_DATA] = $config['DB_' . $config[SYSTEM_DB_INDEX_DATA] . '_NAME'] ?? '';
        $config[SYSTEM_DB_NAME_QFQ] = $config['DB_' . $config[SYSTEM_DB_INDEX_QFQ] . '_NAME'] ?? '';
488
489
490
491

        return $config;
    }

492
    /**
493
     * Copy the BodyText as well as some T3 specific vars to STORE_TYPO3.
Carsten  Rose's avatar
Carsten Rose committed
494
495
     * Attention: if called through API, there is no T3 environment. The only values which are available are fe_user
     * and fe_user_uid.
496
     *
497
     * @param $bodytext
Carsten  Rose's avatar
Carsten Rose committed
498
     *
Marc Egger's avatar
Marc Egger committed
499
500
     * @throws \CodeException
     * @throws \UserFormException
501
     */
502
    private static function fillStoreTypo3($bodytext) {
503

504
        // form=, showDebugBodyText=, 10.20..
505
        $arr = KeyValueStringParser::parse($bodytext, "=", "\n");
506

507
        if (isset($GLOBALS["TSFE"])) {
508
            $arr = array_merge($arr, T3Info::getVars());
509
        } else {
510

511
            // No T3 environment (called by API): restore from SESSION
512
            foreach ([SESSION_FE_USER, SESSION_FE_USER_UID, SESSION_FE_USER_GROUP, SESSION_BE_USER] as $key) {
513
514
515
                if (isset($_SESSION[SESSION_NAME][$key])) {
                    $arr[$key] = $_SESSION[SESSION_NAME][$key];
                }
516
517
            }
        }
518

519
        self::setStore($arr, STORE_TYPO3, true);
520
    }
521

522
    /**
Carsten  Rose's avatar
Carsten Rose committed
523
524
     * Fills the STORE_CLIENT
     *
Marc Egger's avatar
Marc Egger committed
525
526
     * @throws \CodeException
     * @throws \UserFormException
527
     */
528
    private static function fillStoreClient() {
529

Carsten  Rose's avatar
Carsten Rose committed
530
        $arr = Client::getParam();
531

532
        self::setStore($arr, STORE_CLIENT, true);
533

534
    }
535

Carsten  Rose's avatar
Carsten Rose committed
536
    /**
Carsten  Rose's avatar
Carsten Rose committed
537
538
     * Fills the STORE_SIP. Reads therefore specified SIP, decode the values and stores them in STORE_SIP.
     *
539
     * @param bool|string $s
Marc Egger's avatar
Marc Egger committed
540
541
542
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
543
     */
544
    public static function fillStoreSip($s = false) {
Carsten  Rose's avatar
Carsten Rose committed
545

546
547
548
549
550
551
552
        if (self::$sip === null) {
            self::$sip = new Sip(self::$phpUnit);
        }

        if ($s === false) {
            $s = self::getVar(CLIENT_SIP, STORE_CLIENT);
        }
553

554
555
556
        if ($s !== false) {
            // if session is given, copy values to store
            $param = self::$sip->getVarsFromSip($s);
557
558
            $param[SIP_SIP] = $s;
            $param[SIP_URLPARAM] = self::$sip->getQueryStringFromSip($s);
559

560
//            self::setVarArray(KeyValueStringParser::parse($param, "=", "&"), STORE_SIP);
561
            self::setStore($param, STORE_SIP, true);
562
563
564
        }
    }

565
566
567
568
    /**
     * Fills the STORE_EXTRA.
     *
     * @param string $storeName STORE_EXTRA, STORE_USER
Marc Egger's avatar
Marc Egger committed
569
570
     * @throws \CodeException
     * @throws \UserFormException
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
     */
    private static function fillStorePhpSession($storeName) {

        $value = Session::get($storeName);

        if (!isset($_SESSION[SESSION_NAME][$storeName]) || $_SESSION[SESSION_NAME][$storeName] === null) {
            $value = false;
        }

        if ($value === false) {
            self::setStore(array(), $storeName, true);
        } else {
            self::setStore($_SESSION[SESSION_NAME][$storeName], $storeName, true);
        }
    }

    /**
     * Fills STORE_TABLE_DEFAULT and STORE_TABLE_COLUMN_TYPES
     *
     * @param array $tableDefinition
     *
Marc Egger's avatar
Marc Egger committed
592
593
     * @throws \CodeException
     * @throws \UserFormException
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
     */
    public static function fillStoreTableDefaultColumnType(array $tableDefinition) {

        self::setStore(array_column($tableDefinition, 'Default', 'Field'), STORE_TABLE_DEFAULT, true);
        self::setStore(array_column($tableDefinition, 'Type', 'Field'), STORE_TABLE_COLUMN_TYPES, true);
    }


    /**
     * Set's a single $key/$value pair $store.
     *
     * @param string $key
     * @param string|array $value
     * @param string $storeName
     * @param bool|true $overWrite
     *
Marc Egger's avatar
Marc Egger committed
610
611
     * @throws \UserFormException
     * @throws \CodeException
612
613
     */
    public static function setVar($key, $value, $storeName, $overWrite = true) {
614
        // Check valid Store name
615
        if (!isset(self::$sanitizeStore)) {
Marc Egger's avatar
Marc Egger committed
616
            throw new \UserFormException("Unknown Store: $storeName", ERROR_UNNOWN_STORE);
617
618
619
        }

        if ($storeName === STORE_ZERO) {
Marc Egger's avatar
Marc Egger committed
620
            throw new \CodeException("setVar() for STORE_ZERO is impossible - there are no values.", ERROR_SET_STORE_ZERO);
621
622
623
624
        }

        // Complain if already set and different.
        if ($overWrite === false && isset(self::$raw[$storeName][$key]) && self::$raw[$storeName][$key] != $value) {
Marc Egger's avatar
Marc Egger committed
625
            throw new \UserFormException("Value of '$key' already set in store '$storeName'.", ERROR_STORE_KEY_EXIST);
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
        }

        self::$raw[$storeName][$key] = $value;

        // The STORE_EXTRA / STORE_USER saves arrays and is persistent
        if ($storeName === STORE_EXTRA || $storeName === STORE_USER) {

            $data = Session::get($storeName);

            if ($data === false) {
                $data = array();
            }

            // Switch FeUser: log this to qfq.log
            if ($storeName === STORE_USER && $key == TYPO3_FE_USER) {
                $qfqLog = self::getVar(SYSTEM_QFQ_LOG, STORE_SYSTEM);
                $feUserOld = isset($data[$key]) ? $data[$key] : self::getVar($key, STORE_TYPO3 . STORE_EMPTY);
                Logger::logMessage(date('Y.m.d H:i:s ') . ": Switch feUser '$feUserOld' to '$value'", $qfqLog);
            }

            $data[$key] = $value;
            Session::set($storeName, $data);
        }
    }

651
652
653
654
655
656
    /**
     * Unset a variable in store.
     *
     * @param $key
     * @param $store
     */
657
    public static function unsetVar($key, $store) {
658

659
        if (isset(self::$raw[$store][$key])) {
660
661
662
663
            unset(self::$raw[$store][$key]);
        }
    }

664
    /**
665
     * Cycles through all stores in $useStore.
666
     * First match will return the found value.
667
     * During cycling: fill cache with requested value and sanitize raw value.
668
     *
669
     * @param string $key
670
     * @param string $useStores f.e.: 'FSRVD'
Carsten  Rose's avatar
Carsten Rose committed
671
     * @param string $sanitizeClass
Carsten  Rose's avatar
Carsten Rose committed
672
673
674
     * @param string $foundInStore Returns the name of the store where $key has been found. If $key is not found,
     *                             return ''.
     *
675
     * @param string $typeMessageViolate
676
     * @return string|array a) if found: value, b) false. STORE_EXTRA returns an array for the given key.
Marc Egger's avatar
Marc Egger committed
677
678
     * @throws \CodeException
     * @throws \UserFormException    SANITIZE_TYPE_MESSAGE_VIOLATE_ZERO | SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY | SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS
679
     */
680
681
    public static function getVar($key, $useStores = STORE_USE_DEFAULT, $sanitizeClass = '', &$foundInStore = '',
                                  $typeMessageViolate = SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS) {
682

683
        // no store specified?
684
        if ($useStores === "" || $useStores === null) {
685
            $useStores = STORE_USE_DEFAULT;
686
687
        }

688
        // no sanitizeClass specified: take predefined (if exist) or default.
689
        if ($sanitizeClass === '' || $sanitizeClass === null) {
Carsten  Rose's avatar
Carsten Rose committed
690
            $sanitizeClass = isset(self::$sanitizeClass[$key]) ? self::$sanitizeClass[$key] : SANITIZE_DEFAULT;
691
692
        }

Carsten  Rose's avatar
Carsten Rose committed
693
694
        $len = strlen(SIP_PREFIX_BASE64);

695
        while ($useStores !== false && $useStores !== '') {
696
            $store = $useStores[0]; // current store
697
698
699
700
701
702

            $finalKey = $key;
            if ($store == STORE_LDAP) {
                $finalKey = strtolower($key); // in STORE_LDAP all keys are lowercase
            }

Carsten  Rose's avatar
Carsten Rose committed
703
            $foundInStore = $store;
704
705
            $useStores = substr($useStores, 1); // shift left remaining stores

706
            if (!isset(self::$raw[$store][$finalKey])) {
707
708
709
                switch ($store) {
                    case STORE_ZERO:
                        return 0;
710
711
                    case STORE_EMPTY:
                        return '';
712
                    case STORE_VAR:
713
                        if ($finalKey === VAR_RANDOM) {
714
                            return Support::randomAlphaNum(RANDOM_LENGTH);
715
716
717
718
719
720
721
                        } else {
                            continue 2;  // no value provided, continue with while loop
                        }
                        break;
                    default:
                        continue 2; // no value provided, continue with while loop
                        break;
Carsten  Rose's avatar
Carsten Rose committed
722
                }
723
724
            }

725
            $rawVal = isset(self::$raw[$store][$finalKey]) ? self::$raw[$store][$finalKey] : null;
Carsten  Rose's avatar
Carsten Rose committed
726
            if (self::$sanitizeStore[$store] && $sanitizeClass != '') {
727
                if ($sanitizeClass == SANITIZE_ALLOW_PATTERN) {
728
                    // We do not have any pattern at this point. For those who be affected, they already checked earlier. So set 'no check'
729
730
                    $sanitizeClass = SANITIZE_ALLOW_ALL;
                }
Carsten  Rose's avatar
Carsten Rose committed
731

732
                return Sanitize::sanitize($rawVal, $sanitizeClass, '', '', SANITIZE_EMPTY_STRING, '', $typeMessageViolate);
733
            } else {
Carsten  Rose's avatar
Carsten Rose committed
734
735
736
                if ($store == STORE_SIP && (substr($key, 0, $len) == SIP_PREFIX_BASE64)) {
                    $rawVal = base64_decode($rawVal);
                }
Carsten  Rose's avatar
Carsten Rose committed
737

738
                return $rawVal;
739
            }
740
        }
Carsten  Rose's avatar
Carsten Rose committed
741
        $foundInStore = '';
Carsten  Rose's avatar
Carsten Rose committed
742

743
        return false;
744
    }
745

746
    /**
Carsten  Rose's avatar
Carsten Rose committed
747
748
     * Create a SIP after a form load. This is necessary on forms without a sip and on forms with r=0 (new record).
     *
749
     * @param $formName
Carsten  Rose's avatar
Carsten Rose committed
750
     *
Marc Egger's avatar
Marc Egger committed
751
752
     * @throws \CodeException
     * @throws \UserFormException
753
     */
754
    public static function createSipAfterFormLoad($formName) {
755

756
        $recordId = self::getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_CLIENT);
757
758
759
760
        if ($recordId === false) {
            $recordId = 0;
        }

761
762
763
764
765
766
        // If there are existing SIP param, keep them by copying to the new SIP Param Array
        $tmpParam = self::getNonSystemSipParam();

        $tmpParam[SIP_RECORD_ID] = $recordId;
        $tmpParam[SIP_FORM] = $formName;

Carsten  Rose's avatar
#2067    
Carsten Rose committed
767
768
769
770
        if ($recordId == 0) {
            // SIPs for 'new records' needs to be uniq per TAB! Therefore add a uniq parameter
            $tmpParam[SIP_MAKE_URLPARAM_UNIQ] = uniqid();
        }
771
772

        // Construct fake urlparam
773
        $tmpUrlparam = OnArray::toString($tmpParam);
774
775

        // Create a fake SIP which has never been passed by URL - further processing might expect this to exist.
776
        $sip = self::getSipInstance()->queryStringToSip($tmpUrlparam, RETURN_SIP);
777
        self::setVar(CLIENT_SIP, $sip, STORE_CLIENT);
778
779
780

        // Store in SIP Store (cause it's empty until now).
        $tmpParam[SIP_SIP] = $sip;
781
        self::setStore($tmpParam, STORE_SIP, true);
782

783
784
785
    }

    /**
786
     * Return an array with non system SIP parameter. Take the whole STORE_SIP and search for non system parameter.
787
     *
788
     * @return array
Marc Egger's avatar
Marc Egger committed
789
790
     * @throws \UserFormException
     * @throws \CodeException
791
     */
792
    public static function getNonSystemSipParam() {
793
794
795
796
797
        $tmpParam = array();

        $sipArray = self::getStore(STORE_SIP);

        foreach ($sipArray as $key => $value) {
798

799
800
801
            if ($key[0] === '_') {
                continue;
            }
802

803
804
805
806
807
            switch ($key) {
                case SIP_SIP:
                case SIP_RECORD_ID:
                case SIP_FORM;
                case SIP_URLPARAM:
808
                    continue 2;
809
810
811
812
813
814
                default:
                    $tmpParam[$key] = $value;
            }
        }

        return $tmpParam;
815
    }
816

Carsten  Rose's avatar
Carsten Rose committed
817
818
819
820
821
    /**
     * Iterate over an array and look for keynames starting with SIP_PREFIX_BASE64.
     * Found elements will be base64_decode().
     *
     * @param array $vars
Carsten  Rose's avatar
Carsten Rose committed
822
     *
Carsten  Rose's avatar
Carsten Rose committed
823
824
825
826
827
828
829
830
831
832
833
     * @return array - incl. decoded base64 vars.
     */
    private static function checkDecodeBase64Arr(array $vars) {

        $len = strlen(SIP_PREFIX_BASE64);

        foreach ($vars as $key => $value) {
            if (substr($key, 0, $len) == SIP_PREFIX_BASE64) {
                $vars[$key] = base64_decode($value);
            }
        }
Carsten  Rose's avatar
Carsten Rose committed
834

Carsten  Rose's avatar
Carsten Rose committed
835
        return $vars;
836
837
    }

838
839
840
841
842
843
844
845
846
847
    /**
     * Returns a pointer to this class.
     *
     * @return null|Sip
     */
    public static function getSipInstance() {
        return self::$sip;
    }

    /**
848
849
850
851
     * Saves a subset of STORE_TYPO3 vars as a SIP. The SIP will be transmitted as hidden form element.
     *
     * More docs: CODING.md > 'Faking the STORE_TYPO3 for API calls'
     *
Marc Egger's avatar
Marc Egger committed
852
853
     * @throws \CodeException
     * @throws \UserFormException
854
855
856
857
858
     */
    public static function copyT3VarsToSip() {

        $tempArray = self::getStore(STORE_TYPO3);

859
        foreach ([TYPO3_FE_USER, TYPO3_FE_USER_UID, TYPO3_FE_USER_GROUP, TYPO3_TT_CONTENT_UID, TYPO3_PAGE_ID, TYPO3_PAGE_ALIAS, TYPO3_PAGE_TYPE, TYPO3_PAGE_LANGUAGE, TYPO3_BE_USER_LOGGED_IN] as $key) {
860
861
862
863
864
865
866
867
868
869
870
871
872
873
            if (isset($tempArray[$key])) {
                $t3varsArray[$key] = $tempArray[$key];
            }
        }

        $t3varsArray[TYPO3_BE_USER_LOGGED_IN] = (isset($GLOBALS["TSFE"]->beUserLogin) && $GLOBALS["TSFE"]->beUserLogin === true) ? 'yes' : 'no';

        $t3varsString = KeyValueStringParser::unparse($t3varsArray, '=', '&');
        $t3sip = self::$sip->queryStringToSip($t3varsString, RETURN_SIP);

        return $t3sip;
    }

    /**
874
875
876
     * Get stored STORE_TYPO3 vars from SIP and restore the store.
     *
     * More docs: CODING.md > 'Faking the STORE_TYPO3 for API calls'
877
878
     *
     * @param string $sipTypo3Vars
Marc Egger's avatar
Marc Egger committed
879
880
881
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
882
     */
Carsten  Rose's avatar
Carsten Rose committed
883
    public static function fillTypo3StoreFromSip($sipTypo3Vars) {
884
885
886
887
888
889
890
891
892
893
        $t3vars = self::getStore(STORE_TYPO3);

        // Do nothing if STORE_TYPO3 is already filled
        if (isset($t3vars[TYPO3_TT_CONTENT_UID]) && $t3vars[TYPO3_TT_CONTENT_UID] != '0') {
            return;
        }

        $typo3VarsArray = self::$sip->getVarsFromSip($sipTypo3Vars);

        self::setStore($typo3VarsArray, STORE_TYPO3, true);
Carsten  Rose's avatar
Carsten Rose committed
894

895
        $value = self::getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM);
896
897
        $flag = isset($typo3VarsArray[TYPO3_BE_USER_LOGGED_IN]) && ($typo3VarsArray[TYPO3_BE_USER_LOGGED_IN] == 'yes');
        $value = self::adjustConfigShowDebugInfo($value, $flag);
898
        self::setVar(SYSTEM_SHOW_DEBUG_INFO, $value, STORE_SYSTEM);
899
    }
900

901
902
903
    /**
     * Load $recordId from $tableName using $db and saves it in $store.
     *
904
905
906
907
     * @param string $tableName
     * @param int $recordId
     * @param Database $db
     * @param string $primaryKey
908
     * @param string $store
Marc Egger's avatar
Marc Egger committed
909
910
911
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
912
     */
913
    public static function fillStoreWithRecord($tableName, $recordId, Database $db,
914
                                               $primaryKey = F_PRIMARY_KEY_DEFAULT, $store = STORE_RECORD) {
915
916
        if ($recordId !== false && $recordId > 0 &&
            is_string($tableName) && $tableName !== '') {
917
918
919
            if (!$primaryKey) {
                $primaryKey = F_PRIMARY_KEY_DEFAULT;
            }
920

921
            $record = $db->sql("SELECT * FROM $tableName WHERE $primaryKey = ?", ROW_EXPECT_1, [$recordId]);
922
923
924
925
            self::setStore($record, $store, true);
        }
    }

926
    /**
927
     * Read SYSTEM_FILL_STORE_SYSTEM_BY_SQL_1|2|3 from SYSTEM_STORE and if set:
928
     * a) fire the SQL
929
     * b) merge all columns to STORE_SYSTEM
930
     *
Marc Egger's avatar
Marc Egger committed
931
932
933
934
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
935
     */
936
    public static function FillStoreSystemBySql() {
937

938
        $db = null;
939
        $flagDirty = false;
940
941
        $storeSystem = self::getStore(STORE_SYSTEM);

942
        for ($ii = 1; $ii <= 3; $ii++) {
943
            if (empty($storeSystem[SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii"])) {
944
945
946
947
                continue;
            }

            if ($db == null) {
Marc Egger's avatar
Marc Egger committed
948
                $db = new Database();
949
950
            }

951
            $errMsg = "More than 1 record found. " . CONFIG_QFQ_PHP . ": " . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii";
952
953
954
            $mode = ROW_EXPECT_0_1;

            // If there is an error message defined, this means there should be exactly one record.
955
            if (!empty($storeSystem[SYSTEM_FILL_STORE_SYSTEM_ERROR_MSG . "$ii"])) {
956
                $mode = ROW_EXPECT_1;
957
                $errMsg = $storeSystem[SYSTEM_FILL_STORE_SYSTEM_ERROR_MSG . "$ii"];
958
            }
959

960
            $storeSystemAdd = $db->sql($storeSystem[SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii"], $mode, array(), $errMsg);
961