Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.65% covered (success)
90.65%
97 / 107
80.00% covered (success)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Configuration
90.65% covered (success)
90.65%
97 / 107
80.00% covered (success)
80.00%
4 / 5
46.65
0.00% covered (danger)
0.00%
0 / 1
 __construct
89.80% covered (success)
89.80%
88 / 98
0.00% covered (danger)
0.00%
0 / 1
40.62
 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            'template'                 => 'bootstrap',
49            'info'                     => 'More information on the <a href=\'https://privatebin.info/\'>project page</a>.',
50            'notice'                   => '',
51            'languageselection'        => false,
52            'languagedefault'          => '',
53            'urlshortener'             => '',
54            'qrcode'                   => true,
55            'email'                    => true,
56            'icon'                     => 'identicon',
57            '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\'; img-src \'self\' data: blob:; media-src blob:; object-src blob:; sandbox allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-downloads',
58            'zerobincompatibility'     => false,
59            'httpwarning'              => true,
60            'compression'              => 'zlib',
61        ),
62        'expire' => array(
63            'default' => '1week',
64        ),
65        'expire_options' => array(
66            '5min'   => 300,
67            '10min'  => 600,
68            '1hour'  => 3600,
69            '1day'   => 86400,
70            '1week'  => 604800,
71            '1month' => 2592000,
72            '1year'  => 31536000,
73            'never'  => 0,
74        ),
75        'formatter_options' => array(
76            'plaintext'          => 'Plain Text',
77            'syntaxhighlighting' => 'Source Code',
78            'markdown'           => 'Markdown',
79        ),
80        'traffic' => array(
81            'limit'     => 10,
82            'header'    => '',
83            'exempted'  => '',
84            'creators'  => '',
85        ),
86        'purge' => array(
87            'limit'     => 300,
88            'batchsize' => 10,
89        ),
90        'model' => array(
91            'class' => 'Filesystem',
92        ),
93        'model_options' => array(
94            'dir' => 'data',
95        ),
96        'yourls' => array(
97            'signature' => '',
98            'apiurl'    => '',
99        ),
100        // update this array when adding/changing/removing js files
101        'sri' => array(
102            'js/base-x-4.0.0.js'     => 'sha512-nNPg5IGCwwrveZ8cA/yMGr5HiRS5Ps2H+s0J/mKTPjCPWUgFGGw7M5nqdnPD3VsRwCVysUh3Y8OWjeSKGkEQJQ==',
103            'js/base64-1.7.js'       => 'sha512-JdwsSP3GyHR+jaCkns9CL9NTt4JUJqm/BsODGmYhBcj5EAPKcHYh+OiMfyHbcDLECe17TL0hjXADFkusAqiYgA==',
104            'js/bootstrap-3.4.1.js'  => 'sha512-oBTprMeNEKCnqfuqKd6sbvFzmFQtlXS3e0C/RGFV0hD6QzhHV+ODfaQbAlmY6/q0ubbwlAM/nCJjkrgA3waLzg==',
105            'js/bootstrap-5.3.3.js'  => 'sha512-in2rcOpLTdJ7/pw5qjF4LWHFRtgoBDxXCy49H4YGOcVdGiPaQucGIbOqxt1JvmpvOpq3J/C7VTa0FlioakB2gQ==',
106            'js/dark-mode-switch.js' => 'sha512-BhY7dNU14aDN5L+muoUmA66x0CkYUWkQT0nxhKBLP/o2d7jE025+dvWJa4OiYffBGEFgmhrD/Sp+QMkxGMTz2g==',
107            'js/jquery-3.7.1.js'     => 'sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==',
108            'js/kjua-0.9.0.js'       => 'sha512-CVn7af+vTMBd9RjoS4QM5fpLFEOtBCoB0zPtaqIDC7sF4F8qgUSRFQQpIyEDGsr6yrjbuOLzdf20tkHHmpaqwQ==',
109            'js/legacy.js'           => 'sha512-UxW/TOZKon83n6dk/09GsYKIyeO5LeBHokxyIq+r7KFS5KMBeIB/EM7NrkVYIezwZBaovnyNtY2d9tKFicRlXg==',
110            'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
111            'js/privatebin.js'       => 'sha512-POa+8KNXFFwJFsqp7r9APmR5Rc1w2l363y+OScSzLCySrHN7UhOOgt1VH/o8mVddFvvUozj3FZVmdkTxRlrS5g==',
112            'js/purify-3.2.4.js'     => 'sha512-Mu9BqoHURMeycg6AgqTpokUv9guq88pajfaFqz53fx1OxohyROkydXPLEIbdKCQ7EdDs9hgcrYeZ9zTiPQQ4CA==',
113            'js/rawinflate-0.3.js'   => 'sha512-g8uelGgJW9A/Z1tB6Izxab++oj5kdD7B4qC7DHwZkB6DGMXKyzx7v5mvap2HXueI2IIn08YlRYM56jwWdm2ucQ==',
114            'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
115            'js/zlib-1.3.1.js'       => 'sha512-5bU9IIP4PgBrOKLZvGWJD4kgfQrkTz8Z3Iqeu058mbQzW3mCumOU6M3UVbVZU9rrVoVwaW4cZK8U8h5xjF88eQ==',
116        ),
117    );
118
119    /**
120     * parse configuration file and ensure default configuration values are present
121     *
122     * @throws Exception
123     */
124    public function __construct()
125    {
126        $basePaths  = array();
127        $config     = array();
128        $configPath = getenv('CONFIG_PATH');
129        if ($configPath !== false && !empty($configPath)) {
130            $basePaths[] = $configPath;
131        }
132        $basePaths[] = PATH . 'cfg';
133        foreach ($basePaths as $basePath) {
134            $configFile = $basePath . DIRECTORY_SEPARATOR . 'conf.php';
135            if (is_readable($configFile)) {
136                $config = parse_ini_file($configFile, true);
137                foreach (array('main', 'model', 'model_options') as $section) {
138                    if (!array_key_exists($section, $config)) {
139                        throw new Exception(I18n::_('PrivateBin requires configuration section [%s] to be present in configuration file.', $section), 2);
140                    }
141                }
142                break;
143            }
144        }
145
146        $opts = '_options';
147        foreach (self::getDefaults() as $section => $values) {
148            // fill missing sections with default values
149            if (!array_key_exists($section, $config) || count($config[$section]) == 0) {
150                $this->_configuration[$section] = $values;
151                if (array_key_exists('dir', $this->_configuration[$section])) {
152                    $this->_configuration[$section]['dir'] = PATH . $this->_configuration[$section]['dir'];
153                }
154                continue;
155            }
156            // provide different defaults for database model
157            elseif (
158                $section == 'model_options' && in_array(
159                    $this->_configuration['model']['class'],
160                    array('Database', 'privatebin_db', 'zerobin_db')
161                )
162            ) {
163                $values = array(
164                    'dsn' => 'sqlite:' . PATH . 'data' . DIRECTORY_SEPARATOR . 'db.sq3',
165                    'tbl' => null,
166                    'usr' => null,
167                    'pwd' => null,
168                    'opt' => array(),
169                );
170            } elseif (
171                $section == 'model_options' && in_array(
172                    $this->_configuration['model']['class'],
173                    array('GoogleCloudStorage')
174                )
175            ) {
176                $values = array(
177                    'bucket'     => getenv('PRIVATEBIN_GCS_BUCKET') ? getenv('PRIVATEBIN_GCS_BUCKET') : null,
178                    'prefix'     => 'pastes',
179                    'uniformacl' => false,
180                );
181            } elseif (
182                $section == 'model_options' && in_array(
183                    $this->_configuration['model']['class'],
184                    array('S3Storage')
185                )
186            ) {
187                $values = array(
188                    'region'                  => null,
189                    'version'                 => null,
190                    'endpoint'                => null,
191                    'accesskey'               => null,
192                    'secretkey'               => null,
193                    'use_path_style_endpoint' => null,
194                    'bucket'                  => null,
195                    'prefix'                  => '',
196                );
197            }
198
199            // "*_options" sections don't require all defaults to be set
200            if (
201                $section !== 'model_options' &&
202                ($from = strlen($section) - strlen($opts)) >= 0 &&
203                strpos($section, $opts, $from) !== false
204            ) {
205                if (is_int(current($values))) {
206                    $config[$section] = array_map('intval', $config[$section]);
207                }
208                $this->_configuration[$section] = $config[$section];
209            }
210            // check for missing keys and set defaults if necessary
211            else {
212                // preserve configured SRI hashes
213                if ($section == 'sri' && array_key_exists($section, $config)) {
214                    $this->_configuration[$section] = $config[$section];
215                }
216                foreach ($values as $key => $val) {
217                    if ($key == 'dir') {
218                        $val = PATH . $val;
219                    }
220                    $result = $val;
221                    if (array_key_exists($key, $config[$section])) {
222                        if ($val === null) {
223                            $result = $config[$section][$key];
224                        } elseif (is_bool($val)) {
225                            $val = strtolower($config[$section][$key]);
226                            if (in_array($val, array('true', 'yes', 'on'))) {
227                                $result = true;
228                            } elseif (in_array($val, array('false', 'no', 'off'))) {
229                                $result = false;
230                            } else {
231                                $result = (bool) $config[$section][$key];
232                            }
233                        } elseif (is_int($val)) {
234                            $result = (int) $config[$section][$key];
235                        } elseif (is_string($val) && !empty($config[$section][$key])) {
236                            $result = (string) $config[$section][$key];
237                        } elseif (is_array($val) && is_array($config[$section][$key])) {
238                            $result = $config[$section][$key];
239                        }
240                    }
241                    $this->_configuration[$section][$key] = $result;
242                }
243            }
244        }
245
246        // support for old config file format, before the fork was renamed and PSR-4 introduced
247        $this->_configuration['model']['class'] = str_replace(
248            'zerobin_', 'privatebin_',
249            $this->_configuration['model']['class']
250        );
251
252        $this->_configuration['model']['class'] = str_replace(
253            array('privatebin_data', 'privatebin_db'),
254            array('Filesystem', 'Database'),
255            $this->_configuration['model']['class']
256        );
257
258        // ensure a valid expire default key is set
259        if (!array_key_exists($this->_configuration['expire']['default'], $this->_configuration['expire_options'])) {
260            $this->_configuration['expire']['default'] = key($this->_configuration['expire_options']);
261        }
262
263        // ensure the basepath ends in a slash, if one is set
264        if (
265            !empty($this->_configuration['main']['basepath']) &&
266            substr_compare($this->_configuration['main']['basepath'], '/', -1) !== 0
267        ) {
268            $this->_configuration['main']['basepath'] .= '/';
269        }
270    }
271
272    /**
273     * get configuration as array
274     *
275     * @return array
276     */
277    public function get()
278    {
279        return $this->_configuration;
280    }
281
282    /**
283     * get default configuration as array
284     *
285     * @return array
286     */
287    public static function getDefaults()
288    {
289        return self::$_defaults;
290    }
291
292    /**
293     * get a key from the configuration, typically the main section or all keys
294     *
295     * @param string $key
296     * @param string $section defaults to main
297     * @throws Exception
298     * @return mixed
299     */
300    public function getKey($key, $section = 'main')
301    {
302        $options = $this->getSection($section);
303        if (!array_key_exists($key, $options)) {
304            throw new Exception(I18n::_('Invalid data.') . " $section / $key", 4);
305        }
306        return $this->_configuration[$section][$key];
307    }
308
309    /**
310     * get a section from the configuration, must exist
311     *
312     * @param string $section
313     * @throws Exception
314     * @return mixed
315     */
316    public function getSection($section)
317    {
318        if (!array_key_exists($section, $this->_configuration)) {
319            throw new Exception(I18n::_('%s requires configuration section [%s] to be present in configuration file.', I18n::_($this->getKey('name')), $section), 3);
320        }
321        return $this->_configuration[$section];
322    }
323}