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 $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
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
        self::fillStoreTypo3($bodytext);  // should be filled before fillStoreSystem() to offer T3 variables, especially 'feUser'
175
        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($fileConfigPhp);
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 $qfqConfigPhpUnit
188
189
     *
     * @return null|Store
190
     * @throws CodeException
191
     * @throws UserFormException
192
     * @throws UserReportException
193
     */
194
    public static function getInstance($bodytext = '', $phpUnit = false, $qfqConfigPhpUnit = '') {
195

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

        if (self::$phpUnit) {
201

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

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

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

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

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

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

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

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

260
261
262
            if (defined('PHPUNIT_QFQ')) {
                $cwd = getcwd();
                $pos = strpos($cwd, '/typo3conf/');
Carsten  Rose's avatar
Carsten Rose committed
263
                if ($pos == false) {
264
265
266
267
                    throw new CodeException("Directory component '/typo3conf/' not found in '$cwd'", 1);
                }
                // this means phpUnit.
                $config[SYSTEM_SITE_PATH] = substr($cwd, 0, $pos);
268
                $config[SYSTEM_EXT_PATH] = $config[SYSTEM_SITE_PATH] . $relExtDir;
269
270
271

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

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

        return $config;
    }

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

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

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

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

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

314
        return $config;
315
316
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self::checkMandatoryParameter($config);

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

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

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

        return $config;
    }

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

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

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

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

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

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

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

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

532
    }
533

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

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

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

552
//            self::setVarArray(KeyValueStringParser::parse($param, "=", "&"), STORE_SIP);
553
            self::setStore($param, STORE_SIP, true);
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
642
    /**
     * 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);
        }
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

735
        return false;
736
    }
737

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

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

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

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

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

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

775
776
777
    }

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

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

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

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

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

        $tempArray = self::getStore(STORE_TYPO3);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

977
978
        if (isset($dataArray[0]) && is_array($dataArray[0])) {
            $append = $dataArray[0];
979
        } else {
980
            $append = $dataArray;
981