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