1<?php
2
3namespace dokuwiki\plugin\config\core;
4
5use dokuwiki\plugin\config\core\Setting\Setting;
6use dokuwiki\plugin\config\core\Setting\SettingNoClass;
7use dokuwiki\plugin\config\core\Setting\SettingNoDefault;
8use dokuwiki\plugin\config\core\Setting\SettingNoKnownClass;
9use dokuwiki\plugin\config\core\Setting\SettingUndefined;
10
11/**
12 * Holds all the current settings and proxies the Loader and Writer
13 *
14 * @author Chris Smith <chris@jalakai.co.uk>
15 * @author Ben Coburn <btcoburn@silicodon.net>
16 * @author Andreas Gohr <andi@splitbrain.org>
17 */
18class Configuration
19{
20    public const KEYMARKER = '____';
21
22    /** @var Setting[] metadata as array of Settings objects */
23    protected $settings = [];
24    /** @var Setting[] undefined and problematic settings */
25    protected $undefined = [];
26
27    /** @var array all metadata */
28    protected $metadata;
29    /** @var array all default settings */
30    protected $default;
31    /** @var array all local settings */
32    protected $local;
33    /** @var array all protected settings */
34    protected $protected;
35
36    /** @var bool have the settings been changed since loading from disk? */
37    protected $changed = false;
38
39    /** @var Loader */
40    protected $loader;
41    /** @var Writer */
42    protected $writer;
43
44    /**
45     * ConfigSettings constructor.
46     */
47    public function __construct()
48    {
49        $this->loader = new Loader(new ConfigParser());
50        $this->writer = new Writer();
51
52        $this->metadata = $this->loader->loadMeta();
53        $this->default = $this->loader->loadDefaults();
54        $this->local = $this->loader->loadLocal();
55        $this->protected = $this->loader->loadProtected();
56
57        $this->initSettings();
58    }
59
60    /**
61     * Get all settings
62     *
63     * @return Setting[]
64     */
65    public function getSettings()
66    {
67        return $this->settings;
68    }
69
70    /**
71     * Get all unknown or problematic settings
72     *
73     * @return Setting[]
74     */
75    public function getUndefined()
76    {
77        return $this->undefined;
78    }
79
80    /**
81     * Have the settings been changed since loading from disk?
82     *
83     * @return bool
84     */
85    public function hasChanged()
86    {
87        return $this->changed;
88    }
89
90    /**
91     * Check if the config can be written
92     *
93     * @return bool
94     */
95    public function isLocked()
96    {
97        return $this->writer->isLocked();
98    }
99
100    /**
101     * Update the settings using the data provided
102     *
103     * @param array $input as posted
104     * @return bool true if all updates went through, false on errors
105     */
106    public function updateSettings($input)
107    {
108        $ok = true;
109
110        foreach ($this->settings as $key => $obj) {
111            $value = $input[$key] ?? null;
112            if ($obj->update($value)) {
113                $this->changed = true;
114            }
115            if ($obj->hasError()) $ok = false;
116        }
117
118        return $ok;
119    }
120
121    /**
122     * Save the settings
123     *
124     * This save the current state as defined in this object, including the
125     * undefined settings
126     *
127     * @throws \Exception
128     */
129    public function save()
130    {
131        // only save the undefined settings that have not been handled in settings
132        $undefined = array_diff_key($this->undefined, $this->settings);
133        $this->writer->save(array_merge($this->settings, $undefined));
134    }
135
136    /**
137     * Touch the settings
138     *
139     * @throws \Exception
140     */
141    public function touch()
142    {
143        $this->writer->touch();
144    }
145
146    /**
147     * Load the extension language strings
148     *
149     * @return array
150     */
151    public function getLangs()
152    {
153        return $this->loader->loadLangs();
154    }
155
156    /**
157     * Initalizes the $settings and $undefined properties
158     */
159    protected function initSettings()
160    {
161        $keys = [
162            ...array_keys($this->metadata),
163            ...array_keys($this->default),
164            ...array_keys($this->local),
165            ...array_keys($this->protected)
166        ];
167        $keys = array_unique($keys);
168
169        foreach ($keys as $key) {
170            $obj = $this->instantiateClass($key);
171
172            if ($obj->shouldHaveDefault() && !isset($this->default[$key])) {
173                $this->undefined[$key] = new SettingNoDefault($key);
174            }
175
176            $d = $this->default[$key] ?? null;
177            $l = $this->local[$key] ?? null;
178            $p = $this->protected[$key] ?? null;
179
180            $obj->initialize($d, $l, $p);
181        }
182    }
183
184    /**
185     * Instantiates the proper class for the given config key
186     *
187     * The class is added to the $settings or $undefined arrays and returned
188     *
189     * @param string $key
190     * @return Setting
191     */
192    protected function instantiateClass($key)
193    {
194        if (isset($this->metadata[$key])) {
195            $param = $this->metadata[$key];
196            $class = $this->determineClassName(array_shift($param), $key); // first param is class
197            $obj = new $class($key, $param);
198            $this->settings[$key] = $obj;
199        } else {
200            $obj = new SettingUndefined($key);
201            $this->undefined[$key] = $obj;
202        }
203        return $obj;
204    }
205
206    /**
207     * Return the class to load
208     *
209     * @param string $class the class name as given in the meta file
210     * @param string $key the settings key
211     * @return string
212     */
213    protected function determineClassName($class, $key)
214    {
215        // try namespaced class first
216        if (is_string($class)) {
217            $modern = str_replace('_', '', ucwords($class, '_'));
218            $modern = '\\dokuwiki\\plugin\\config\\core\\Setting\\Setting' . $modern;
219            if ($modern && class_exists($modern)) return $modern;
220            // try class as given
221            if (class_exists($class)) return $class;
222            // class wasn't found add to errors
223            $this->undefined[$key] = new SettingNoKnownClass($key);
224        } else {
225            // no class given, add to errors
226            $this->undefined[$key] = new SettingNoClass($key);
227        }
228        return '\\dokuwiki\\plugin\\config\\core\\Setting\\Setting';
229    }
230}
231