Store.php 33.4 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

Carsten  Rose's avatar
Carsten Rose committed
202
203
204
205
206
207
208
209
210
211
212
213
            if (self::$instance !== null && $fileConfigIni != '') {

                if($fileConfigIni=='init'){
                    $fileConfigIni='';
                }
                // fake to have a clean environment for the next test.
                self::unsetStore(STORE_TYPO3);
                self::fillStoreTypo3($bodytext);

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

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

        // Design Pattern: Singleton
        if (self::$instance === null) {
223
//            self::$phpUnit = $phpUnit;
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241

            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;
242
243
244
    }

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

254
255
256
257
        // 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;

258
259
260
            if (defined('PHPUNIT_QFQ')) {
                $cwd = getcwd();
                $pos = strpos($cwd, '/typo3conf/');
Carsten  Rose's avatar
Carsten Rose committed
261
                if ($pos == false) {
262
263
264
265
266
267
268
269
                    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.
270
                $pos = strpos($_SERVER['SCRIPT_FILENAME'], $relExtDir);
271
272
                if ($pos === false && isset($GLOBALS['TYPO3_LOADED_EXT'][EXT_KEY]['ext_localconf.php'])) {

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

        return $config;
    }

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

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

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

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

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

310
311
312
313
314
315
316
317
        // 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];
        }

318
319
320
321
        if ($config[SYSTEM_THROW_GENERAL_ERROR] == 'auto') {
            $config[SYSTEM_THROW_GENERAL_ERROR] = $config[SYSTEM_FLAG_PRODUCTION] == 'yes' ? 'no' : 'yes';
        }

322
        return $config;
323
324
    }

325
    /**
326
     * @param string $value
Carsten  Rose's avatar
Carsten Rose committed
327
     *
328
     * @param $flag
329
     * @return string
330
     */
331
    private static function adjustConfigShowDebugInfo($value, $flag) {
332

333
        // Check if SHOW_DEBUG_INFO contains 'auto'. Replace with appropriate.
334
335
        if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_AUTO, $value) && $flag) {
            $value = str_replace(SYSTEM_SHOW_DEBUG_INFO_AUTO, SYSTEM_SHOW_DEBUG_INFO_YES, $value);
336
337
        }

338
339
340
341
342
343
344
345
346
347
348
        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);
349
350
    }

351
352
353
354
    /**
     * 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
355
     *
356
357
358
     * @throws UserFormException
     */
    private static function checkMandatoryParameter(array $config) {
359

360
        // Check mandatory config vars.
361
        $names = array_merge([SYSTEM_SQL_LOG, SYSTEM_SQL_LOG_MODE],
362
363
364
            self::dbCredentialName($config[SYSTEM_DB_INDEX_DATA]),
            self::dbCredentialName($config[SYSTEM_DB_INDEX_QFQ]));

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

372
    /**
373
374
     * @param $index
     * @return array
375
376
377
378
379
380
381
382
383
384
385
     */
    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;
    }

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

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

403
404
405
        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);
        }
406

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

411
412
413
414
        if ($store === STORE_EXTRA || $store === STORE_USER) {
            Session::set($store, $dataArray);
        }

415
416
        self::$raw[$store] = $dataArray;
    }
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
473
474
475
476
    /**
     * 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);
477
        $config = self::setAutoConfigValue($config);
478
479
480
481
482
483

        self::checkMandatoryParameter($config);

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

484
485
486
487
488
489
490
491
    /**
     * Set automatic filled values
     *
     * @param array $config
     * @return array
     */
    public static function setAutoConfigValue(array $config) {

492
493
        $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'] ?? '';
494
495
496
497

        return $config;
    }

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

510
        // form=, showDebugBodyText=, 10.20..
511
        $arr = KeyValueStringParser::parse($bodytext, "=", "\n");
512

513
        if (isset($GLOBALS["TSFE"])) {
514
            $arr = array_merge($arr, T3Info::getVars());
515
        } else {
516

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

525
        self::setStore($arr, STORE_TYPO3, true);
526
    }
527

528
    /**
Carsten  Rose's avatar
Carsten Rose committed
529
530
     * Fills the STORE_CLIENT
     *
531
     * @throws CodeException
532
     * @throws UserFormException
533
     */
534
    private static function fillStoreClient() {
535

Carsten  Rose's avatar
Carsten Rose committed
536
        $arr = Client::getParam();
537

538
        self::setStore($arr, STORE_CLIENT, true);
539

540
    }
541

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

551
        self::$sip = new Sip(self::$phpUnit);
552

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

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

565
566
567
568
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
647
648
649
650
    /**
     * 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);
        }
    }

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

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

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

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

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

Carsten  Rose's avatar
Carsten Rose committed
691
692
        $len = strlen(SIP_PREFIX_BASE64);

693
694
        while ($useStores !== false) {
            $store = substr($useStores, 0, 1); // next store
695
696
697
698
699
700

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

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

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

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

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

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

741
        return false;
742
    }
743

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

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

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

        // Construct fake urlparam
771
        $tmpUrlparam = OnArray::toString($tmpParam);
772
773

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

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

781
782
783
    }

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

Carsten  Rose's avatar
Carsten Rose committed
813
814
815
816
817
    /**
     * 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
818
     *
Carsten  Rose's avatar
Carsten Rose committed
819
820
821
822
823
824
825
826
827
828
829
     * @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
830

Carsten  Rose's avatar
Carsten Rose committed
831
        return $vars;
832
833
    }

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

    /**
844
845
846
847
     * 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'
     *
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
     * @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;
    }

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

891
        $value = self::getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM);
892
893
        $flag = isset($typo3VarsArray[TYPO3_BE_USER_LOGGED_IN]) && ($typo3VarsArray[TYPO3_BE_USER_LOGGED_IN] == 'yes');
        $value = self::adjustConfigShowDebugInfo($value, $flag);
894
        self::setVar(SYSTEM_SHOW_DEBUG_INFO, $value, STORE_SYSTEM);
895
    }
896

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

917
            $record = $db->sql("SELECT * FROM $tableName WHERE $primaryKey = ?", ROW_EXPECT_1, [$recordId]);
918
919
920
921
            self::setStore($record, $store, true);
        }
    }

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

934
        $db = null;
935
        $flagDirty = false;
936
937
        $storeSystem = self::getStore(STORE_SYSTEM);

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

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

947
            $errMsg = "More than 1 record found. " . CONFIG_QFQ_PHP . ": " . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii";
948
949
950
            $mode = ROW_EXPECT_0_1;

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

956
            $storeSystemAdd = $db->sql($storeSystem[SYSTEM_FILL_STORE_SYSTEM_BY_SQL . "$ii"], $mode, array(), $errMsg);
957
958
            $storeSystemAdd = OnArray::keyNameRemoveLeadingUnderscore($storeSystemAdd);
            $storeSystem = array_merge($storeSystem, $storeSystemAdd);
959
            $flagDirty = true;
960
961
        }

962
        if ($flagDirty) {
963
            self::setStore($storeSystem, STORE_SYSTEM, true);
964
965
        }
    }
966
967

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

979
        if (empty($dataArray) || !is_array($dataArray)) {
980
981
            return;
        }
982

983
984
        if (isset($dataArray[0]) && is_array($dataArray[0])) {
            $append = $dataArray[0];
985
        } else {
986
            $append = $dataArray;
987
988
989
990
991
992
993
994
995
        }

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

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