1<?php
2/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
3
4namespace Icinga\Application;
5
6use Iterator;
7use Countable;
8use LogicException;
9use UnexpectedValueException;
10use Icinga\Util\File;
11use Icinga\Data\ConfigObject;
12use Icinga\Data\Selectable;
13use Icinga\Data\SimpleQuery;
14use Icinga\File\Ini\IniWriter;
15use Icinga\File\Ini\IniParser;
16use Icinga\Exception\IcingaException;
17use Icinga\Exception\NotReadableError;
18use Icinga\Web\Navigation\Navigation;
19
20/**
21 * Container for INI like configuration and global registry of application and module related configuration.
22 */
23class Config implements Countable, Iterator, Selectable
24{
25    /**
26     * Configuration directory where ALL (application and module) configuration is located
27     *
28     * @var string
29     */
30    public static $configDir;
31
32    /**
33     * Application config instances per file
34     *
35     * @var array
36     */
37    protected static $app = array();
38
39    /**
40     * Module config instances per file
41     *
42     * @var array
43     */
44    protected static $modules = array();
45
46    /**
47     * Navigation config instances per type
48     *
49     * @var array
50     */
51    protected static $navigation = array();
52
53    /**
54     * The internal ConfigObject
55     *
56     * @var ConfigObject
57     */
58    protected $config;
59
60    /**
61     * The INI file this config has been loaded from or should be written to
62     *
63     * @var string
64     */
65    protected $configFile;
66
67    /**
68     * Create a new config
69     *
70     * @param   ConfigObject    $config     The config object to handle
71     */
72    public function __construct(ConfigObject $config = null)
73    {
74        $this->config = $config !== null ? $config : new ConfigObject();
75    }
76
77    /**
78     * Return this config's file path
79     *
80     * @return  string
81     */
82    public function getConfigFile()
83    {
84        return $this->configFile;
85    }
86
87    /**
88     * Set this config's file path
89     *
90     * @param   string      $filepath   The path to the ini file
91     *
92     * @return  $this
93     */
94    public function setConfigFile($filepath)
95    {
96        $this->configFile = $filepath;
97        return $this;
98    }
99
100    /**
101     * Return the internal ConfigObject
102     *
103     * @return  ConfigObject
104     */
105    public function getConfigObject()
106    {
107        return $this->config;
108    }
109
110    /**
111     * Provide a query for the internal config object
112     *
113     * @return  SimpleQuery
114     */
115    public function select()
116    {
117        return $this->config->select();
118    }
119
120    /**
121     * Return the count of available sections
122     *
123     * @return  int
124     */
125    public function count()
126    {
127        return $this->select()->count();
128    }
129
130    /**
131     * Reset the current position of the internal config object
132     *
133     * @return  ConfigObject
134     */
135    public function rewind()
136    {
137        return $this->config->rewind();
138    }
139
140    /**
141     * Return the section of the current iteration
142     *
143     * @return  ConfigObject
144     */
145    public function current()
146    {
147        return $this->config->current();
148    }
149
150    /**
151     * Return whether the position of the current iteration is valid
152     *
153     * @return  bool
154     */
155    public function valid()
156    {
157        return $this->config->valid();
158    }
159
160    /**
161     * Return the section's name of the current iteration
162     *
163     * @return  string
164     */
165    public function key()
166    {
167        return $this->config->key();
168    }
169
170    /**
171     * Advance the position of the current iteration and return the new section
172     *
173     * @return  ConfigObject
174     */
175    public function next()
176    {
177        return $this->config->next();
178    }
179
180    /**
181     * Return whether this config has any sections
182     *
183     * @return  bool
184     */
185    public function isEmpty()
186    {
187        return $this->config->isEmpty();
188    }
189
190    /**
191     * Return this config's section names
192     *
193     * @return  array
194     */
195    public function keys()
196    {
197        return $this->config->keys();
198    }
199
200    /**
201     * Return this config's data as associative array
202     *
203     * @return  array
204     */
205    public function toArray()
206    {
207        return $this->config->toArray();
208    }
209
210    /**
211     * Return the value from a section's property
212     *
213     * @param   string  $section    The section where the given property can be found
214     * @param   string  $key        The section's property to fetch the value from
215     * @param   mixed   $default    The value to return in case the section or the property is missing
216     *
217     * @return  mixed
218     *
219     * @throws  UnexpectedValueException    In case the given section does not hold any configuration
220     */
221    public function get($section, $key, $default = null)
222    {
223        $value = $this->config->$section;
224        if ($value instanceof ConfigObject) {
225            $value = $value->$key;
226        } elseif ($value !== null) {
227            throw new UnexpectedValueException(
228                sprintf('Value "%s" is not of type "%s" or a sub-type of it', $value, get_class($this->config))
229            );
230        }
231
232        if ($value === null && $default !== null) {
233            $value = $default;
234        }
235
236        return $value;
237    }
238
239    /**
240     * Return the given section
241     *
242     * @param   string  $name   The section's name
243     *
244     * @return  ConfigObject
245     */
246    public function getSection($name)
247    {
248        $section = $this->config->get($name);
249        return $section !== null ? $section : new ConfigObject();
250    }
251
252    /**
253     * Set or replace a section
254     *
255     * @param   string              $name
256     * @param   array|ConfigObject  $config
257     *
258     * @return  $this
259     */
260    public function setSection($name, $config = null)
261    {
262        if ($config === null) {
263            $config = new ConfigObject();
264        } elseif (! $config instanceof ConfigObject) {
265            $config = new ConfigObject($config);
266        }
267
268        $this->config->$name = $config;
269        return $this;
270    }
271
272    /**
273     * Remove a section
274     *
275     * @param   string  $name
276     *
277     * @return  $this
278     */
279    public function removeSection($name)
280    {
281        unset($this->config->$name);
282        return $this;
283    }
284
285    /**
286     * Return whether the given section exists
287     *
288     * @param   string  $name
289     *
290     * @return  bool
291     */
292    public function hasSection($name)
293    {
294        return isset($this->config->$name);
295    }
296
297    /**
298     * Initialize a new config using the given array
299     *
300     * The returned config has no file associated to it.
301     *
302     * @param   array   $array      The array to initialize the config with
303     *
304     * @return  Config
305     */
306    public static function fromArray(array $array)
307    {
308        return new static(new ConfigObject($array));
309    }
310
311    /**
312     * Load configuration from the given INI file
313     *
314     * @param   string      $file   The file to parse
315     *
316     * @throws  NotReadableError    When the file cannot be read
317     */
318    public static function fromIni($file)
319    {
320        $emptyConfig = new static();
321
322        $filepath = realpath($file);
323        if ($filepath === false) {
324            $emptyConfig->setConfigFile($file);
325        } elseif (is_readable($filepath)) {
326            return IniParser::parseIniFile($filepath);
327        } elseif (@file_exists($filepath)) {
328            throw new NotReadableError(t('Cannot read config file "%s". Permission denied'), $filepath);
329        }
330
331        return $emptyConfig;
332    }
333
334    /**
335     * Save configuration to the given INI file
336     *
337     * @param   string|null     $filePath   The path to the INI file or null in case this config's path should be used
338     * @param   int             $fileMode   The file mode to store the file with
339     *
340     * @throws  LogicException              In case this config has no path and none is passed in either
341     * @throws  NotWritableError            In case the INI file cannot be written
342     *
343     * @todo    create basepath and throw NotWritableError in case its not possible
344     */
345    public function saveIni($filePath = null, $fileMode = 0660)
346    {
347        if ($filePath === null && $this->configFile) {
348            $filePath = $this->configFile;
349        } elseif ($filePath === null) {
350            throw new LogicException('You need to pass $filePath or set a path using Config::setConfigFile()');
351        }
352
353        if (! file_exists($filePath)) {
354            File::create($filePath, $fileMode);
355        }
356
357        $this->getIniWriter($filePath, $fileMode)->write();
358    }
359
360    /**
361     * Return a IniWriter for this config
362     *
363     * @param   string|null     $filePath
364     * @param   int             $fileMode
365     *
366     * @return  IniWriter
367     */
368    protected function getIniWriter($filePath = null, $fileMode = null)
369    {
370        return new IniWriter($this, $filePath, $fileMode);
371    }
372
373    /**
374     * Prepend configuration base dir to the given relative path
375     *
376     * @param   string  $path   A relative path
377     *
378     * @return  string
379     */
380    public static function resolvePath($path)
381    {
382        return self::$configDir . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
383    }
384
385    /**
386     * Retrieve a application config
387     *
388     * @param   string  $configname     The configuration name (without ini suffix) to read and return
389     * @param   bool    $fromDisk       When set true, the configuration will be read from disk, even
390     *                                  if it already has been read
391     *
392     * @return  Config                  The requested configuration
393     */
394    public static function app($configname = 'config', $fromDisk = false)
395    {
396        if (! isset(self::$app[$configname]) || $fromDisk) {
397            self::$app[$configname] = static::fromIni(static::resolvePath($configname . '.ini'));
398        }
399
400        return self::$app[$configname];
401    }
402
403    /**
404     * Retrieve a module config
405     *
406     * @param   string  $modulename     The name of the module where to look for the requested configuration
407     * @param   string  $configname     The configuration name (without ini suffix) to read and return
408     * @param   string  $fromDisk       When set true, the configuration will be read from disk, even
409     *                                  if it already has been read
410     *
411     * @return  Config                  The requested configuration
412     */
413    public static function module($modulename, $configname = 'config', $fromDisk = false)
414    {
415        if (! isset(self::$modules[$modulename])) {
416            self::$modules[$modulename] = array();
417        }
418
419        if (! isset(self::$modules[$modulename][$configname]) || $fromDisk) {
420            self::$modules[$modulename][$configname] = static::fromIni(
421                static::resolvePath('modules/' . $modulename . '/' . $configname . '.ini')
422            );
423        }
424        return self::$modules[$modulename][$configname];
425    }
426
427    /**
428     * Retrieve a navigation config
429     *
430     * @param   string  $type       The type identifier of the navigation item for which to return its config
431     * @param   string  $username   A user's name or null if the shared config is desired
432     * @param   bool    $fromDisk   If true, the configuration will be read from disk
433     *
434     * @return  Config              The requested configuration
435     */
436    public static function navigation($type, $username = null, $fromDisk = false)
437    {
438        if (! isset(self::$navigation[$type])) {
439            self::$navigation[$type] = array();
440        }
441
442        $branch = $username ?: 'shared';
443        $typeConfigs = self::$navigation[$type];
444        if (! isset($typeConfigs[$branch]) || $fromDisk) {
445            $typeConfigs[$branch] = static::fromIni(static::getNavigationConfigPath($type, $username));
446        }
447
448        return $typeConfigs[$branch];
449    }
450
451    /**
452     * Return the path to the configuration file for the given navigation item type and user
453     *
454     * @param   string  $type
455     * @param   string  $username
456     *
457     * @return  string
458     *
459     * @throws  IcingaException     In case the given type is unknown
460     */
461    protected static function getNavigationConfigPath($type, $username = null)
462    {
463        $itemTypeConfig = Navigation::getItemTypeConfiguration();
464        if (! isset($itemTypeConfig[$type])) {
465            throw new IcingaException('Invalid navigation item type %s provided', $type);
466        }
467
468        if (isset($itemTypeConfig[$type]['config'])) {
469            $filename = $itemTypeConfig[$type]['config'] . '.ini';
470        } else {
471            $filename = $type . 's.ini';
472        }
473
474        if ($username) {
475            $path = static::resolvePath(implode(DIRECTORY_SEPARATOR, array('preferences', $username, $filename)));
476            if (realpath($path) === false) {
477                $path = static::resolvePath(implode(
478                    DIRECTORY_SEPARATOR,
479                    array('preferences', strtolower($username), $filename)
480                ));
481            }
482        } else {
483            $path = static::resolvePath('navigation' . DIRECTORY_SEPARATOR . $filename);
484        }
485        return $path;
486    }
487
488    /**
489     * Return this config rendered as a INI structured string
490     *
491     * @return  string
492     */
493    public function __toString()
494    {
495        return $this->getIniWriter()->render();
496    }
497}
498