Store.php 33.6 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
        Logger::setSystemSitePath($config[SYSTEM_SITE_PATH]);

287
288
289
290
291
292
293
        return $config;
    }

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

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

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

303
304
305
306
307
308
309
        // Make path absolute
        foreach ([SYSTEM_MAIL_LOG, SYSTEM_QFQ_LOG, SYSTEM_SQL_LOG] AS $key) {
            if (!empty($config[$key]) && $config[$key][0] != '/') {
                $config[$key] = $config[SYSTEM_SITE_PATH] . '/' . $config[$key];
            }
        }

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

398
        // Check valid store name
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
    /**
     * 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.
     *
434
     * @param string $store STORE_SYSTEM, ...
435
436
437
438
439
440
441
442
     *
     * @return array
     * @throws UserFormException
     * @throws \qfq\CodeException
     */
    public static function getStore($store) {
        $vars = array();

443
        // Check valid store name
444
445
446
447
        if (!isset(self::$sanitizeStore[$store])) {
            throw new UserFormException("Unknown Store: $store", ERROR_UNNOWN_STORE);
        }

448
449
        if ($store === STORE_ZERO || $store === STORE_EMPTY) {
            throw new CodeException("getStore() for $store is impossible - there are no values saved.", ERROR_GET_INVALID_STORE);
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
        }

        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.
     *
466
     * @param string $fileConfigPhp
467
468
469
470
     * @throws CodeException
     * @throws UserFormException
     * @throws UserReportException
     */
471
    private static function fillStoreSystem($fileConfigPhp = '') {
472

473
        $config = Config::readConfig($fileConfigPhp);
474
475
476

        $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
     * @param string $typeMessageViolate
676
     * @return string|array a) if found: value, b) false. STORE_EXTRA returns an array for the given key.
677
     * @throws CodeException
678
     * @throws UserFormException    SANITIZE_TYPE_MESSAGE_VIOLATE_ZERO | SANITIZE_TYPE_MESSAGE_VIOLATE_EMPTY | SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS
679
     */
680
681
    public static function getVar($key, $useStores = STORE_USE_DEFAULT, $sanitizeClass = '', &$foundInStore = '',
                                  $typeMessageViolate = SANITIZE_TYPE_MESSAGE_VIOLATE_CLASS) {
682

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

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

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

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

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

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

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

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

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

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

743
        return false;
744
    }
745

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

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

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

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

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

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

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

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

783
784
785
    }

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

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

Carsten  Rose's avatar
Carsten Rose committed
833
        return $vars;
834
835
    }

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

    /**
846
847
848
849
     * 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'
     *
850
851
852
853
854
855
856
     * @throws CodeException
     * @throws UserFormException
     */
    public static function copyT3VarsToSip() {

        $tempArray = self::getStore(STORE_TYPO3);

857
        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) {
858
859
860
861
862
863
864
865
866
867
868
869
870
871
            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;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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