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