Store.php 33.2 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 $fileConfigPhp
Carsten  Rose's avatar
Carsten Rose committed
85
     *
86
     * @throws CodeException
87
     * @throws UserFormException
88
     * @throws UserReportException
89
     */
90
    private function __construct($bodytext = '', $fileConfigPhp = '') {
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
            CLIENT_REMOTE_ADDRESS => SANITIZE_ALLOW_ALNUMX,
            CLIENT_REQUEST_SCHEME => SANITIZE_ALLOW_ALNUMX,
127
            CLIENT_REQUEST_METHOD => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
128
            CLIENT_SCRIPT_FILENAME => SANITIZE_ALLOW_ALNUMX,
Carsten  Rose's avatar
Carsten Rose committed
129
130
131
132
            CLIENT_QUERY_STRING => SANITIZE_ALLOW_ALL,
            CLIENT_REQUEST_URI => SANITIZE_ALLOW_ALL,
            CLIENT_SCRIPT_NAME => SANITIZE_ALLOW_ALNUMX,
            CLIENT_PHP_SELF => SANITIZE_ALLOW_ALNUMX,
133
//            CLIENT_UPLOAD_FILENAME => SANITIZE_ALLOW_ALLBUT,
Carsten  Rose's avatar
Carsten Rose committed
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

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

153
154
        ];

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

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

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

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

        if (self::$phpUnit) {
202

203
204
205
            // If $qfqConfigPhpUnit is given: clean STORE
//            if (self::$instance !== null && $qfqConfigPhpUnit != '') {
            if ($qfqConfigPhpUnit != '') {
Carsten  Rose's avatar
Carsten Rose committed
206

207
208
                if ($qfqConfigPhpUnit == 'init') {
                    $qfqConfigPhpUnit = '';
Carsten  Rose's avatar
Carsten Rose committed
209
210
211
212
213
214
215
216
                }
                // fake to have a clean environment for the next test.
                self::unsetStore(STORE_TYPO3);
                self::fillStoreTypo3($bodytext);

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

218
            // Testing different config files means initialize completely
219
            if ($qfqConfigPhpUnit != '') {
220
221
222
223
224
225
                self::$instance = null;
            }
        }

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

228
            self::$instance = new self($bodytext, $qfqConfigPhpUnit);
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
        } 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;
245
246
247
    }

    /**
248
     * QFQ might be called via Typo3 (index.php) or directly via AJAX (directory: api). The
Carsten  Rose's avatar
Carsten Rose committed
249
     *
250
     * @param array $config
Carsten  Rose's avatar
Carsten Rose committed
251
     *
252
     * @return array
253
     * @throws CodeException
254
     */
Carsten  Rose's avatar
Carsten Rose committed
255
    private static function doSystemPath(array $config) {
256

257
        // SYSTEM_PATH_EXT: compute only if not already defined.
258
        if (!isset($config[SYSTEM_EXT_PATH]) || $config[SYSTEM_EXT_PATH] === '' || $config[SYSTEM_EXT_PATH][0] !== '/') {
259
260
            $relExtDir = '/typo3conf/ext/' . EXT_KEY;

261
262
263
            if (defined('PHPUNIT_QFQ')) {
                $cwd = getcwd();
                $pos = strpos($cwd, '/typo3conf/');
264

265
266
                // this means phpUnit.
                $config[SYSTEM_SITE_PATH] = substr($cwd, 0, $pos);
267
                $config[SYSTEM_EXT_PATH] = $config[SYSTEM_SITE_PATH] . $relExtDir;
268
269
270

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

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

        return $config;
    }

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

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

299
        $config[SYSTEM_SEND_E_MAIL] = $config[SYSTEM_EXT_PATH] . '/Source/external/sendEmail';
300

301
302
303
304
305
306
307
308
        // 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];
        }

309
310
311
312
        if ($config[SYSTEM_THROW_GENERAL_ERROR] == 'auto') {
            $config[SYSTEM_THROW_GENERAL_ERROR] = $config[SYSTEM_FLAG_PRODUCTION] == 'yes' ? 'no' : 'yes';
        }

313
        return $config;
314
315
    }

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

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

329
330
331
332
333
334
335
336
337
338
339
        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);
340
341
    }

342
343
344
345
    /**
     * 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
346
     *
347
348
349
     * @throws UserFormException
     */
    private static function checkMandatoryParameter(array $config) {
350

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

356
357
        foreach ($names as $name) {
            if (!isset($config[$name])) {
358
                throw new qfq\UserFormException ("Missing configuration in `" . CONFIG_QFQ_PHP . "`: $name", ERROR_MISSING_CONFIG_INI_VALUE);
359
360
361
362
            }
        }
    }

363
    /**
364
365
     * @param $index
     * @return array
366
367
368
369
370
371
372
373
374
375
376
     */
    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;
    }

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

389
        // Check valid store name
390
        if (!isset(self::$sanitizeStore)) {
391
            throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE);
392
        }
Carsten  Rose's avatar
Carsten Rose committed
393

394
395
396
        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);
        }
397

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

402
403
404
405
        if ($store === STORE_EXTRA || $store === STORE_USER) {
            Session::set($store, $dataArray);
        }

406
407
        self::$raw[$store] = $dataArray;
    }
408

409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
    /**
     * 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.
     *
425
     * @param string $store STORE_SYSTEM, ...
426
427
428
429
430
431
432
433
     *
     * @return array
     * @throws UserFormException
     * @throws \qfq\CodeException
     */
    public static function getStore($store) {
        $vars = array();

434
        // Check valid store name
435
436
437
438
        if (!isset(self::$sanitizeStore[$store])) {
            throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE);
        }

439
440
        if ($store === STORE_ZERO || $store === STORE_EMPTY) {
            throw new CodeException("getStore() for $store is impossible - there are no values saved.", ERROR_GET_INVALID_STORE);
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
        }

        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.
     *
457
     * @param string $fileConfigPhp
458
459
460
461
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
462
    private static function fillStoreSystem($fileConfigPhp = '') {
463

464
        $config = Config::readConfig($fileConfigPhp);
465
466
467

        $config = self::doSystemPath($config);
        $config = self::adjustConfig($config);
468
        $config = self::setAutoConfigValue($config);
469
470
471
472
473
474

        self::checkMandatoryParameter($config);

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

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

483
484
        $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'] ?? '';
485
486
487
488

        return $config;
    }

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

501
        // form=, showDebugBodyText=, 10.20..
502
        $arr = KeyValueStringParser::parse($bodytext, "=", "\n");
503

504
        if (isset($GLOBALS["TSFE"])) {
505
            $arr = array_merge($arr, T3Info::getVars());
506
        } else {
507

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

516
        self::setStore($arr, STORE_TYPO3, true);
517
    }
518

519
    /**
Carsten  Rose's avatar
Carsten Rose committed
520
521
     * Fills the STORE_CLIENT
     *
522
     * @throws CodeException
523
     * @throws UserFormException
524
     */
525
    private static function fillStoreClient() {
526

Carsten  Rose's avatar
Carsten Rose committed
527
        $arr = Client::getParam();
528

529
        self::setStore($arr, STORE_CLIENT, true);
530

531
    }
532

Carsten  Rose's avatar
Carsten Rose committed
533
    /**
Carsten  Rose's avatar
Carsten Rose committed
534
535
     * Fills the STORE_SIP. Reads therefore specified SIP, decode the values and stores them in STORE_SIP.
     *
Carsten  Rose's avatar
Carsten Rose committed
536
     * @throws CodeException
537
     * @throws UserFormException
538
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
539
     */
540
    private static function fillStoreSip() {
Carsten  Rose's avatar
Carsten Rose committed
541

542
        self::$sip = new Sip(self::$phpUnit);
543

544
545
546
547
        $s = self::getVar(CLIENT_SIP, STORE_CLIENT);
        if ($s !== false) {
            // if session is given, copy values to store
            $param = self::$sip->getVarsFromSip($s);
548
549
            $param[SIP_SIP] = $s;
            $param[SIP_URLPARAM] = self::$sip->getQueryStringFromSip($s);
550

551
//            self::setVarArray(KeyValueStringParser::parse($param, "=", "&"), STORE_SIP);
552
            self::setStore($param, STORE_SIP, true);
553
554
555
        }
    }

556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
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
    /**
     * 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);
        }
    }

642
643
644
645
646
647
    /**
     * Unset a variable in store.
     *
     * @param $key
     * @param $store
     */
648
    public static function unsetVar($key, $store) {
649

650
        if (isset(self::$raw[$store][$key])) {
651
652
653
654
            unset(self::$raw[$store][$key]);
        }
    }

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

674
        // no store specified?
675
        if ($useStores === "" || $useStores === null) {
676
            $useStores = STORE_USE_DEFAULT;
677
678
        }

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

Carsten  Rose's avatar
Carsten Rose committed
684
685
        $len = strlen(SIP_PREFIX_BASE64);

686
687
        while ($useStores !== false && $useStores!=='') {
            $store = $useStores[0]; // current store
688
689
690
691
692
693

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

Carsten  Rose's avatar
Carsten Rose committed
694
            $foundInStore = $store;
695
696
            $useStores = substr($useStores, 1); // shift left remaining stores

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

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

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

729
                return $rawVal;
730
            }
731
        }
Carsten  Rose's avatar
Carsten Rose committed
732
        $foundInStore = '';
Carsten  Rose's avatar
Carsten Rose committed
733

734
        return false;
735
    }
736

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

747
        $recordId = self::getVar(CLIENT_RECORD_ID, STORE_TYPO3 . STORE_CLIENT);
748
749
750
751
        if ($recordId === false) {
            $recordId = 0;
        }

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

        // Construct fake urlparam
764
        $tmpUrlparam = OnArray::toString($tmpParam);
765
766

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

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

774
775
776
    }

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

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

Carsten  Rose's avatar
Carsten Rose committed
824
        return $vars;
825
826
    }

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

    /**
837
838
839
840
     * 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'
     *
841
842
843
844
845
846
847
     * @throws CodeException
     * @throws UserFormException
     */
    public static function copyT3VarsToSip() {

        $tempArray = self::getStore(STORE_TYPO3);

848
        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) {
849
850
851
852
853
854
855
856
857
858
859
860
861
862
            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;
    }

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

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

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

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

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

927
        $db = null;
928
        $flagDirty = false;
929
930
        $storeSystem = self::getStore(STORE_SYSTEM);

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

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

940
            $errMsg = "More than 1 record found. " . CONFIG_QFQ_PHP . ": " . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii";
941
942
943
            $mode = ROW_EXPECT_0_1;

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

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

955
        if ($flagDirty) {
956
            self::setStore($storeSystem, STORE_SYSTEM, true);
957
958
        }
    }
959
960

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

972
        if (empty($dataArray) || !is_array($dataArray)) {
973
974
            return;
        }
975

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

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

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