Store.php 33.3 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
 */

9
namespace qfq;
10

11
use qfq;
12

13
14
15
16
17
18
19
20
21
require_once(__DIR__ . '/../../core/helper/KeyValueStringParser.php');
require_once(__DIR__ . '/../../core/helper/Sanitize.php');
require_once(__DIR__ . '/../../core/helper/Logger.php');
require_once(__DIR__ . '/../../core/Constants.php');
require_once(__DIR__ . '/../../core/store/Sip.php');
require_once(__DIR__ . '/../../core/store/T3Info.php');
require_once(__DIR__ . '/../../core/database/Database.php');
require_once(__DIR__ . '/../../core/store/Config.php');
require_once(__DIR__ . '/../../core/store/Client.php');
22
23
24
25
26
27
28
29
30
31

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

Carsten  Rose's avatar
Carsten Rose committed
32
33
/**
 * Class Store
34
 * @package qfq
Carsten  Rose's avatar
Carsten Rose committed
35
 */
36
37
class Store {

Carsten  Rose's avatar
Carsten Rose committed
38
39
40
    /**
     * @var Store Instance of class Store. There should only be one class 'Store' at a time.
     */
41
42
    private static $instance = null;

Carsten  Rose's avatar
Carsten Rose committed
43
44
45
    /**
     * @var Sip Instance of class SIP
     */
46
47
    private static $sip = null;

48
49
50
    /**
     * @var Session Instance of class Session
     */
51
//    private static $session = null;
52

Carsten  Rose's avatar
Carsten Rose committed
53
54
55
56
57
58
59
60
61
62
    /**
     * @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
     */
63
    private static $raw = array();
Carsten  Rose's avatar
Carsten Rose committed
64
65

    /**
Carsten  Rose's avatar
Carsten Rose committed
66
     * @var array Default sanitize classes.
Carsten  Rose's avatar
Carsten Rose committed
67
     */
Carsten  Rose's avatar
Carsten Rose committed
68
    private static $sanitizeClass = array();
Carsten  Rose's avatar
Carsten Rose committed
69
70

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

Carsten  Rose's avatar
Carsten Rose committed
79
    private static $phpUnit = false;
80

81

82
    /**
83
     * @param string $bodytext
84
     * @param string $fileConfigIni
Carsten  Rose's avatar
Carsten Rose committed
85
     *
86
     * @throws CodeException
87
     * @throws UserFormException
88
     * @throws UserReportException
89
     */
90
    private function __construct($bodytext = '', $fileConfigIni = '') {
91

92
//        self::$session = Session::getInstance(self::$phpUnit);
93

94
        // This check is critical for some unwanted exception recursion during startup.
Carsten  Rose's avatar
Carsten Rose committed
95
96
        if (!function_exists('normalizer_normalize')) {
            throw new CodeException("Function normalizer_normalize() not found - Please install 'php5-intl' / 'php7.0-intl'", ERROR_MISSING_INTL);
97
98
        }

Carsten  Rose's avatar
Carsten Rose committed
99
100
101
102
103
104
105
106
        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
107
108
109
110
            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
111
            CLIENT_KEY_SEM_ID_USER => SANITIZE_ALLOW_DIGIT,
Carsten  Rose's avatar
Carsten Rose committed
112
113
114
115
            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
116
117

            // Part of $_SERVER. Missing vars must be requested individual with the needed sanitize class.
Carsten  Rose's avatar
Carsten Rose committed
118
119
120
            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
121
            CLIENT_HTTP_USER_AGENT => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
122
            CLIENT_SERVER_NAME => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
123
            CLIENT_SERVER_ADDRESS => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
124
            CLIENT_SERVER_PORT => SANITIZE_ALLOW_DIGIT,
Carsten  Rose's avatar
Carsten Rose committed
125
126
127
            CLIENT_REMOTE_ADDRESS => SANITIZE_ALLOW_ALNUMX,
            CLIENT_REQUEST_SCHEME => SANITIZE_ALLOW_ALNUMX,
            CLIENT_SCRIPT_FILENAME => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
128
129
130
131
            CLIENT_QUERY_STRING => SANITIZE_ALLOW_ALL,
            CLIENT_REQUEST_URI => SANITIZE_ALLOW_ALL,
            CLIENT_SCRIPT_NAME => SANITIZE_ALLOW_ALNUMX,
            CLIENT_PHP_SELF => SANITIZE_ALLOW_ALNUMX,
132
//            CLIENT_UPLOAD_FILENAME => SANITIZE_ALLOW_ALLBUT,
Carsten  Rose's avatar
Carsten Rose committed
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150

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

152
153
        ];

Carsten  Rose's avatar
Carsten Rose committed
154
        self::$sanitizeStore = [
Carsten  Rose's avatar
Carsten Rose committed
155
156
157
158
159
160
161
162
163
164
165
166
167
168
            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,
169
            STORE_USER => false,
Carsten  Rose's avatar
Carsten Rose committed
170
            STORE_LDAP => false,
Carsten  Rose's avatar
Carsten Rose committed
171
            STORE_ADDITIONAL_FORM_ELEMENTS => false,
172
173
        ];

174
175
        self::fillStoreTypo3($bodytext);  // should be filled before fillStoreSystem() to offer T3 variables
        self::fillStoreClient();  // should be filled before fillStoreSystem() to offer Client variables
176
177
        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
178
        self::fillStoreSystem($fileConfigIni);
179
        self::fillStoreSip();
180
    }
181

182
    /**
183
     * Returns a pointer to this Class.
Carsten  Rose's avatar
Carsten Rose committed
184
     *
185
186
     * @param string $bodytext
     * @param bool|false $phpUnit
187
     * @param string $fileConfigIni
188
189
     *
     * @return null|Store
190
     * @throws CodeException
191
     * @throws UserFormException
192
     * @throws UserReportException
193
     */
194
    public static function getInstance($bodytext = '', $phpUnit = false, $fileConfigIni = '') {
195

196
197
198
199
200
        if (defined('PHPUNIT_QFQ')) {
            self::$phpUnit = true;
        }

        if (self::$phpUnit) {
201

202
203
204
205
            if (self::$instance !== null) {
                // fake to have a clean environment for the next test.
                self::unsetStore(STORE_TYPO3);
                self::fillStoreTypo3($bodytext);
206

207
208
209
                self::unsetStore(STORE_CLIENT);
                self::fillStoreClient();
            }
210

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

        // Design Pattern: Singleton
        if (self::$instance === null) {
219
//            self::$phpUnit = $phpUnit;
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237

            self::$instance = new self($bodytext, $fileConfigIni);
        } 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;
238
239
240
    }

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

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

254
255
256
257
258
259
260
261
262
263
264
265
            if (defined('PHPUNIT_QFQ')) {
                $cwd = getcwd();
                $pos = strpos($cwd, '/typo3conf/');
                if($pos==false){
                    throw new CodeException("Directory component '/typo3conf/' not found in '$cwd'", 1);
                }
                // this means phpUnit.
                $config[SYSTEM_SITE_PATH] = substr($cwd, 0, $pos);
                $config[SYSTEM_PATH_EXT] = $config[SYSTEM_SITE_PATH] . $relExtDir;

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

269
                    // Typo3 extension: probably index.php
270
                    $config[SYSTEM_PATH_EXT] = dirname($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php']);
271
                    $config[SYSTEM_SITE_PATH] = dirname($_SERVER['SCRIPT_FILENAME']);
272
                } else {
273
                    // API
274
                    $config[SYSTEM_PATH_EXT] = substr($_SERVER['SCRIPT_FILENAME'], 0, $pos + strlen($relExtDir));
275
                    $config[SYSTEM_SITE_PATH] = substr($_SERVER['SCRIPT_FILENAME'], 0, $pos);
276
277
278
                }
            }
        }
279
280
281
282
283
284
285
286

        return $config;
    }

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

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

        // make SQL PATH absolute. This is necessary to work in different directories correctly.
295
        if (!empty($config[SYSTEM_SQL_LOG]) && $config[SYSTEM_SQL_LOG][0] !== '/') {
296
            $config[SYSTEM_SQL_LOG] = $config[SYSTEM_SITE_PATH] . '/' . $config[SYSTEM_SQL_LOG];
297
298
        }

299
300
        // make SQL PATH absolute. This is necessary to work in different directories correctly.
        if (!empty($config[SYSTEM_MAIL_LOG]) && $config[SYSTEM_MAIL_LOG][0] !== '/') {
301
            $config[SYSTEM_MAIL_LOG] = $config[SYSTEM_SITE_PATH] . '/' . $config[SYSTEM_MAIL_LOG];
302
303
304
305
        }

        $config[SYSTEM_SEND_E_MAIL] = $config[SYSTEM_PATH_EXT] . '/qfq/external/sendEmail';

306
307
308
309
310
311
312
313
        // 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];
        }

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

318
        return $config;
319
320
    }

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

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

334
335
336
337
338
339
340
341
342
343
344
        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);
345
346
    }

347
348
349
350
    /**
     * 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
351
     *
352
353
354
     * @throws UserFormException
     */
    private static function checkMandatoryParameter(array $config) {
355

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

361
362
        foreach ($names as $name) {
            if (!isset($config[$name])) {
363
                throw new qfq\UserFormException ("Missing configuration in `" . CONFIG_QFQ_PHP . "`: $name", ERROR_MISSING_CONFIG_INI_VALUE);
364
365
366
367
            }
        }
    }

368
    /**
369
370
     * @param $index
     * @return array
371
372
373
374
375
376
377
378
379
380
381
     */
    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;
    }

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

Carsten  Rose's avatar
Carsten Rose committed
394
        // Check valid Storename
395
        if (!isset(self::$sanitizeStore)) {
396
            throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE);
397
        }
Carsten  Rose's avatar
Carsten Rose committed
398

399
400
401
        if ($store === STORE_ZERO || $store === STORE_EMPTY) {
            throw new CodeException("setVarArray() for STORE_ZERO/STORE_EMPTY is impossible - there are no values.", ERROR_SET_STORE_ZERO);
        }
402

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

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

411
412
        self::$raw[$store] = $dataArray;
    }
413

414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
    /**
     * Deletes a store assigning a new empty array to it.
     *
     * @param $store
     *
     * @throws UserFormException
     * @throws \qfq\CodeException
     */
    public static function unsetStore($store) {

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

    /**
     * Returns a complete $store.
     *
     * @param $store
     *
     * @return array
     * @throws UserFormException
     * @throws \qfq\CodeException
     */
    public static function getStore($store) {
        $vars = array();

        // Check valid Storename
        if (!isset(self::$sanitizeStore[$store])) {
            throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE);
        }

        if ($store === STORE_ZERO) {
            throw new CodeException("getStore() for STORE_ZERO is impossible - there are no values saved.", ERROR_GET_STORE_ZERO);
        }

        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.
     *
     * @param string $fileConfigIni
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
    private static function fillStoreSystem($fileConfigIni = '') {

        $config = Config::readConfig($fileConfigIni);

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

        self::checkMandatoryParameter($config);

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

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

488
489
        $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'] ?? '';
490
491
492
493

        return $config;
    }

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

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

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

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

521
        self::setStore($arr, STORE_TYPO3, true);
522
    }
523

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

Carsten  Rose's avatar
Carsten Rose committed
532
        $arr = Client::getParam();
533

534
        self::setStore($arr, STORE_CLIENT, true);
535

536
    }
537

Carsten  Rose's avatar
Carsten Rose committed
538
    /**
Carsten  Rose's avatar
Carsten Rose committed
539
540
     * Fills the STORE_SIP. Reads therefore specified SIP, decode the values and stores them in STORE_SIP.
     *
Carsten  Rose's avatar
Carsten Rose committed
541
     * @throws CodeException
542
     * @throws UserFormException
543
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
544
     */
545
    private static function fillStoreSip() {
Carsten  Rose's avatar
Carsten Rose committed
546

547
        self::$sip = new Sip(self::$phpUnit);
548

549
550
551
552
        $s = self::getVar(CLIENT_SIP, STORE_CLIENT);
        if ($s !== false) {
            // if session is given, copy values to store
            $param = self::$sip->getVarsFromSip($s);
553
554
            $param[SIP_SIP] = $s;
            $param[SIP_URLPARAM] = self::$sip->getQueryStringFromSip($s);
555

556
//            self::setVarArray(KeyValueStringParser::parse($param, "=", "&"), STORE_SIP);
557
            self::setStore($param, STORE_SIP, true);
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
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
    /**
     * Fills the STORE_EXTRA.
     *
     * @param string $storeName STORE_EXTRA, STORE_USER
     * @throws CodeException
     * @throws UserFormException
     */
    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
     *
     * @throws CodeException
     * @throws UserFormException
     */
    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
     *
     * @throws UserFormException
     * @throws \qfq\CodeException
     */
    public static function setVar($key, $value, $storeName, $overWrite = true) {
        // Check valid Storename
        if (!isset(self::$sanitizeStore)) {
            throw new UserFormException("Unknown Store: $storeName", ERROR_UNNOWN_STORE);
        }

        if ($storeName === STORE_ZERO) {
            throw new CodeException("setVar() for STORE_ZERO is impossible - there are no values.", ERROR_SET_STORE_ZERO);
        }

        // Complain if already set and different.
        if ($overWrite === false && isset(self::$raw[$storeName][$key]) && self::$raw[$storeName][$key] != $value) {
            throw new UserFormException("Value of '$key' already set in store '$storeName'.", ERROR_STORE_KEY_EXIST);
        }

        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);
        }
    }

647
648
649
650
651
652
    /**
     * Unset a variable in store.
     *
     * @param $key
     * @param $store
     */
653
    public static function unsetVar($key, $store) {
654

655
        if (isset(self::$raw[$store][$key])) {
656
657
658
659
            unset(self::$raw[$store][$key]);
        }
    }

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

677
        // no store specified?
678
        if ($useStores === "" || $useStores === null) {
679
            $useStores = STORE_USE_DEFAULT;
680
681
        }

682
        // no sanitizeClass specified: take predefined (if exist) or default.
683
        if ($sanitizeClass === '' || $sanitizeClass === null) {
Carsten  Rose's avatar
Carsten Rose committed
684
            $sanitizeClass = isset(self::$sanitizeClass[$key]) ? self::$sanitizeClass[$key] : SANITIZE_DEFAULT;
685
686
        }

Carsten  Rose's avatar
Carsten Rose committed
687
688
        $len = strlen(SIP_PREFIX_BASE64);

689
690
        while ($useStores !== false) {
            $store = substr($useStores, 0, 1); // next store
691
692
693
694
695
696

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

Carsten  Rose's avatar
Carsten Rose committed
697
            $foundInStore = $store;
698
699
            $useStores = substr($useStores, 1); // shift left remaining stores

700
            if (!isset(self::$raw[$store][$finalKey])) {
701
702
703
                switch ($store) {
                    case STORE_ZERO:
                        return 0;
704
705
                    case STORE_EMPTY:
                        return '';
706
                    case STORE_VAR:
707
                        if ($finalKey === VAR_RANDOM) {
708
                            return Support::randomAlphaNum(RANDOM_LENGTH);
709
710
711
712
713
714
715
                        } 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
716
                }
717
718
            }

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

726
                return Sanitize::sanitize($rawVal, $sanitizeClass, '', '', SANITIZE_EMPTY_STRING);
727
            } else {
Carsten  Rose's avatar
Carsten Rose committed
728
729
730
                if ($store == STORE_SIP && (substr($key, 0, $len) == SIP_PREFIX_BASE64)) {
                    $rawVal = base64_decode($rawVal);
                }
Carsten  Rose's avatar
Carsten Rose committed
731

732
                return $rawVal;
733
            }
734
        }
Carsten  Rose's avatar
Carsten Rose committed
735
        $foundInStore = '';
Carsten  Rose's avatar
Carsten Rose committed
736

737
        return false;
738
    }
739

740
    /**
Carsten  Rose's avatar
Carsten Rose committed
741
742
     * Create a SIP after a form load. This is necessary on forms without a sip and on forms with r=0 (new record).
     *
743
     * @param $formName
Carsten  Rose's avatar
Carsten Rose committed
744
     *
745
     * @throws CodeException
746
     * @throws UserFormException
747
     */
748
    public static function createSipAfterFormLoad($formName) {
749

750
        $recordId = self::getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_CLIENT);
751
752
753
754
        if ($recordId === false) {
            $recordId = 0;
        }

755
756
757
758
759
760
        // 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
761
762
763
764
        if ($recordId == 0) {
            // SIPs for 'new records' needs to be uniq per TAB! Therefore add a uniq parameter
            $tmpParam[SIP_MAKE_URLPARAM_UNIQ] = uniqid();
        }
765
766

        // Construct fake urlparam
767
        $tmpUrlparam = OnArray::toString($tmpParam);
768
769

        // Create a fake SIP which has never been passed by URL - further processing might expect this to exist.
770
        $sip = self::getSipInstance()->queryStringToSip($tmpUrlparam, RETURN_SIP);
771
        self::setVar(CLIENT_SIP, $sip, STORE_CLIENT);
772
773
774

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

777
778
779
    }

    /**
780
     * Return an array with non system SIP parameter. Take the whole STORE_SIP and search for non system parameter.
781
     *
782
783
784
     * @return array
     * @throws UserFormException
     * @throws \qfq\CodeException
785
     */
786
    public static function getNonSystemSipParam() {
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
        $tmpParam = array();

        $sipArray = self::getStore(STORE_SIP);

        foreach ($sipArray as $key => $value) {
            if ($key[0] === '_') {
                continue;
            }
            switch ($key) {
                case SIP_SIP:
                case SIP_RECORD_ID:
                case SIP_FORM;
                case SIP_URLPARAM:
                    continue;
                default:
                    $tmpParam[$key] = $value;
            }
        }

        return $tmpParam;
807
    }
808

Carsten  Rose's avatar
Carsten Rose committed
809
810
811
812
813
    /**
     * 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
814
     *
Carsten  Rose's avatar
Carsten Rose committed
815
816
817
818
819
820
821
822
823
824
825
     * @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
826

Carsten  Rose's avatar
Carsten Rose committed
827
        return $vars;
828
829
    }

830
831
832
833
834
835
836
837
838
839
    /**
     * Returns a pointer to this class.
     *
     * @return null|Sip
     */
    public static function getSipInstance() {
        return self::$sip;
    }

    /**
840
841
842
843
     * 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'
     *
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
     * @throws CodeException
     * @throws UserFormException
     */
    public static function copyT3VarsToSip() {

        $tempArray = self::getStore(STORE_TYPO3);

        foreach ([TYPO3_FE_USER, TYPO3_FE_USER_UID, TYPO3_FE_USER_GROUP, TYPO3_TT_CONTENT_UID, TYPO3_PAGE_ID, TYPO3_PAGE_TYPE, TYPO3_PAGE_LANGUAGE, TYPO3_BE_USER_LOGGED_IN] as $key) {
            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;
    }

    /**
866
867
868
     * Get stored STORE_TYPO3 vars from SIP and restore the store.
     *
     * More docs: CODING.md > 'Faking the STORE_TYPO3 for API calls'
869
870
     *
     * @param string $sipTypo3Vars
871
872
     * @throws CodeException
     * @throws UserFormException
873
     * @throws UserReportException
874
     */
Carsten  Rose's avatar
Carsten Rose committed
875
    public static function fillTypo3StoreFromSip($sipTypo3Vars) {
876
877
878
879
880
881
882
883
884
885
        $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
886

887
        $value = self::getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM);
888
889
        $flag = isset($typo3VarsArray[TYPO3_BE_USER_LOGGED_IN]) && ($typo3VarsArray[TYPO3_BE_USER_LOGGED_IN] == 'yes');
        $value = self::adjustConfigShowDebugInfo($value, $flag);
890
        self::setVar(SYSTEM_SHOW_DEBUG_INFO, $value, STORE_SYSTEM);
891
    }
892

893
894
895
    /**
     * Load $recordId from $tableName using $db and saves it in $store.
     *
896
897
898
899
     * @param string $tableName
     * @param int $recordId
     * @param Database $db
     * @param string $primaryKey
900
901
     * @param string $store
     * @throws CodeException
902
     * @throws DbException
903
904
     * @throws UserFormException
     */
905
    public static function fillStoreWithRecord($tableName, $recordId, Database $db,
906
                                               $primaryKey = F_PRIMARY_KEY_DEFAULT, $store = STORE_RECORD) {
907
908
        if ($recordId !== false && $recordId > 0 &&
            is_string($tableName) && $tableName !== '') {
909
910
911
            if (!$primaryKey) {
                $primaryKey = F_PRIMARY_KEY_DEFAULT;
            }
912

913
            $record = $db->sql("SELECT * FROM $tableName WHERE $primaryKey = ?", ROW_EXPECT_1, [$recordId]);
914
915
916
917
            self::setStore($record, $store, true);
        }
    }

918
    /**
919
     * Read SYSTEM_FILL_STORE_SYSTEM_BY_SQL_1|2|3 from SYSTEM_STORE and if set:
920
     * a) fire the SQL
921
     * b) merge all columns to STORE_SYSTEM
922
923
924
925
     *
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
926
     * @throws UserReportException
927
     */
928
    public static function StoreSystemUpdate() {
929

930
        $db = null;
931
        $flagDirty = false;
932
933
        $storeSystem = self::getStore(STORE_SYSTEM);

934
        for ($ii = 1; $ii <= 3; $ii++) {
935
            if (empty($storeSystem[SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii"])) {
936
937
938
939
940
941
942
                continue;
            }

            if ($db == null) {
                $db = new qfq\Database();
            }

943
            $errMsg = "More than 1 record found. " . CONFIG_QFQ_PHP . ": " . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii";
944
945
946
            $mode = ROW_EXPECT_0_1;

            // If there is an error message defined, this means there should be exactly one record.
947
            if (!empty($storeSystem[SYSTEM_FILL_STORE_SYSTEM_ERROR_MSG . "$ii"])) {
948
                $mode = ROW_EXPECT_1;
949
                $errMsg = $storeSystem[SYSTEM_FILL_STORE_SYSTEM_ERROR_MSG . "$ii"];
950
            }
951

952
            $storeSystemAdd = $db->sql($storeSystem[SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii"], $mode, array(), $errMsg);
953
954
            $storeSystemAdd = OnArray::keyNameRemoveLeadingUnderscore($storeSystemAdd);
            $storeSystem = array_merge($storeSystem, $storeSystemAdd);
955
            $flagDirty = true;
956
957
        }

958
        if ($flagDirty) {
959
            self::setStore($storeSystem, STORE_SYSTEM, true);
960
961
        }
    }
962
963

    /**
964
     * Append an array (in case of 'array of array': the first row of array) to store $storeName.
965
966
     * Existing values will be overwritten.
     *
967
     * @param array $dataArray - in special cases it might be an empty string -therefore no type definition to 'array'.
968
969
970
     * @param $storeName
     * @throws CodeException
     * @throws UserFormException
971
     * @throws UserReportException
972
     */
973
    public static function appendToStore($dataArray, $storeName) {
974

975
        if (empty($dataArray) || !is_array($dataArray)) {
976
977
            return;
        }
978

979
980
        if (isset($dataArray[0]) && is_array($dataArray[0])) {
            $append = $dataArray[0];
981
        } else {
982
            $append = $dataArray;
983
984
985
986
987
988
989
990
991
        }

        if (empty($append)) {
            return;
        }

        $store = Store::getInstance();
        $store->setStore(array_merge($store->getStore($storeName), $append), $storeName, true);
    }
992
}