xref: /dokuwiki/lib/plugins/config/core/Configuration.php (revision c6639e6a6a4b11d65ecbc19f1bbbf2d9b32d0c19)
1<?php
2/**
3 * Configuration Class
4 *
5 * @author  Chris Smith <chris@jalakai.co.uk>
6 * @author  Ben Coburn <btcoburn@silicodon.net>
7 */
8
9namespace dokuwiki\plugin\config\core;
10
11/**
12 * Class configuration
13 */
14class Configuration {
15
16    const KEYMARKER = '____';
17
18    protected $_name = 'conf';     // name of the config variable found in the files (overridden by $config['varname'])
19    protected $_format = 'php';    // format of the config file, supported formats - php (overridden by $config['format'])
20    protected $_heading = '';      // heading string written at top of config file - don't include comment indicators
21    protected $_loaded = false;    // set to true after configuration files are loaded
22    protected $_metadata = array();// holds metadata describing the settings
23    /** @var Setting[] */
24    public $setting = array();  // array of setting objects
25    public $locked = false;     // configuration is considered locked if it can't be updated
26    public $show_disabled_plugins = false;
27
28    // configuration filenames
29    protected $_default_files = array();
30    protected $_local_files = array();      // updated configuration is written to the first file
31    protected $_protected_files = array();
32
33    protected $_plugin_list = null;
34
35    /**
36     * constructor
37     *
38     * @param string $datafile path to config metadata file
39     */
40    public function __construct($datafile) {
41        global $conf, $config_cascade;
42
43        if(!file_exists($datafile)) {
44            msg('No configuration metadata found at - ' . htmlspecialchars($datafile), -1);
45            return;
46        }
47        $meta = array();
48        /** @var array $config gets loaded via include here */
49        include($datafile);
50
51        if(isset($config['varname'])) $this->_name = $config['varname'];
52        if(isset($config['format'])) $this->_format = $config['format'];
53        if(isset($config['heading'])) $this->_heading = $config['heading'];
54
55        $this->_default_files = $config_cascade['main']['default'];
56        $this->_local_files = $config_cascade['main']['local'];
57        $this->_protected_files = $config_cascade['main']['protected'];
58
59        $this->locked = $this->_is_locked();
60        $this->_metadata = array_merge($meta, $this->get_plugintpl_metadata($conf['template']));
61        $this->retrieve_settings();
62    }
63
64    /**
65     * Retrieve and stores settings in setting[] attribute
66     */
67    public function retrieve_settings() {
68        global $conf;
69        $no_default_check = array('setting_fieldset', 'setting_undefined', 'setting_no_class');
70
71        if(!$this->_loaded) {
72            $default = array_merge(
73                $this->get_plugintpl_default($conf['template']),
74                $this->_read_config_group($this->_default_files)
75            );
76            $local = $this->_read_config_group($this->_local_files);
77            $protected = $this->_read_config_group($this->_protected_files);
78
79            $keys = array_merge(
80                array_keys($this->_metadata),
81                array_keys($default),
82                array_keys($local),
83                array_keys($protected)
84            );
85            $keys = array_unique($keys);
86
87            $param = null;
88            foreach($keys as $key) {
89                if(isset($this->_metadata[$key])) {
90                    $class = $this->_metadata[$key][0];
91
92                    if($class && class_exists('setting_' . $class)) {
93                        $class = 'setting_' . $class;
94                    } else {
95                        if($class != '') {
96                            $this->setting[] = new SettingNoClass($key, $param);
97                        }
98                        $class = 'setting';
99                    }
100
101                    $param = $this->_metadata[$key];
102                    array_shift($param);
103                } else {
104                    $class = 'setting_undefined';
105                    $param = null;
106                }
107
108                if(!in_array($class, $no_default_check) && !isset($default[$key])) {
109                    $this->setting[] = new SettingNoDefault($key, $param);
110                }
111
112                $this->setting[$key] = new $class($key, $param);
113
114                $d = array_key_exists($key, $default) ? $default[$key] : null;
115                $l = array_key_exists($key, $local) ? $local[$key] : null;
116                $p = array_key_exists($key, $protected) ? $protected[$key] : null;
117
118                $this->setting[$key]->initialize($d, $l, $p);
119            }
120
121            $this->_loaded = true;
122        }
123    }
124
125    /**
126     * Stores setting[] array to file
127     *
128     * @param string $id Name of plugin, which saves the settings
129     * @param string $header Text at the top of the rewritten settings file
130     * @param bool $backup backup current file? (remove any existing backup)
131     * @return bool succesful?
132     */
133    public function save_settings($id, $header = '', $backup = true) {
134        global $conf;
135
136        if($this->locked) return false;
137
138        // write back to the last file in the local config cascade
139        $file = end($this->_local_files);
140
141        // backup current file (remove any existing backup)
142        if(file_exists($file) && $backup) {
143            if(file_exists($file . '.bak')) @unlink($file . '.bak');
144            if(!io_rename($file, $file . '.bak')) return false;
145        }
146
147        if(!$fh = @fopen($file, 'wb')) {
148            io_rename($file . '.bak', $file);     // problem opening, restore the backup
149            return false;
150        }
151
152        if(empty($header)) $header = $this->_heading;
153
154        $out = $this->_out_header($id, $header);
155
156        foreach($this->setting as $setting) {
157            $out .= $setting->out($this->_name, $this->_format);
158        }
159
160        $out .= $this->_out_footer();
161
162        @fwrite($fh, $out);
163        fclose($fh);
164        if($conf['fperm']) chmod($file, $conf['fperm']);
165        return true;
166    }
167
168    /**
169     * Update last modified time stamp of the config file
170     *
171     * @return bool
172     */
173    public function touch_settings() {
174        if($this->locked) return false;
175        $file = end($this->_local_files);
176        return @touch($file);
177    }
178
179    /**
180     * Read and merge given config files
181     *
182     * @param array $files file paths
183     * @return array config settings
184     */
185    protected function _read_config_group($files) {
186        $config = array();
187        foreach($files as $file) {
188            $config = array_merge($config, $this->_read_config($file));
189        }
190
191        return $config;
192    }
193
194    /**
195     * Return an array of config settings
196     *
197     * @param string $file file path
198     * @return array config settings
199     */
200    protected function _read_config($file) {
201
202        if(!$file) return array();
203
204        $config = array();
205
206        if($this->_format == 'php') {
207
208            if(file_exists($file)) {
209                $contents = @php_strip_whitespace($file);
210            } else {
211                $contents = '';
212            }
213            $pattern = '/\$' . $this->_name . '\[[\'"]([^=]+)[\'"]\] ?= ?(.*?);(?=[^;]*(?:\$' . $this->_name . '|$))/s';
214            $matches = array();
215            preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER);
216
217            for($i = 0; $i < count($matches); $i++) {
218                $value = $matches[$i][2];
219
220                // correct issues with the incoming data
221                // FIXME ... for now merge multi-dimensional array indices using ____
222                $key = preg_replace('/.\]\[./', Configuration::KEYMARKER, $matches[$i][1]);
223
224                // handle arrays
225                if(preg_match('/^array ?\((.*)\)/', $value, $match)) {
226                    $arr = explode(',', $match[1]);
227
228                    // remove quotes from quoted strings & unescape escaped data
229                    $len = count($arr);
230                    for($j = 0; $j < $len; $j++) {
231                        $arr[$j] = trim($arr[$j]);
232                        $arr[$j] = $this->_readValue($arr[$j]);
233                    }
234
235                    $value = $arr;
236                } else {
237                    $value = $this->_readValue($value);
238                }
239
240                $config[$key] = $value;
241            }
242        }
243
244        return $config;
245    }
246
247    /**
248     * Convert php string into value
249     *
250     * @param string $value
251     * @return bool|string
252     */
253    protected function _readValue($value) {
254        $removequotes_pattern = '/^(\'|")(.*)(?<!\\\\)\1$/s';
255        $unescape_pairs = array(
256            '\\\\' => '\\',
257            '\\\'' => '\'',
258            '\\"' => '"'
259        );
260
261        if($value == 'true') {
262            $value = true;
263        } elseif($value == 'false') {
264            $value = false;
265        } else {
266            // remove quotes from quoted strings & unescape escaped data
267            $value = preg_replace($removequotes_pattern, '$2', $value);
268            $value = strtr($value, $unescape_pairs);
269        }
270        return $value;
271    }
272
273    /**
274     * Returns header of rewritten settings file
275     *
276     * @param string $id plugin name of which generated this output
277     * @param string $header additional text for at top of the file
278     * @return string text of header
279     */
280    protected function _out_header($id, $header) {
281        $out = '';
282        if($this->_format == 'php') {
283            $out .= '<' . '?php' . "\n" .
284                "/*\n" .
285                " * " . $header . "\n" .
286                " * Auto-generated by " . $id . " plugin\n" .
287                " * Run for user: " . $_SERVER['REMOTE_USER'] . "\n" .
288                " * Date: " . date('r') . "\n" .
289                " */\n\n";
290        }
291
292        return $out;
293    }
294
295    /**
296     * Returns footer of rewritten settings file
297     *
298     * @return string text of footer
299     */
300    protected function _out_footer() {
301        $out = '';
302        if($this->_format == 'php') {
303            $out .= "\n// end auto-generated content\n";
304        }
305
306        return $out;
307    }
308
309    /**
310     * Configuration is considered locked if there is no local settings filename
311     * or the directory its in is not writable or the file exists and is not writable
312     *
313     * @return bool true: locked, false: writable
314     */
315    protected function _is_locked() {
316        if(!$this->_local_files) return true;
317
318        $local = $this->_local_files[0];
319
320        if(!is_writable(dirname($local))) return true;
321        if(file_exists($local) && !is_writable($local)) return true;
322
323        return false;
324    }
325
326    /**
327     * not used ... conf's contents are an array!
328     * reduce any multidimensional settings to one dimension using Configuration::KEYMARKER
329     *
330     * @param $conf
331     * @param string $prefix
332     * @return array
333     */
334    protected function _flatten($conf, $prefix = '') {
335
336        $out = array();
337
338        foreach($conf as $key => $value) {
339            if(!is_array($value)) {
340                $out[$prefix . $key] = $value;
341                continue;
342            }
343
344            $tmp = $this->_flatten($value, $prefix . $key . Configuration::KEYMARKER);
345            $out = array_merge($out, $tmp);
346        }
347
348        return $out;
349    }
350
351    /**
352     * Returns array of plugin names
353     *
354     * @return array plugin names
355     * @triggers PLUGIN_CONFIG_PLUGINLIST event
356     */
357    protected function get_plugin_list() {
358        if(is_null($this->_plugin_list)) {
359            $list = plugin_list('', $this->show_disabled_plugins);
360
361            // remove this plugin from the list
362            $idx = array_search('config', $list);
363            unset($list[$idx]);
364
365            trigger_event('PLUGIN_CONFIG_PLUGINLIST', $list);
366            $this->_plugin_list = $list;
367        }
368
369        return $this->_plugin_list;
370    }
371
372    /**
373     * load metadata for plugin and template settings
374     *
375     * @param string $tpl name of active template
376     * @return array metadata of settings
377     */
378    protected function get_plugintpl_metadata($tpl) {
379        $file = '/conf/metadata.php';
380        $class = '/conf/settings.class.php';
381        $metadata = array();
382
383        foreach($this->get_plugin_list() as $plugin) {
384            $plugin_dir = plugin_directory($plugin);
385            if(file_exists(DOKU_PLUGIN . $plugin_dir . $file)) {
386                $meta = array();
387                @include(DOKU_PLUGIN . $plugin_dir . $file);
388                @include(DOKU_PLUGIN . $plugin_dir . $class);
389                if(!empty($meta)) {
390                    $metadata['plugin' . Configuration::KEYMARKER . $plugin . Configuration::KEYMARKER . 'plugin_settings_name'] = ['fieldset'];
391                }
392                foreach($meta as $key => $value) {
393                    if($value[0] == 'fieldset') {
394                        continue;
395                    } //plugins only get one fieldset
396                    $metadata['plugin' . Configuration::KEYMARKER . $plugin . Configuration::KEYMARKER . $key] = $value;
397                }
398            }
399        }
400
401        // the same for the active template
402        if(file_exists(tpl_incdir() . $file)) {
403            $meta = array();
404            @include(tpl_incdir() . $file);
405            @include(tpl_incdir() . $class);
406            if(!empty($meta)) {
407                $metadata['tpl' . Configuration::KEYMARKER . $tpl . Configuration::KEYMARKER . 'template_settings_name'] = array('fieldset');
408            }
409            foreach($meta as $key => $value) {
410                if($value[0] == 'fieldset') {
411                    continue;
412                } //template only gets one fieldset
413                $metadata['tpl' . Configuration::KEYMARKER . $tpl . Configuration::KEYMARKER . $key] = $value;
414            }
415        }
416
417        return $metadata;
418    }
419
420    /**
421     * Load default settings for plugins and templates
422     *
423     * @param string $tpl name of active template
424     * @return array default settings
425     */
426    protected function get_plugintpl_default($tpl) {
427        $file = '/conf/default.php';
428        $default = array();
429
430        foreach($this->get_plugin_list() as $plugin) {
431            $plugin_dir = plugin_directory($plugin);
432            if(file_exists(DOKU_PLUGIN . $plugin_dir . $file)) {
433                $conf = $this->_read_config(DOKU_PLUGIN . $plugin_dir . $file);
434                foreach($conf as $key => $value) {
435                    $default['plugin' . Configuration::KEYMARKER . $plugin . Configuration::KEYMARKER . $key] = $value;
436                }
437            }
438        }
439
440        // the same for the active template
441        if(file_exists(tpl_incdir() . $file)) {
442            $conf = $this->_read_config(tpl_incdir() . $file);
443            foreach($conf as $key => $value) {
444                $default['tpl' . Configuration::KEYMARKER . $tpl . Configuration::KEYMARKER . $key] = $value;
445            }
446        }
447
448        return $default;
449    }
450
451}
452
453