Config.php 16.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 3/6/17
 * Time: 8:47 PM
 */

namespace qfq;

use qfq;

13
14
require_once(__DIR__ . '/../Constants.php');
require_once(__DIR__ . '/../helper/Support.php');
15

16
17
18
19
/**
 * Class Config
 * @package qfq
 */
20
21
class Config {

22
    /**
23
     * Migrate config.qfq.ini to config.qfq.php
24
     *
25
26
27
     * @param $configIni
     * @param $configPhp
     */
28
    private static function migrateConfigIniToPhp($configIni, $configPhp) {
29
30
31
32
33
34

        $config = parse_ini_file($configIni, false);

        $pre = isset($config[SYSTEM_DB_NAME]) ? 'DB' : 'DB_1';

        $content = '<?php' . PHP_EOL . 'return [' . PHP_EOL;
35

36
        foreach ([$pre . '_NAME', $pre . '_PASSWORD', $pre . '_SERVER', $pre . '_USER', SYSTEM_LDAP_1_RDN, SYSTEM_LDAP_1_PASSWORD] as $key) {
Carsten  Rose's avatar
Carsten Rose committed
37
            $content .= "  '$key' => '" . ($config[$key] ?? "") . "'," . PHP_EOL;
38
39
        }

40
41
42
43
44
45
46
47
48
        $content .= "];" . PHP_EOL;

        // Write new config file
        file_put_contents($configPhp, $content);

        // Make old file unreadable
        chmod($configIni, 000);
    }

49
50
51
52
53
54
55
    /**
     * Iterates over all 30 custom vars, explode them to split between key and value, append to $config.
     *
     * @param array $config
     * @return array
     * @throws UserReportException
     */
56
    private static function getCustomVariable(array $config) {
57
58
59
60
61
62

        for ($i = 1; $i <= 30; $i++) {
            if (isset($config['custom' . $i])) {
                $arr = explode('=', $config['custom' . $i], 2);
                if (!empty($arr[0]) && !empty($arr[1])) {

63
                    $arr[0] = trim($arr[0]);
64
                    $arr[1] = OnString::trimQuote(trim($arr[1]));
65
66
67
68
69
70
71
72
73
74
75
76
77

                    if (isset($config[$arr[0]])) {
                        throw new UserReportException("Variable '$arr[0]' already defined", ERROR_INVALID_OR_MISSING_PARAMETER);
                    }

                    $config[$arr[0]] = $arr[1];
                }
            }
        }

        return $config;
    }

78
    /**
Carsten  Rose's avatar
Carsten Rose committed
79
     * Read config.qfq.ini. In case
80
     *
81
     * @param string $fileConfigPhp
82
     * @return array
83
     * @throws CodeException
84
     * @throws UserFormException
85
     * @throws UserReportException
86
     */
87
    public static function readConfig($fileConfigPhp = '') {
Carsten  Rose's avatar
Carsten Rose committed
88

89
        $configT3qfq = array();
90
        $configIni = ''; // outdated config file format
91
92
93
94
95
96
97
98

        // Production Path to CONFIG_INI
        $pathTypo3Conf = __DIR__ . '/../../../../..';
        if (!file_exists($pathTypo3Conf . '/' . CONFIG_T3)) {
            // PHPUnit Path to CONFIG_INI
            $pathTypo3Conf = __DIR__ . '/../../..';
        }

99
        // In case of missing $configPhp
100
        if (empty($fileConfigPhp)) {
101

102
103
104
            # Read 'LocalConfiguration.php'
            if (isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][EXT_KEY])) {
                $configT3qfq = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][EXT_KEY]);
105
                $configT3qfq[SYSTEM_DB_NAME_T3] = self::getDbName($GLOBALS['TYPO3_CONF_VARS']['DB']);
106

107
            } else {
108
                $all = include($pathTypo3Conf . '/' . CONFIG_T3);
109
                if (empty($all) || $all === true) {
110
111
112
                    throw new qfq\UserFormException ("Error read file: " . $pathTypo3Conf . '/' . CONFIG_T3, ERROR_IO_READ_FILE);
                }

113
                $configT3qfq = unserialize($all['EXT']['extConf'][EXT_KEY]);
114
                if (!is_array($configT3qfq)) {
115
116
                    throw new qfq\UserFormException ("Error read file: " . $pathTypo3Conf . '/' . CONFIG_T3, ERROR_IO_READ_FILE);
                }
117

118
                $configT3qfq[SYSTEM_DB_NAME_T3] = self::getDbName($all['DB']);
119
                unset($all);
120
            }
121

122
            $configIni = $pathTypo3Conf . '/' . CONFIG_QFQ_INI;
123
            $fileConfigPhp = $pathTypo3Conf . '/' . CONFIG_QFQ_PHP;
124

125
126
127
        }

        // Migrate legacy config file.
128
129
        if (is_readable($configIni) && !is_readable($fileConfigPhp)) {
            self::migrateConfigIniToPhp($configIni, $fileConfigPhp);
130
131
        }

132
        $config = include($fileConfigPhp);
133

134
        if ($config === false) {
135
            throw new qfq\UserFormException ("Error read file: " . $fileConfigPhp, ERROR_IO_READ_FILE);
136
        }
137

138
139
140
        // in case $configIni doesn't exist: just skip
        if (!is_array($config)) {
            $config = array();
141
        }
142

143
        $configT3qfq = self::getCustomVariable($configT3qfq);
144

145
        // Settings in  config.qfq.php overwrite T3 settings
146
        $config = array_merge($configT3qfq, $config);
147
148

        $config = self::renameConfigElements($config);
149
        $config = self::setDefaults($config);
150
        self::checkDeprecated($config);
151

152
153
        self::checkForAttack($config);

154
        // Copy values to detect custom settings later
155
        $config[F_FE_DATA_PATTERN_ERROR_SYSTEM] = $config[F_FE_DATA_PATTERN_ERROR];
156

157
158
159
        return $config;
    }

160
161
162
163
164
165
    /**
     * Returns T3 DB-Name, depending on T3 version
     *
     * @param array $db
     * @return mixed
     */
Carsten  Rose's avatar
Carsten Rose committed
166
    private static function getDbName(array $db) {
167
168
169
170
171

        // T3 7.x: $GLOBALS['TYPO3_CONF_VARS']['DB']['database'],  T3 8.x: $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']
        return isset($db['database']) ? $db['database'] : $db['Connections']['Default']['dbname'];
    }

172
173
    /**
     * Checks for deprecated options.
174
     *
175
176
     * @param array $config
     * @throws UserFormException
177
178
179
180
181
182
183
184
185
     */
    private static function checkDeprecated(array $config) {

        foreach ([SYSTEM_VAR_ADD_BY_SQL] as $key) {

            if (isset($config[$key])) {
                $msg = '';
                switch ($key) {
                    case SYSTEM_VAR_ADD_BY_SQL:
186
                        $msg = 'Replaced by: ' . SYSTEM_FILL_STORE_SYSTEM_BY_SQL . '1|2|3';
187
                }
188
                throw new qfq\UserFormException ("Deprecated option in " . CONFIG_QFQ_PHP . ": " . SYSTEM_VAR_ADD_BY_SQL . " - " . $msg);
189
190
191
192
193
            }
        }
    }


194
    /**
195
196
     * Check for attack
     *
197
     * @param array $config
Carsten  Rose's avatar
Carsten Rose committed
198
     * @throws CodeException
199
     * @throws UserFormException
200
     * @throws UserReportException
201
     */
202
    public static function checkForAttack(array $config) {
203
        $attack = false;
204
        $key = '';
205
        $reason = 'Problem: ';
206
207
208
209

        // Iterate over all fake vars
        $arr = explode(',', $config[SYSTEM_SECURITY_VARS_HONEYPOT]);
        foreach ($arr as $key) {
210

211
212
213
214
            $key = trim($key);
            if ($key === '') {
                continue;
            }
215
216

            if (!empty($_POST[$key])) {
217
                $attack = true;
218
                $reason .= "Post/Get Honeypot variable '$key' detected: " . htmlentities($_POST[$key]) . PHP_EOL;
219
220
221
222
223
            }
        }

        // Limit length of all get vars: protect against SQL injection based on long ...%34%34%24%34...
        $maxLength = $config[SYSTEM_SECURITY_GET_MAX_LENGTH];
224
        if ($maxLength > 0 && $attack === false) {
225
            foreach ($_GET as $key => $value) {
Carsten  Rose's avatar
Carsten Rose committed
226
227
228
229
230

                if (!is_string($value)) {
                    continue;
                }

231
232
                // Check if the variable is something like 'my_name_100' - if the part after the last '_' is numerical, this means a valid, non standard length.
                $arr = explode(GET_EXTRA_LENGTH_TOKEN, $key);
233

234
235
                $cnt = count($arr);
                if ($cnt > 1 && is_numeric($arr[$cnt - 1])) {
236
237
238
239
240
                    $maxLength = $arr[$cnt - 1];
                } else {
                    $maxLength = $config[SYSTEM_SECURITY_GET_MAX_LENGTH]; // might change again.
                }

241
242
                $len = strlen($value);
                if ($len > $maxLength) {
243
                    $attack = true;
244
                    $reason .= "Value of GET variable '$key' too long. Allowed: $maxLength, Length: $len. Value: '" . htmlentities($_GET[$key]) . "'" . PHP_EOL;
245
                }
246
247
248
249
            }
        }

        // Nothing found?
250
        if ($attack === false) {
251
252
253
            return;
        }

254
        self::attackDetectedExitNow($config, $reason);
255
256
257
    }

    /**
258
     * @param array $config
259
     * @param string $reason
Carsten  Rose's avatar
Carsten Rose committed
260
     * @throws CodeException
261
     * @throws UserFormException
262
     * @throws UserReportException
263
     */
264
    public static function attackDetectedExitNow(array $config = array(), $reason = '') {
265
266
267

        if (count($config) == 0) {
            $config = self::readConfig();
268
269
        }

270
        Logger::logMessage(Logger::linePre() . 'Security: attack detected' . PHP_EOL . $reason,
271
            $config[SYSTEM_QFQ_LOG] ?? SYSTEM_QFQ_LOG_FILE);
272

273
274
275
        // In case of an attack: log out the current user.
        Session::destroy();

276
277
278
279
        // Sleep
        $penalty = (empty($config[SYSTEM_SECURITY_ATTACK_DELAY]) || !is_numeric($config[SYSTEM_SECURITY_ATTACK_DELAY])) ?
            SYSTEM_SECURITY_ATTACK_DELAY_DEFAULT : $config[SYSTEM_SECURITY_ATTACK_DELAY];

Carsten  Rose's avatar
Carsten Rose committed
280
281
282
        if (!defined('PHPUNIT_QFQ')) {
            sleep($penalty);
        }
283

284
        if ($config[SYSTEM_SECURITY_SHOW_MESSAGE] == 'true' || $config[SYSTEM_SECURITY_SHOW_MESSAGE] == 1) {
285

286
            echo "Attack detected - stop process <p>" . $reason . '</p>';
287
288
289
290
291
292
293
//            $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
//            $answer[API_MESSAGE] = 'Attack detected - stop process.';
//            if($getParamName!='') {
//                $answer[API_MESSAGE] .= " Attack parameter: $getParamName";
//            }
//            header("Content-Type: application/json");
//            echo json_encode($answer);
294
295
        }

296
297
298
299
        if (defined('PHPUNIT_QFQ')) {
            throw new UserFormException('Attack detected', 1);
        }

300
301
302
303
304
        exit;
    }

    /**
     * @param array $config
Carsten  Rose's avatar
Carsten Rose committed
305
     *
306
307
     * @return array
     */
308
    public static function setDefaults(array $config) {
309
310
311

        $default = [

312
            SYSTEM_DB_INIT => 'set names utf8',
313
314
            SYSTEM_DB_INDEX_DATA => DB_INDEX_DEFAULT,
            SYSTEM_DB_INDEX_QFQ => DB_INDEX_DEFAULT,
315
316
317

            SYSTEM_DATE_FORMAT => 'yyyy-mm-dd',
            SYSTEM_SHOW_DEBUG_INFO => SYSTEM_SHOW_DEBUG_INFO_AUTO,
318
            SYSTEM_MAIL_LOG => SYSTEM_MAIL_LOG_FILE,
319
            SYSTEM_QFQ_LOG => SYSTEM_QFQ_LOG_FILE,
320
321
            SYSTEM_SQL_LOG => SYSTEM_SQL_LOG_FILE,
            SYSTEM_SQL_LOG_MODE => 'modify',
322
323
324
325
            F_BS_COLUMNS => 'col-md-12 col-lg-10',
            F_BS_LABEL_COLUMNS => 'col-md-3 col-lg-3',
            F_BS_INPUT_COLUMNS => 'col-md-6 col-lg-6',
            F_BS_NOTE_COLUMNS => 'col-md-3 col-lg-3',
326

327
            SYSTEM_CMD_WKHTMLTOPDF => '/opt/wkhtmltox/bin/wkhtmltopdf',
328
329
330
331
332

            F_CLASS_PILL => 'qfq-color-grey-1',
            F_CLASS_BODY => 'qfq-color-grey-2',

            F_SAVE_BUTTON_TEXT => '',
333
            F_SAVE_BUTTON_TOOLTIP => '',
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
            F_SAVE_BUTTON_CLASS => 'btn btn-default navbar-btn',
            F_SAVE_BUTTON_GLYPH_ICON => GLYPH_ICON_CHECK,

            F_CLOSE_BUTTON_TEXT => '',
            F_CLOSE_BUTTON_TOOLTIP => 'Close',
            F_CLOSE_BUTTON_CLASS => 'btn btn-default navbar-btn',
            F_CLOSE_BUTTON_GLYPH_ICON => GLYPH_ICON_CLOSE,

            F_DELETE_BUTTON_TEXT => '',
            F_DELETE_BUTTON_TOOLTIP => 'Delete',
            F_DELETE_BUTTON_CLASS => 'btn btn-default navbar-btn',
            F_DELETE_BUTTON_GLYPH_ICON => GLYPH_ICON_DELETE,

            F_NEW_BUTTON_TEXT => '',
            F_NEW_BUTTON_TOOLTIP => 'New',
            F_NEW_BUTTON_CLASS => 'btn btn-default navbar-btn',
            F_NEW_BUTTON_GLYPH_ICON => GLYPH_ICON_NEW,

            F_BUTTON_ON_CHANGE_CLASS => 'btn-info alert-info',
            SYSTEM_EDIT_FORM_PAGE => 'form',
354
            SYSTEM_SECURITY_VARS_HONEYPOT => SYSTEM_SECURITY_VARS_HONEYPOT_NAMES,
355
356
357
            SYSTEM_SECURITY_ATTACK_DELAY => SYSTEM_SECURITY_ATTACK_DELAY_DEFAULT,
            SYSTEM_SECURITY_SHOW_MESSAGE => '0',
            SYSTEM_SECURITY_GET_MAX_LENGTH => SYSTEM_SECURITY_GET_MAX_LENGTH_DEFAULT,
358
359
360

            SYSTEM_LABEL_ALIGN => SYSTEM_LABEL_ALIGN_LEFT,

361
            SYSTEM_ESCAPE_TYPE_DEFAULT => TOKEN_ESCAPE_MYSQL,
362
363
            SYSTEM_EXTRA_BUTTON_INFO_INLINE => '<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>',
            SYSTEM_EXTRA_BUTTON_INFO_BELOW => '<span class="glyphicon glyphicon-info-sign text-info" aria-hidden="true"></span>',
364
365
366
367
368
            SYSTEM_EXTRA_BUTTON_INFO_CLASS => '',

            SYSTEM_DB_UPDATE => SYSTEM_DB_UPDATE_AUTO,
            SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS => SYSTEM_RECORD_LOCK_TIMEOUT_SECONDS_DEFAULT,

369
            SYSTEM_SESSION_TIMEOUT_SECONDS => self::getPhpSessionTimeout(),
370

371
            SYSTEM_DOCUMENTATION_QFQ => SYSTEM_DOCUMENTATION_QFQ_URL,
372
373
374
375
376
377
378
            SYSTEM_ENTER_AS_SUBMIT => 1,

            SYSTEM_CMD_INKSCAPE => 'inkscape',
            SYSTEM_CMD_CONVERT => 'convert',
            SYSTEM_THUMBNAIL_DIR_SECURE => SYSTEM_THUMBNAIL_DIR_SECURE_DEFAULT,
            SYSTEM_THUMBNAIL_DIR_PUBLIC => SYSTEM_THUMBNAIL_DIR_PUBLIC_DEFAULT,

379
            F_FE_DATA_REQUIRED_ERROR => F_FE_DATA_REQUIRED_ERROR_DEFAULT,
380
            F_FE_DATA_MATCH_ERROR => F_FE_DATA_MATCH_ERROR_DEFAULT,
381
            F_FE_DATA_ERROR => F_FE_DATA_ERROR_DEFAULT,
382
            F_FE_DATA_PATTERN_ERROR => F_FE_DATA_PATTERN_ERROR_DEFAULT,
383
384
385

            SYSTEM_FLAG_PRODUCTION => 'yes',
            SYSTEM_THROW_GENERAL_ERROR => 'auto',
Carsten  Rose's avatar
Carsten Rose committed
386
387

            SYSTEM_SECURITY_FAILED_AUTH_DELAY => '3',
388
389
390

            SYSTEM_FILE_MAX_FILE_SIZE => min(Support::returnBytes(ini_get('post_max_size')), Support::returnBytes(ini_get('upload_max_filesize'))),

391
392
        ];

393
394
395
396
397
398
        foreach ($default as $key => $value) {
            if (!isset($config[$key]) || $config[$key] == '') {
                $config[$key] = $value;
            }
        }

Marc Egger's avatar
Newdoc    
Marc Egger committed
399
400
401
402
403
        // don't accept deprecated documentation url
        if ($config[SYSTEM_DOCUMENTATION_QFQ] === 'https://docs.typo3.org/typo3cms/drafts/github/T3DocumentationStarter/Public-Info-053/Manual.html') {
            $config[SYSTEM_DOCUMENTATION_QFQ] = SYSTEM_DOCUMENTATION_QFQ_URL;
        }

404
        return $config;
405
406
    }

407

408
409
    /**
     * Rename Elements defined in config.qfq.ini to more appropriate in user interaction.
Carsten  Rose's avatar
Carsten Rose committed
410
411
     * E.g.: in config.qfq.ini everything is in upper case and word space is '_'. In Form.parameter it's lowercase and
     * camel hook.
412
413
     *
     * @param array $config
Carsten  Rose's avatar
Carsten Rose committed
414
     *
415
416
417
418
419
420
     * @return array
     */
    private static function renameConfigElements(array $config) {

        // oldname > newname
        $setting = [
421
            [SYSTEM_FORM_BS_COLUMNS, F_BS_COLUMNS],
422
423
424
            [SYSTEM_FORM_BS_LABEL_COLUMNS, F_BS_LABEL_COLUMNS],
            [SYSTEM_FORM_BS_INPUT_COLUMNS, F_BS_INPUT_COLUMNS],
            [SYSTEM_FORM_BS_NOTE_COLUMNS, F_BS_NOTE_COLUMNS],
425

426
427
428
429
            [SYSTEM_FORM_DATA_PATTERN_ERROR, F_FE_DATA_PATTERN_ERROR],
            [SYSTEM_FORM_DATA_REQUIRED_ERROR, F_FE_DATA_REQUIRED_ERROR],
            [SYSTEM_FORM_DATA_MATCH_ERROR, F_FE_DATA_MATCH_ERROR],
            [SYSTEM_FORM_DATA_ERROR, F_FE_DATA_ERROR],
430

431
432
433
            [SYSTEM_CSS_CLASS_QFQ_FORM, F_CLASS],
            [SYSTEM_CSS_CLASS_QFQ_FORM_PILL, F_CLASS_PILL],
            [SYSTEM_CSS_CLASS_QFQ_FORM_BODY, F_CLASS_BODY],
434
            [SYSTEM_SAVE_BUTTON_CLASS_ON_CHANGE, F_BUTTON_ON_CHANGE_CLASS],
435

436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
        ];

        foreach ($setting as $row) {
            $oldName = $row[0];
            $newName = $row[1];

            if (isset($config[$oldName])) {
                $config[$newName] = $config[$oldName];
                if ($oldName != $newName) {
                    unset($config[$oldName]);
                }
            }
        }

        return $config;
    }
452
453

    /**
454
455
     * Check Session Timeout
     *
456
457
458
459
460
     * @param $timeout
     * @throws UserFormException
     */
    public static function checkSessionTimeout($timeout) {

461
        if (self::getPhpSessionTimeout() < $timeout) {
462
463
464
            throw new qfq\UserFormException ("The specified timeout of $timeout seconds is higher than the PHP config 'session.gc_maxlifetime' ("
                . ini_get('session.gc_maxlifetime') . ") and/or 'session.cookie_lifetime' ("
                . ini_get('session.cookie_lifetime') . ")");
465
466
467
        }

        if ($timeout > SYSTEM_COOKIE_LIFETIME) {
468
            throw new qfq\UserFormException ("The specified timeout of $timeout seconds is higher than the hardcoded cookie lifetime (" . SYSTEM_COOKIE_LIFETIME . ")");
469
470
        }
    }
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492

    /**
     * Get the minimum of 'session.cookie_lifetime' and 'session.gc_maxlifetime'
     * A zero means unlimited and will be limited to one day.
     *
     * @return int|string
     */
    private static function getPhpSessionTimeout() {
        $timeout = ini_get('session.cookie_lifetime');
        $gc_maxlifetime = ini_get('session.gc_maxlifetime');

        if ($timeout == 0) {
            $timeout = 86400;
        }

        if ($gc_maxlifetime == 0) {
            // Just to set a limit
            $timeout = 86400;
        }

        return $timeout > $gc_maxlifetime ? $gc_maxlifetime : $timeout;
    }
493
}