Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.13% covered (success)
89.13%
82 / 92
80.00% covered (success)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Configuration
89.13% covered (success)
89.13%
82 / 92
80.00% covered (success)
80.00%
4 / 5
47.60
0.00% covered (danger)
0.00%
0 / 1
 __construct
87.95% covered (success)
87.95%
73 / 83
0.00% covered (danger)
0.00%
0 / 1
41.66
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaults
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getSection
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php declare(strict_types=1);
2/**
3 * PrivateBin
4 *
5 * a zero-knowledge paste bin
6 *
7 * @link      https://github.com/PrivateBin/PrivateBin
8 * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
9 * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
10 */
11
12namespace PrivateBin;
13
14use Exception;
15
16/**
17 * Configuration
18 *
19 * parses configuration file, ensures default values present
20 */
21class Configuration
22{
23    /**
24     * parsed configuration
25     *
26     * @var array
27     */
28    private $_configuration;
29
30    /**
31     * default configuration
32     *
33     * @var array
34     */
35    private static $_defaults = array(
36        'main' => array(
37            'name'                     => 'PrivateBin',
38            'basepath'                 => '',
39            'discussion'               => true,
40            'opendiscussion'           => false,
41            'discussiondatedisplay'    => true,
42            'password'                 => true,
43            'fileupload'               => false,
44            'burnafterreadingselected' => false,
45            'defaultformatter'         => 'plaintext',
46            'syntaxhighlightingtheme'  => '',
47            'sizelimit'                => 10485760,
48            'templateselection'        => false,
49            'template'                 => 'bootstrap5',
50            'availabletemplates'       => array(
51                'bootstrap5',
52                'bootstrap',
53                'bootstrap-page',
54                'bootstrap-dark',
55                'bootstrap-dark-page',
56                'bootstrap-compact',
57                'bootstrap-compact-page',
58            ),
59            'info'                     => 'More information on the <a href=\'https://privatebin.info/\'>project page</a>.',
60            'notice'                   => '',
61            'languageselection'        => false,
62            'languagedefault'          => '',
63            'urlshortener'             => '',
64            'qrcode'                   => true,
65            'email'                    => true,
66            'icon'                     => 'jdenticon',
67            'cspheader'                => 'default-src \'none\'; base-uri \'self\'; form-action \'none\'; manifest-src \'self\'; connect-src * blob:; script-src \'self\' \'wasm-unsafe-eval\'; style-src \'self\'; font-src \'self\'; frame-ancestors \'none\'; frame-src blob:; img-src \'self\' data: blob:; media-src blob:; object-src blob:; sandbox allow-same-origin allow-scripts allow-forms allow-modals allow-downloads',
68            'httpwarning'              => true,
69            'compression'              => 'zlib',
70        ),
71        'expire' => array(
72            'default' => '1week',
73        ),
74        'expire_options' => array(
75            '5min'   => 300,
76            '10min'  => 600,
77            '1hour'  => 3600,
78            '1day'   => 86400,
79            '1week'  => 604800,
80            '1month' => 2592000,
81            '1year'  => 31536000,
82            'never'  => 0,
83        ),
84        'formatter_options' => array(
85            'plaintext'          => 'Plain Text',
86            'syntaxhighlighting' => 'Source Code',
87            'markdown'           => 'Markdown',
88        ),
89        'traffic' => array(
90            'limit'     => 10,
91            'header'    => '',
92            'exempted'  => '',
93            'creators'  => '',
94        ),
95        'purge' => array(
96            'limit'     => 300,
97            'batchsize' => 10,
98        ),
99        'model' => array(
100            'class' => 'Filesystem',
101        ),
102        'model_options' => array(
103            'dir' => 'data',
104        ),
105        'yourls' => array(
106            'signature' => '',
107            'apiurl'    => '',
108        ),
109        // update this array when adding/changing/removing js files
110        'sri' => array(
111            'js/base-x-5.0.1.js'     => 'sha512-FmhlnjIxQyxkkxQmzf0l6IRGsGbgyCdgqPxypFsEtHMF1naRqaLLo6mcyN5rEaT16nKx1PeJ4g7+07D6gnk/Tg==',
112            'js/bootstrap-3.4.1.js'  => 'sha512-oBTprMeNEKCnqfuqKd6sbvFzmFQtlXS3e0C/RGFV0hD6QzhHV+ODfaQbAlmY6/q0ubbwlAM/nCJjkrgA3waLzg==',
113            'js/bootstrap-5.3.7.js'  => 'sha512-UqmrCkPcp6WOB9cC/NB5GB7vQd2/sB70bLpFk0bqHz/WQIFucjAM0vFNI4xp8B7jJ8KIUWPblNAS/M30AHKSzA==',
114            'js/dark-mode-switch.js' => 'sha512-BhY7dNU14aDN5L+muoUmA66x0CkYUWkQT0nxhKBLP/o2d7jE025+dvWJa4OiYffBGEFgmhrD/Sp+QMkxGMTz2g==',
115            'js/jquery-3.7.1.js'     => 'sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==',
116            'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
117            'js/legacy.js'           => 'sha512-08+subq1Lo+r+la5ENqeXiMgNJcVaaTtBIFGkrjziSpvtgCId3Jtin4/OkSdHYSoeztwwIab8uvCzPKHta6puQ==',
118            'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
119            'js/privatebin.js'       => 'sha512-ytZMcsBxoon+uFaTyES2QBm0oN445Fu1iE4txInHaME1wpo3NUu02gxOvjrZhCimM59wTLgSMhm60BxE/DIm3w==',
120            'js/purify-3.2.6.js'     => 'sha512-zqwL4OoBLFx89QPewkz4Lz5CSA2ktU+f31fuECkF0iK3Id5qd3Zpq5dMby8KwHjIEpsUgOqwF58cnmcaNem0EA==',
121            'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
122            'js/zlib-1.3.1-1.js'     => 'sha512-5bU9IIP4PgBrOKLZvGWJD4kgfQrkTz8Z3Iqeu058mbQzW3mCumOU6M3UVbVZU9rrVoVwaW4cZK8U8h5xjF88eQ==',
123        ),
124    );
125
126    /**
127     * parse configuration file and ensure default configuration values are present
128     *
129     * @throws Exception
130     */
131    public function __construct()
132    {
133        $basePaths  = array();
134        $config     = array();
135        $configPath = getenv('CONFIG_PATH');
136        if ($configPath !== false && !empty($configPath)) {
137            $basePaths[] = $configPath;
138        }
139        $basePaths[] = PATH . 'cfg';
140        foreach ($basePaths as $basePath) {
141            $configFile = $basePath . DIRECTORY_SEPARATOR . 'conf.php';
142            if (is_readable($configFile)) {
143                $config = parse_ini_file($configFile, true);
144                foreach (array('main', 'model', 'model_options') as $section) {
145                    if (!array_key_exists($section, $config)) {
146                        throw new Exception(I18n::_('PrivateBin requires configuration section [%s] to be present in configuration file.', $section), 2);
147                    }
148                }
149                break;
150            }
151        }
152
153        $opts = '_options';
154        foreach (self::getDefaults() as $section => $values) {
155            // fill missing sections with default values
156            if (!array_key_exists($section, $config) || count($config[$section]) == 0) {
157                $this->_configuration[$section] = $values;
158                if (array_key_exists('dir', $this->_configuration[$section])) {
159                    $this->_configuration[$section]['dir'] = PATH . $this->_configuration[$section]['dir'];
160                }
161                continue;
162            }
163            // provide different defaults for database model
164            elseif (
165                $section == 'model_options' &&
166                $this->_configuration['model']['class'] === 'Database'
167            ) {
168                $values = array(
169                    'dsn' => 'sqlite:' . PATH . 'data' . DIRECTORY_SEPARATOR . 'db.sq3',
170                    'tbl' => null,
171                    'usr' => null,
172                    'pwd' => null,
173                    'opt' => array(),
174                );
175            } elseif (
176                $section == 'model_options' &&
177                $this->_configuration['model']['class'] === 'GoogleCloudStorage'
178            ) {
179                $values = array(
180                    'bucket'     => getenv('PRIVATEBIN_GCS_BUCKET') ? getenv('PRIVATEBIN_GCS_BUCKET') : null,
181                    'prefix'     => 'pastes',
182                    'uniformacl' => false,
183                );
184            } elseif (
185                $section == 'model_options' &&
186                $this->_configuration['model']['class'] === 'S3Storage'
187            ) {
188                $values = array(
189                    'region'                  => null,
190                    'version'                 => null,
191                    'endpoint'                => null,
192                    'accesskey'               => null,
193                    'secretkey'               => null,
194                    'use_path_style_endpoint' => null,
195                    'bucket'                  => null,
196                    'prefix'                  => '',
197                );
198            }
199
200            // "*_options" sections don't require all defaults to be set
201            if (
202                $section !== 'model_options' &&
203                ($from = strlen($section) - strlen($opts)) >= 0 &&
204                strpos($section, $opts, $from) !== false
205            ) {
206                if (is_int(current($values))) {
207                    $config[$section] = array_map('intval', $config[$section]);
208                }
209                $this->_configuration[$section] = $config[$section];
210            }
211            // check for missing keys and set defaults if necessary
212            else {
213                // preserve configured SRI hashes
214                if ($section == 'sri' && array_key_exists($section, $config)) {
215                    $this->_configuration[$section] = $config[$section];
216                }
217                foreach ($values as $key => $val) {
218                    if ($key == 'dir') {
219                        $val = PATH . $val;
220                    }
221                    $result = $val;
222                    if (array_key_exists($key, $config[$section])) {
223                        if ($val === null) {
224                            $result = $config[$section][$key];
225                        } elseif (is_bool($val)) {
226                            $val = strtolower($config[$section][$key]);
227                            if (in_array($val, array('true', 'yes', 'on'))) {
228                                $result = true;
229                            } elseif (in_array($val, array('false', 'no', 'off'))) {
230                                $result = false;
231                            } else {
232                                $result = (bool) $config[$section][$key];
233                            }
234                        } elseif (is_int($val)) {
235                            $result = (int) $config[$section][$key];
236                        } elseif (is_string($val) && !empty($config[$section][$key])) {
237                            $result = (string) $config[$section][$key];
238                        } elseif (is_array($val) && is_array($config[$section][$key])) {
239                            $result = $config[$section][$key];
240                        }
241                    }
242                    $this->_configuration[$section][$key] = $result;
243                }
244            }
245        }
246
247        // ensure a valid expire default key is set
248        if (!array_key_exists($this->_configuration['expire']['default'], $this->_configuration['expire_options'])) {
249            $this->_configuration['expire']['default'] = key($this->_configuration['expire_options']);
250        }
251
252        // ensure the basepath ends in a slash, if one is set
253        if (
254            !empty($this->_configuration['main']['basepath']) &&
255            substr_compare($this->_configuration['main']['basepath'], '/', -1) !== 0
256        ) {
257            $this->_configuration['main']['basepath'] .= '/';
258        }
259    }
260
261    /**
262     * get configuration as array
263     *
264     * @return array
265     */
266    public function get()
267    {
268        return $this->_configuration;
269    }
270
271    /**
272     * get default configuration as array
273     *
274     * @return array
275     */
276    public static function getDefaults()
277    {
278        return self::$_defaults;
279    }
280
281    /**
282     * get a key from the configuration, typically the main section or all keys
283     *
284     * @param string $key
285     * @param string $section defaults to main
286     * @throws Exception
287     * @return mixed
288     */
289    public function getKey($key, $section = 'main')
290    {
291        $options = $this->getSection($section);
292        if (!array_key_exists($key, $options)) {
293            throw new Exception(I18n::_('Invalid data.') . " $section / $key", 4);
294        }
295        return $this->_configuration[$section][$key];
296    }
297
298    /**
299     * get a section from the configuration, must exist
300     *
301     * @param string $section
302     * @throws Exception
303     * @return mixed
304     */
305    public function getSection($section)
306    {
307        if (!array_key_exists($section, $this->_configuration)) {
308            throw new Exception(I18n::_('%s requires configuration section [%s] to be present in configuration file.', I18n::_($this->getKey('name')), $section), 3);
309        }
310        return $this->_configuration[$section];
311    }
312}