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