diff options
Diffstat (limited to 'modules/setup/library')
28 files changed, 4519 insertions, 0 deletions
diff --git a/modules/setup/library/Setup/Exception/SetupException.php b/modules/setup/library/Setup/Exception/SetupException.php new file mode 100644 index 0000000..c3ae591 --- /dev/null +++ b/modules/setup/library/Setup/Exception/SetupException.php @@ -0,0 +1,22 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Exception; + +use Icinga\Exception\IcingaException; + +/** + * Class SetupException + * + * Used to indicate that a setup should be aborted. + */ +class SetupException extends IcingaException +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + parent::__construct('Setup abortion'); + } +} diff --git a/modules/setup/library/Setup/Requirement.php b/modules/setup/library/Setup/Requirement.php new file mode 100644 index 0000000..fd16405 --- /dev/null +++ b/modules/setup/library/Setup/Requirement.php @@ -0,0 +1,343 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use LogicException; + +abstract class Requirement +{ + /** + * The state of this requirement + * + * @var bool + */ + protected $state; + + /** + * A descriptive text representing the current state of this requirement + * + * @var string + */ + protected $stateText; + + /** + * The descriptions of this requirement + * + * @var array + */ + protected $descriptions; + + /** + * The title of this requirement + * + * @var string + */ + protected $title; + + /** + * The condition of this requirement + * + * @var mixed + */ + protected $condition; + + /** + * Whether this requirement is optional + * + * @var bool + */ + protected $optional; + + /** + * The alias to display the condition with in a human readable way + * + * @var string + */ + protected $alias; + + /** + * The text to display if the given requirement is fulfilled + * + * @var string + */ + protected $textAvailable; + + /** + * The text to display if the given requirement is not fulfilled + * + * @var string + */ + protected $textMissing; + + /** + * Create a new requirement + * + * @param array $options + * + * @throws LogicException In case there exists no setter for an option's key + */ + public function __construct(array $options = array()) + { + $this->optional = false; + $this->descriptions = array(); + + foreach ($options as $key => $value) { + $setMethod = 'set' . ucfirst($key); + $addMethod = 'add' . ucfirst($key); + if (method_exists($this, $setMethod)) { + $this->$setMethod($value); + } elseif (method_exists($this, $addMethod)) { + $this->$addMethod($value); + } else { + throw LogicException('No setter found for option key: ' . $key); + } + } + } + + /** + * Set the state of this requirement + * + * @param bool $state + * + * @return Requirement + */ + public function setState($state) + { + $this->state = (bool) $state; + return $this; + } + + /** + * Return the state of this requirement + * + * Evaluates the requirement in case there is no state set yet. + * + * @return int + */ + public function getState() + { + if ($this->state === null) { + $this->state = $this->evaluate(); + } + + return $this->state; + } + + /** + * Set a descriptive text for this requirement's current state + * + * @param string $text + * + * @return Requirement + */ + public function setStateText($text) + { + $this->stateText = $text; + return $this; + } + + /** + * Return a descriptive text for this requirement's current state + * + * @return string + */ + public function getStateText() + { + $state = $this->getState(); + if ($this->stateText === null) { + return $state ? $this->getTextAvailable() : $this->getTextMissing(); + } + return $this->stateText; + } + + /** + * Add a description for this requirement + * + * @param string $description + * + * @return Requirement + */ + public function addDescription($description) + { + $this->descriptions[] = $description; + return $this; + } + + /** + * Return the descriptions of this wizard + * + * @return array + */ + public function getDescriptions() + { + return $this->descriptions; + } + + /** + * Set the title for this requirement + * + * @param string $title + * + * @return Requirement + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * Return the title of this requirement + * + * In case there is no title set the alias is returned instead. + * + * @return string + */ + public function getTitle() + { + if ($this->title === null) { + return $this->getAlias(); + } + + return $this->title; + } + + /** + * Set the condition for this requirement + * + * @param mixed $condition + * + * @return Requirement + */ + public function setCondition($condition) + { + $this->condition = $condition; + return $this; + } + + /** + * Return the condition of this requirement + * + * @return mixed + */ + public function getCondition() + { + return $this->condition; + } + + /** + * Set whether this requirement is optional + * + * @param bool $state + * + * @return Requirement + */ + public function setOptional($state = true) + { + $this->optional = (bool) $state; + return $this; + } + + /** + * Return whether this requirement is optional + * + * @return bool + */ + public function isOptional() + { + return $this->optional; + } + + /** + * Set the alias to display the condition with in a human readable way + * + * @param string $alias + * + * @return Requirement + */ + public function setAlias($alias) + { + $this->alias = $alias; + return $this; + } + + /** + * Return the alias to display the condition with in a human readable way + * + * @return string + */ + public function getAlias() + { + return $this->alias; + } + + /** + * Set the text to display if the given requirement is fulfilled + * + * @param string $textAvailable + * + * @return Requirement + */ + public function setTextAvailable($textAvailable) + { + $this->textAvailable = $textAvailable; + return $this; + } + + /** + * Get the text to display if the given requirement is fulfilled + * + * @return string + */ + public function getTextAvailable() + { + return $this->textAvailable; + } + + /** + * Set the text to display if the given requirement is not fulfilled + * + * @param string $textMissing + * + * @return Requirement + */ + public function setTextMissing($textMissing) + { + $this->textMissing = $textMissing; + return $this; + } + + /** + * Get the text to display if the given requirement is not fulfilled + * + * @return string + */ + public function getTextMissing() + { + return $this->textMissing; + } + + /** + * Evaluate this requirement and return whether it is fulfilled + * + * @return bool + */ + abstract protected function evaluate(); + + /** + * Return whether the given requirement equals this one + * + * @param Requirement $requirement + * + * @return bool + */ + public function equals(Requirement $requirement) + { + if ($requirement instanceof static) { + return $this->getCondition() === $requirement->getCondition(); + } + + return false; + } +} diff --git a/modules/setup/library/Setup/Requirement/ClassRequirement.php b/modules/setup/library/Setup/Requirement/ClassRequirement.php new file mode 100644 index 0000000..d884c31 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/ClassRequirement.php @@ -0,0 +1,48 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class ClassRequirement extends Requirement +{ + protected function evaluate() + { + return Platform::classExists($this->getCondition()); + } + + /** + * {@inheritdoc} + */ + public function getStateText() + { + $stateText = parent::getStateText(); + if ($stateText === null) { + $alias = $this->getAlias(); + if ($this->getState()) { + $stateText = $alias === null + ? sprintf( + mt('setup', 'The %s class is available.', 'setup.requirement.class'), + $this->getCondition() + ) + : sprintf( + mt('setup', 'The %s is available.', 'setup.requirement.class'), + $alias + ); + } else { + $stateText = $alias === null + ? sprintf( + mt('setup', 'The %s class is missing.', 'setup.requirement.class'), + $this->getCondition() + ) + : sprintf( + mt('setup', 'The %s is missing.', 'setup.requirement.class'), + $alias + ); + } + } + return $stateText; + } +} diff --git a/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php b/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php new file mode 100644 index 0000000..7e9044c --- /dev/null +++ b/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php @@ -0,0 +1,42 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Module\Setup\Requirement; + +class ConfigDirectoryRequirement extends Requirement +{ + public function getTitle() + { + $title = parent::getTitle(); + if ($title === null) { + return mt('setup', 'Read- and writable configuration directory'); + } + + return $title; + } + + protected function evaluate() + { + $path = $this->getCondition(); + if (file_exists($path)) { + $readable = is_readable($path); + if ($readable && is_writable($path)) { + $this->setStateText(sprintf(mt('setup', 'The directory %s is read- and writable.'), $path)); + return true; + } else { + $this->setStateText(sprintf( + $readable + ? mt('setup', 'The directory %s is not writable.') + : mt('setup', 'The directory %s is not readable.'), + $path + )); + return false; + } + } else { + $this->setStateText(sprintf(mt('setup', 'The directory %s does not exist.'), $path)); + return false; + } + } +} diff --git a/modules/setup/library/Setup/Requirement/OSRequirement.php b/modules/setup/library/Setup/Requirement/OSRequirement.php new file mode 100644 index 0000000..760c97a --- /dev/null +++ b/modules/setup/library/Setup/Requirement/OSRequirement.php @@ -0,0 +1,27 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class OSRequirement extends Requirement +{ + public function getTitle() + { + $title = parent::getTitle(); + if ($title === null) { + return sprintf(mt('setup', '%s Platform'), ucfirst($this->getCondition())); + } + + return $title; + } + + protected function evaluate() + { + $phpOS = Platform::getOperatingSystemName(); + $this->setStateText(sprintf(mt('setup', 'You are running PHP on a %s system.'), ucfirst($phpOS))); + return strtolower($phpOS) === strtolower($this->getCondition()); + } +} diff --git a/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php b/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php new file mode 100644 index 0000000..6c77af5 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php @@ -0,0 +1,22 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class PhpConfigRequirement extends Requirement +{ + protected function evaluate() + { + list($configDirective, $value) = $this->getCondition(); + $configValue = Platform::getPhpConfig($configDirective); + $this->setStateText( + $configValue + ? sprintf(mt('setup', 'The PHP config `%s\' is set to "%s".'), $configDirective, $configValue) + : sprintf(mt('setup', 'The PHP config `%s\' is not defined.'), $configDirective) + ); + return is_bool($value) ? $configValue == $value : $configValue === $value; + } +} diff --git a/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php b/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php new file mode 100644 index 0000000..f8ab129 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php @@ -0,0 +1,42 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class PhpModuleRequirement extends Requirement +{ + public function getTitle() + { + $title = parent::getTitle(); + if ($title === $this->getAlias()) { + if ($title === null) { + $title = $this->getCondition(); + } + + return sprintf(mt('setup', 'PHP Module: %s'), $title); + } + + return $title; + } + + protected function evaluate() + { + $moduleName = $this->getCondition(); + if (Platform::extensionLoaded($moduleName)) { + $this->setStateText(sprintf( + mt('setup', 'The PHP module %s is available.'), + $this->getAlias() ?: $moduleName + )); + return true; + } else { + $this->setStateText(sprintf( + mt('setup', 'The PHP module %s is missing.'), + $this->getAlias() ?: $moduleName + )); + return false; + } + } +} diff --git a/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php b/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php new file mode 100644 index 0000000..b811ca8 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php @@ -0,0 +1,28 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class PhpVersionRequirement extends Requirement +{ + public function getTitle() + { + $title = parent::getTitle(); + if ($title === null) { + return mt('setup', 'PHP Version'); + } + + return $title; + } + + protected function evaluate() + { + $phpVersion = Platform::getPhpVersion(); + $this->setStateText(sprintf(mt('setup', 'You are running PHP version %s.'), $phpVersion)); + list($operator, $requiredVersion) = $this->getCondition(); + return version_compare($phpVersion, $requiredVersion, $operator); + } +} diff --git a/modules/setup/library/Setup/Requirement/SetRequirement.php b/modules/setup/library/Setup/Requirement/SetRequirement.php new file mode 100644 index 0000000..77cbaf0 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/SetRequirement.php @@ -0,0 +1,34 @@ +<?php +/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Module\Setup\Requirement; + +/** + * Add requirement field + * + * @package Icinga\Module\Setup\Requirement + */ +class SetRequirement extends Requirement +{ + protected function evaluate() + { + $condition = $this->getCondition(); + + if ($condition->getState()) { + $this->setStateText(sprintf( + mt('setup', '%s is available.'), + $this->getAlias() ?: $this->getTitle() + )); + return true; + } + + $this->setStateText(sprintf( + mt('setup', '%s is missing.'), + $this->getAlias() ?: $this->getTitle() + )); + + return false; + } +} diff --git a/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php b/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php new file mode 100644 index 0000000..bab587a --- /dev/null +++ b/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php @@ -0,0 +1,24 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Icinga; +use Icinga\Module\Setup\Requirement; + +class WebLibraryRequirement extends Requirement +{ + protected function evaluate() + { + list($name, $op, $version) = $this->getCondition(); + + $libs = Icinga::app()->getLibraries(); + if (! $libs->has($name)) { + $this->setStateText(sprintf(mt('setup', '%s is not installed'), $this->getAlias())); + return false; + } + + $this->setStateText(sprintf(mt('setup', '%s version: %s'), $this->getAlias(), $libs->get($name)->getVersion())); + return $libs->has($name, $op . $version); + } +} diff --git a/modules/setup/library/Setup/Requirement/WebModuleRequirement.php b/modules/setup/library/Setup/Requirement/WebModuleRequirement.php new file mode 100644 index 0000000..ad600e1 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/WebModuleRequirement.php @@ -0,0 +1,31 @@ +<?php +/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Icinga; +use Icinga\Module\Setup\Requirement; + +class WebModuleRequirement extends Requirement +{ + protected function evaluate() + { + list($name, $op, $version) = $this->getCondition(); + + $mm = Icinga::app()->getModuleManager(); + if (! $mm->hasInstalled($name)) { + $this->setStateText(sprintf(mt('setup', '%s is not installed'), $this->getAlias())); + return false; + } + + $module = $mm->getModule($name, false); + + $moduleVersion = $module->getVersion(); + if ($moduleVersion[0] === 'v') { + $moduleVersion = substr($moduleVersion, 1); + } + + $this->setStateText(sprintf(mt('setup', '%s version: %s'), $this->getAlias(), $moduleVersion)); + return version_compare($moduleVersion, $version, $op); + } +} diff --git a/modules/setup/library/Setup/RequirementSet.php b/modules/setup/library/Setup/RequirementSet.php new file mode 100644 index 0000000..672fad4 --- /dev/null +++ b/modules/setup/library/Setup/RequirementSet.php @@ -0,0 +1,335 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use LogicException; +use RecursiveIterator; +use Traversable; + +/** + * Container to store and handle requirements + */ +class RequirementSet implements RecursiveIterator +{ + /** + * Mode AND (all requirements must be met) + */ + const MODE_AND = 0; + + /** + * Mode OR (at least one requirement must be met) + */ + const MODE_OR = 1; + + /** + * Whether all requirements meet their condition + * + * @var bool + */ + protected $state; + + /** + * Whether this set is optional + * + * @var bool + */ + protected $optional; + + /** + * The mode by which the requirements are evaluated + * + * @var string + */ + protected $mode; + + /** + * The registered requirements + * + * @var array + */ + protected $requirements; + + /** + * The raw state of this set's requirements + * + * @var bool + */ + private $forcedState; + + /** + * Initialize a new set of requirements + * + * @param bool $optional Whether this set is optional + * @param int $mode The mode by which to evaluate this set + */ + public function __construct($optional = false, $mode = null) + { + $this->optional = $optional; + $this->requirements = array(); + $this->setMode($mode ?: static::MODE_AND); + } + + /** + * Set the state of this set + * + * @param bool $state + * + * @return RequirementSet + */ + public function setState($state) + { + $this->state = (bool) $state; + return $this; + } + + /** + * Return the state of this set + * + * Alias for RequirementSet::fulfilled(true). + * + * @return bool + */ + public function getState() + { + return $this->fulfilled(true); + } + + /** + * Set whether this set of requirements should be optional + * + * @param bool $state + * + * @return RequirementSet + */ + public function setOptional($state = true) + { + $this->optional = (bool) $state; + return $this; + } + + /** + * Return whether this set of requirements is optional + * + * @return bool + */ + public function isOptional() + { + return $this->optional; + } + + /** + * Set the mode by which to evaluate the requirements + * + * @param int $mode + * + * @return RequirementSet + * + * @throws LogicException In case the given mode is invalid + */ + public function setMode($mode) + { + if ($mode !== static::MODE_AND && $mode !== static::MODE_OR) { + throw new LogicException(sprintf('Invalid mode %u given.'), $mode); + } + + $this->mode = $mode; + return $this; + } + + /** + * Return the mode by which the requirements are evaluated + * + * @return int + */ + public function getMode() + { + return $this->mode; + } + + /** + * Register a requirement + * + * @param Requirement $requirement The requirement to add + * + * @return RequirementSet + */ + public function add(Requirement $requirement) + { + $merged = false; + foreach ($this->requirements as $knownRequirement) { + if ($knownRequirement instanceof Requirement && $requirement->equals($knownRequirement)) { + $knownRequirement->setOptional($requirement->isOptional()); + foreach ($requirement->getDescriptions() as $description) { + $knownRequirement->addDescription($description); + } + + $merged = true; + break; + } + } + + if (! $merged) { + $this->requirements[] = $requirement; + } + + return $this; + } + + /** + * Return all registered requirements + * + * @return array + */ + public function getAll() + { + return $this->requirements; + } + + /** + * Register the given set of requirements + * + * @param RequirementSet $set The set to register + * + * @return RequirementSet + */ + public function merge(RequirementSet $set) + { + if ($this->getMode() === $set->getMode() && $this->isOptional() === $set->isOptional()) { + foreach ($set->getAll() as $requirement) { + if ($requirement instanceof static) { + $this->merge($requirement); + } else { + $this->add($requirement); + } + } + } else { + $this->requirements[] = $set; + } + + return $this; + } + + /** + * Return whether all requirements can successfully be evaluated based on the current mode + * + * In case this is a optional set of requirements (and $force is false), true is returned immediately. + * + * @param bool $force Whether to ignore the optionality of a set or single requirement + * + * @return bool + */ + public function fulfilled($force = false) + { + $state = $this->isOptional(); + if (! $force && $state) { + return true; + } + + if (! $force && $this->state !== null) { + return $this->state; + } elseif ($force && $this->forcedState !== null) { + return $this->forcedState; + } + + $self = $this->requirements; + foreach ($self as $requirement) { + if ($requirement->getState()) { + $state = true; + if ($this->getMode() === static::MODE_OR) { + break; + } + } elseif ($force || !$requirement->isOptional()) { + $state = false; + if ($this->getMode() === static::MODE_AND) { + break; + } + } + } + + if ($force) { + return $this->forcedState = $state; + } + + return $this->state = $state; + } + + /** + * Return whether the current element represents a nested set of requirements + * + * @return bool + */ + public function hasChildren(): bool + { + $current = $this->current(); + return $current instanceof static; + } + + /** + * Return a iterator for the current nested set of requirements + * + * @return ?RecursiveIterator + */ + public function getChildren(): ?RecursiveIterator + { + return $this->current(); + } + + /** + * Rewind the iterator to its first element + */ + public function rewind(): void + { + reset($this->requirements); + } + + /** + * Return whether the current iterator position is valid + * + * @return bool + */ + public function valid(): bool + { + return key($this->requirements) !== null; + } + + /** + * Return the current element in the iteration + * + * @return Requirement|RequirementSet + */ + #[\ReturnTypeWillChange] + public function current() + { + return current($this->requirements); + } + + /** + * Return the position of the current element in the iteration + * + * @return int + */ + public function key(): int + { + return key($this->requirements); + } + + /** + * Advance the iterator to the next element + */ + public function next(): void + { + next($this->requirements); + } + + /** + * Return this set of requirements rendered as HTML + * + * @return string + */ + public function __toString() + { + $renderer = new RequirementsRenderer($this); + return (string) $renderer; + } +} diff --git a/modules/setup/library/Setup/RequirementsRenderer.php b/modules/setup/library/Setup/RequirementsRenderer.php new file mode 100644 index 0000000..cc9392a --- /dev/null +++ b/modules/setup/library/Setup/RequirementsRenderer.php @@ -0,0 +1,64 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use RecursiveIteratorIterator; + +class RequirementsRenderer extends RecursiveIteratorIterator +{ + public function beginIteration(): void + { + $this->tags[] = '<ul class="requirements">'; + } + + public function endIteration(): void + { + $this->tags[] = '</ul>'; + } + + public function beginChildren(): void + { + $this->tags[] = '<li>'; + $currentSet = $this->getSubIterator(); + $state = $currentSet->getState() ? 'fulfilled' : ($currentSet->isOptional() ? 'not-available' : 'missing'); + $this->tags[] = '<ul class="set-state ' . $state . '">'; + } + + public function endChildren(): void + { + $this->tags[] = '</ul>'; + $this->tags[] = '</li>'; + } + + public function render() + { + foreach ($this as $requirement) { + $this->tags[] = '<li class="clearfix">'; + $this->tags[] = '<div class="title"><h2>' . $requirement->getTitle() . '</h2></div>'; + $this->tags[] = '<div class="description">'; + $descriptions = $requirement->getDescriptions(); + if (count($descriptions) > 1) { + $this->tags[] = '<ul>'; + foreach ($descriptions as $d) { + $this->tags[] = '<li>' . $d . '</li>'; + } + $this->tags[] = '</ul>'; + } elseif (! empty($descriptions)) { + $this->tags[] = $descriptions[0]; + } + $this->tags[] = '</div>'; + $this->tags[] = '<div class="state ' . ($requirement->getState() ? 'fulfilled' : ( + $requirement->isOptional() ? 'not-available' : 'missing' + )) . '">' . $requirement->getStateText() . '</div>'; + $this->tags[] = '</li>'; + } + + return implode("\n", $this->tags); + } + + public function __toString() + { + return $this->render(); + } +} diff --git a/modules/setup/library/Setup/Setup.php b/modules/setup/library/Setup/Setup.php new file mode 100644 index 0000000..7b0baed --- /dev/null +++ b/modules/setup/library/Setup/Setup.php @@ -0,0 +1,99 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use ArrayIterator; +use IteratorAggregate; +use Icinga\Module\Setup\Exception\SetupException; +use Traversable; + +/** + * Container for multiple configuration steps + */ +class Setup implements IteratorAggregate +{ + protected $steps; + + protected $state; + + public function __construct() + { + $this->steps = array(); + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->getSteps()); + } + + public function addStep(Step $step) + { + $this->steps[] = $step; + } + + public function addSteps(array $steps) + { + foreach ($steps as $step) { + $this->addStep($step); + } + } + + public function getSteps() + { + return $this->steps; + } + + /** + * Run the configuration and return whether it succeeded + * + * @return bool + */ + public function run() + { + $this->state = true; + + try { + foreach ($this->steps as $step) { + $this->state &= $step->apply(); + } + } catch (SetupException $_) { + $this->state = false; + } + + return $this->state; + } + + /** + * Return a summary of all actions designated to run + * + * @return array An array of HTML strings + */ + public function getSummary() + { + $summaries = array(); + foreach ($this->steps as $step) { + $summaries[] = $step->getSummary(); + } + + return $summaries; + } + + /** + * Return a report of all actions that were run + * + * @return array An array of arrays of strings + */ + public function getReport() + { + $reports = array(); + foreach ($this->steps as $step) { + $report = $step->getReport(); + if (! empty($report)) { + $reports[] = $report; + } + } + + return $reports; + } +} diff --git a/modules/setup/library/Setup/SetupWizard.php b/modules/setup/library/Setup/SetupWizard.php new file mode 100644 index 0000000..c7ad0c3 --- /dev/null +++ b/modules/setup/library/Setup/SetupWizard.php @@ -0,0 +1,24 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +/** + * Interface for wizards providing a setup and requirements + */ +interface SetupWizard +{ + /** + * Return the setup for this wizard + * + * @return Setup + */ + public function getSetup(); + + /** + * Return the requirements of this wizard + * + * @return RequirementSet + */ + public function getRequirements(); +} diff --git a/modules/setup/library/Setup/Step.php b/modules/setup/library/Setup/Step.php new file mode 100644 index 0000000..1d0797d --- /dev/null +++ b/modules/setup/library/Setup/Step.php @@ -0,0 +1,31 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +/** + * Class to implement functionality for a single setup step + */ +abstract class Step +{ + /** + * Apply this step's configuration changes + * + * @return bool + */ + abstract public function apply(); + + /** + * Return a HTML representation of this step's configuration changes supposed to be made + * + * @return string + */ + abstract public function getSummary(); + + /** + * Return a textual summary of all configuration changes made + * + * @return array + */ + abstract public function getReport(); +} diff --git a/modules/setup/library/Setup/Steps/AuthenticationStep.php b/modules/setup/library/Setup/Steps/AuthenticationStep.php new file mode 100644 index 0000000..3c6c64a --- /dev/null +++ b/modules/setup/library/Setup/Steps/AuthenticationStep.php @@ -0,0 +1,238 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use Icinga\Application\Config; +use Icinga\Data\ConfigObject; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\IcingaException; +use Icinga\Authentication\User\DbUserBackend; +use Icinga\Module\Setup\Step; + +class AuthenticationStep extends Step +{ + protected $data; + + protected $dbError; + + protected $authIniError; + + protected $permIniError; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $success = $this->createAuthenticationIni(); + if (isset($this->data['adminAccountData']['resourceConfig'])) { + $success &= $this->createAccount(); + } + + $success &= $this->createRolesIni(); + return $success; + } + + protected function createAuthenticationIni() + { + $config = array(); + $backendConfig = $this->data['backendConfig']; + $backendName = $backendConfig['name']; + unset($backendConfig['name']); + $config[$backendName] = $backendConfig; + if (isset($this->data['resourceName'])) { + $config[$backendName]['resource'] = $this->data['resourceName']; + } + + try { + Config::fromArray($config) + ->setConfigFile(Config::resolvePath('authentication.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->authIniError = $e; + return false; + } + + $this->authIniError = false; + return true; + } + + protected function createRolesIni() + { + if (isset($this->data['adminAccountData']['username'])) { + $config = array( + 'users' => $this->data['adminAccountData']['username'], + 'permissions' => '*' + ); + + if ($this->data['backendConfig']['backend'] === 'db') { + $config['groups'] = mt('setup', 'Administrators', 'setup.role.name'); + } + } else { // isset($this->data['adminAccountData']['groupname']) + $config = array( + 'groups' => $this->data['adminAccountData']['groupname'], + 'permissions' => '*' + ); + } + + try { + Config::fromArray(array(mt('setup', 'Administrators', 'setup.role.name') => $config)) + ->setConfigFile(Config::resolvePath('roles.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->permIniError = $e; + return false; + } + + $this->permIniError = false; + return true; + } + + protected function createAccount() + { + try { + $backend = new DbUserBackend( + ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig'])) + ); + + if ($backend->select()->where('user_name', $this->data['adminAccountData']['username'])->count() === 0) { + $backend->insert('user', array( + 'user_name' => $this->data['adminAccountData']['username'], + 'password' => $this->data['adminAccountData']['password'], + 'is_active' => true + )); + $this->dbError = false; + } + } catch (Exception $e) { + $this->dbError = $e; + return false; + } + + return true; + } + + public function getSummary() + { + $pageTitle = '<h2>' . mt('setup', 'Authentication', 'setup.page.title') . '</h2>'; + $backendTitle = '<h3>' . mt('setup', 'Authentication Backend', 'setup.page.title') . '</h3>'; + $adminTitle = '<h3>' . mt('setup', 'Administration', 'setup.page.title') . '</h3>'; + + $authType = $this->data['backendConfig']['backend']; + $backendDesc = '<p>' . sprintf( + mt('setup', 'Users will authenticate using %s.', 'setup.summary.auth'), + $authType === 'db' ? mt('setup', 'a database', 'setup.summary.auth.type') : ( + $authType === 'ldap' || $authType === 'msldap' ? 'LDAP' : ( + mt('setup', 'webserver authentication', 'setup.summary.auth.type') + ) + ) + ) . '</p>'; + + $backendHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Backend Name') . '</strong></td>' + . '<td>' . $this->data['backendConfig']['name'] . '</td>' + . '</tr>' + . ($authType === 'ldap' || $authType === 'msldap' ? ( + '<tr>' + . '<td><strong>' . mt('setup', 'User Object Class') . '</strong></td>' + . '<td>' . ($authType === 'msldap' ? 'user' : $this->data['backendConfig']['user_class']) . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Custom Filter') . '</strong></td>' + . '<td>' . (trim($this->data['backendConfig']['filter']) ?: t('None', 'auth.ldap.filter')) . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'User Name Attribute') . '</strong></td>' + . '<td>' . ($authType === 'msldap' + ? 'sAMAccountName' + : $this->data['backendConfig']['user_name_attribute']) . '</td>' + . '</tr>' + ) : ($authType === 'external' ? ( + '<tr>' + . '<td><strong>' . t('Filter Pattern') . '</strong></td>' + . '<td>' . $this->data['backendConfig']['strip_username_regexp'] . '</td>' + . '</tr>' + ) : '')) + . '</tbody>' + . '</table>'; + + if (isset($this->data['adminAccountData']['username'])) { + $adminHtml = '<p>' . (isset($this->data['adminAccountData']['resourceConfig']) ? sprintf( + mt('setup', 'Administrative rights will initially be granted to a new account called "%s".'), + $this->data['adminAccountData']['username'] + ) : sprintf( + mt('setup', 'Administrative rights will initially be granted to an existing account called "%s".'), + $this->data['adminAccountData']['username'] + )) . '</p>'; + } else { // isset($this->data['adminAccountData']['groupname']) + $adminHtml = '<p>' . sprintf( + mt('setup', 'Administrative rights will initially be granted to members of the user group "%s".'), + $this->data['adminAccountData']['groupname'] + ) . '</p>'; + } + + return $pageTitle . '<div class="topic">' . $backendDesc . $backendTitle . $backendHtml . '</div>' + . '<div class="topic">' . $adminTitle . $adminHtml . '</div>'; + } + + public function getReport() + { + $report = array(); + + if ($this->authIniError === false) { + $report[] = sprintf( + mt('setup', 'Authentication configuration has been successfully written to: %s'), + Config::resolvePath('authentication.ini') + ); + } elseif ($this->authIniError !== null) { + $report[] = sprintf( + mt('setup', 'Authentication configuration could not be written to: %s. An error occured:'), + Config::resolvePath('authentication.ini') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->authIniError)); + } + + if ($this->dbError === false) { + $report[] = sprintf( + mt('setup', 'Account "%s" has been successfully created.'), + $this->data['adminAccountData']['username'] + ); + } elseif ($this->dbError !== null) { + $report[] = sprintf( + mt('setup', 'Unable to create account "%s". An error occured:'), + $this->data['adminAccountData']['username'] + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->dbError)); + } + + if ($this->permIniError === false) { + $report[] = isset($this->data['adminAccountData']['username']) ? sprintf( + mt('setup', 'Account "%s" has been successfully defined as initial administrator.'), + $this->data['adminAccountData']['username'] + ) : sprintf( + mt('setup', 'The members of the user group "%s" were successfully defined as initial administrators.'), + $this->data['adminAccountData']['groupname'] + ); + } elseif ($this->permIniError !== null) { + $report[] = isset($this->data['adminAccountData']['username']) ? sprintf( + mt('setup', 'Unable to define account "%s" as initial administrator. An error occured:'), + $this->data['adminAccountData']['username'] + ) : sprintf( + mt( + 'setup', + 'Unable to define the members of the user group "%s" as initial administrators. An error occured:' + ), + $this->data['adminAccountData']['groupname'] + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->permIniError)); + } + + return $report; + } +} diff --git a/modules/setup/library/Setup/Steps/DatabaseStep.php b/modules/setup/library/Setup/Steps/DatabaseStep.php new file mode 100644 index 0000000..32b2d15 --- /dev/null +++ b/modules/setup/library/Setup/Steps/DatabaseStep.php @@ -0,0 +1,266 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use PDOException; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; +use Icinga\Module\Setup\Utils\DbTool; +use Icinga\Module\Setup\Exception\SetupException; + +class DatabaseStep extends Step +{ + protected $data; + + protected $error; + + protected $messages; + + public function __construct(array $data) + { + $this->data = $data; + $this->messages = array(); + } + + public function apply() + { + $resourceConfig = $this->data['resourceConfig']; + if (isset($this->data['adminName'])) { + $resourceConfig['username'] = $this->data['adminName']; + if (isset($this->data['adminPassword'])) { + $resourceConfig['password'] = $this->data['adminPassword']; + } + } + + $db = new DbTool($resourceConfig); + + try { + if ($resourceConfig['db'] === 'mysql') { + $this->setupMysqlDatabase($db); + } elseif ($resourceConfig['db'] === 'pgsql') { + $this->setupPgsqlDatabase($db); + } + } catch (Exception $e) { + $this->error = $e; + throw new SetupException(); + } + + $this->error = false; + return true; + } + + protected function setupMysqlDatabase(DbTool $db) + { + try { + $db->connectToDb(); + $this->log( + mt('setup', 'Successfully connected to existing database "%s"...'), + $this->data['resourceConfig']['dbname'] + ); + } catch (PDOException $_) { + $db->connectToHost(); + $this->log(mt('setup', 'Creating new database "%s"...'), $this->data['resourceConfig']['dbname']); + $db->exec('CREATE DATABASE ' . $db->quoteIdentifier($this->data['resourceConfig']['dbname'])); + $db->reconnect($this->data['resourceConfig']['dbname']); + } + + if (array_search(reset($this->data['tables']), $db->listTables(), true) !== false) { + $this->log(mt('setup', 'Database schema already exists...')); + } else { + $this->log(mt('setup', 'Creating database schema...')); + $db->import($this->data['schemaPath'] . '/mysql.schema.sql'); + } + + if ($db->hasLogin($this->data['resourceConfig']['username'])) { + $this->log(mt('setup', 'Login "%s" already exists...'), $this->data['resourceConfig']['username']); + } else { + $this->log(mt('setup', 'Creating login "%s"...'), $this->data['resourceConfig']['username']); + $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']); + } + + $username = $this->data['resourceConfig']['username']; + if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) { + $this->log( + mt('setup', 'Required privileges were already granted to login "%s".'), + $this->data['resourceConfig']['username'] + ); + } else { + $this->log( + mt('setup', 'Granting required privileges to login "%s"...'), + $this->data['resourceConfig']['username'] + ); + $db->grantPrivileges( + $this->data['privileges'], + $this->data['tables'], + $this->data['resourceConfig']['username'] + ); + } + } + + protected function setupPgsqlDatabase(DbTool $db) + { + try { + $db->connectToDb(); + $this->log( + mt('setup', 'Successfully connected to existing database "%s"...'), + $this->data['resourceConfig']['dbname'] + ); + } catch (PDOException $_) { + $db->connectToHost(); + $this->log(mt('setup', 'Creating new database "%s"...'), $this->data['resourceConfig']['dbname']); + $db->exec(sprintf( + "CREATE DATABASE %s WITH ENCODING 'UTF-8'", + $db->quoteIdentifier($this->data['resourceConfig']['dbname']) + )); + $db->reconnect($this->data['resourceConfig']['dbname']); + } + + if (array_search(reset($this->data['tables']), $db->listTables(), true) !== false) { + $this->log(mt('setup', 'Database schema already exists...')); + } else { + $this->log(mt('setup', 'Creating database schema...')); + $db->import($this->data['schemaPath'] . '/pgsql.schema.sql'); + } + + if ($db->hasLogin($this->data['resourceConfig']['username'])) { + $this->log(mt('setup', 'Login "%s" already exists...'), $this->data['resourceConfig']['username']); + } else { + $this->log(mt('setup', 'Creating login "%s"...'), $this->data['resourceConfig']['username']); + $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']); + } + + $username = $this->data['resourceConfig']['username']; + if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) { + $this->log( + mt('setup', 'Required privileges were already granted to login "%s".'), + $this->data['resourceConfig']['username'] + ); + } else { + $this->log( + mt('setup', 'Granting required privileges to login "%s"...'), + $this->data['resourceConfig']['username'] + ); + $db->grantPrivileges( + $this->data['privileges'], + $this->data['tables'], + $this->data['resourceConfig']['username'] + ); + } + } + + public function getSummary() + { + $resourceConfig = $this->data['resourceConfig']; + if (isset($this->data['adminName'])) { + $resourceConfig['username'] = $this->data['adminName']; + if (isset($this->data['adminPassword'])) { + $resourceConfig['password'] = $this->data['adminPassword']; + } + } + + $db = new DbTool($resourceConfig); + + try { + $db->connectToDb(); + if (array_search(reset($this->data['tables']), $db->listTables(), true) === false) { + if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to setup the missing schema required by Icinga' + . ' Web 2 in database "%s" and to grant access to it to a new login called "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'], + $this->data['resourceConfig']['username'] + ); + } else { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to setup the missing' + . ' schema required by Icinga Web 2 in database "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'] + ); + } + } else { + $message = sprintf( + mt('setup', 'The database "%s" already seems to be fully set up. No action required.'), + $resourceConfig['dbname'] + ); + } + } catch (PDOException $_) { + try { + $db->connectToHost(); + if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) { + if ($db->hasLogin($this->data['resourceConfig']['username'])) { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to create the missing database' + . ' "%s" with the schema required by Icinga Web 2 and to grant' + . ' access to it to an existing login called "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'], + $this->data['resourceConfig']['username'] + ); + } else { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to create the missing database' + . ' "%s" with the schema required by Icinga Web 2 and to grant' + . ' access to it to a new login called "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'], + $this->data['resourceConfig']['username'] + ); + } + } else { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to create the missing' + . ' database "%s" with the schema required by Icinga Web 2.' + ), + $resourceConfig['username'], + $resourceConfig['dbname'] + ); + } + } catch (Exception $_) { + $message = mt( + 'setup', + 'No connection to database host possible. You\'ll need to setup the' + . ' database with the schema required by Icinga Web 2 manually.' + ); + } + } + + return '<h2>' . mt('setup', 'Database Setup', 'setup.page.title') . '</h2><p>' . $message . '</p>'; + } + + public function getReport() + { + if ($this->error === false) { + $report = $this->messages; + $report[] = mt('setup', 'The database has been fully set up!'); + return $report; + } elseif ($this->error !== null) { + $report = $this->messages; + $report[] = mt('setup', 'Failed to fully setup the database. An error occured:'); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)); + return $report; + } + } + + protected function log() + { + $this->messages[] = call_user_func_array('sprintf', func_get_args()); + } +} diff --git a/modules/setup/library/Setup/Steps/GeneralConfigStep.php b/modules/setup/library/Setup/Steps/GeneralConfigStep.php new file mode 100644 index 0000000..2c928f6 --- /dev/null +++ b/modules/setup/library/Setup/Steps/GeneralConfigStep.php @@ -0,0 +1,131 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Application\Config; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; + +class GeneralConfigStep extends Step +{ + protected $data; + + protected $error; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $config = array(); + foreach ($this->data['generalConfig'] as $sectionAndPropertyName => $value) { + list($section, $property) = explode('_', $sectionAndPropertyName, 2); + $config[$section][$property] = $value; + } + + $config['global']['config_resource'] = $this->data['resourceName']; + + try { + Config::fromArray($config) + ->setConfigFile(Config::resolvePath('config.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + $pageTitle = '<h2>' . mt('setup', 'Application Configuration', 'setup.page.title') . '</h2>'; + $generalTitle = '<h3>' . t('General', 'app.config') . '</h3>'; + $loggingTitle = '<h3>' . t('Logging', 'app.config') . '</h3>'; + + $generalHtml = '' + . '<ul>' + . '<li>' . ($this->data['generalConfig']['global_show_stacktraces'] + ? t('An exception\'s stacktrace is shown to every user by default.') + : t('An exception\'s stacktrace is hidden from every user by default.') + ) . '</li>' + . '<li>' . t('Preferences will be stored using a database.') . '</li>' + . '</ul>'; + + $type = $this->data['generalConfig']['logging_log']; + if ($type === 'none') { + $loggingHtml = '<p>' . mt('setup', 'Logging will be disabled.') . '</p>'; + } else { + $level = $this->data['generalConfig']['logging_level']; + + switch ($type) { + case 'php': + $typeDescription = t('Webserver Log', 'app.config.logging.type'); + $typeSpecificHtml = ''; + break; + + case 'syslog': + $typeDescription = 'Syslog'; + $typeSpecificHtml = '<td><strong>' . t('Application Prefix') . '</strong></td>' + . '<td>' . $this->data['generalConfig']['logging_application'] . '</td>'; + break; + + case 'file': + $typeDescription = t('File', 'app.config.logging.type'); + $typeSpecificHtml = '<td><strong>' . t('Filepath') . '</strong></td>' + . '<td>' . $this->data['generalConfig']['logging_file'] . '</td>'; + break; + } + + $loggingHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Type', 'app.config.logging') . '</strong></td>' + . '<td>' . $typeDescription . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Level', 'app.config.logging') . '</strong></td>' + . '<td>' . ($level === Logger::$levels[Logger::ERROR] ? t('Error', 'app.config.logging.level') : ( + $level === Logger::$levels[Logger::WARNING] ? t('Warning', 'app.config.logging.level') : ( + $level === Logger::$levels[Logger::INFO] ? t('Information', 'app.config.logging.level') : ( + t('Debug', 'app.config.logging.level') + ) + ) + )) . '</td>' + . '</tr>' + . '<tr>' + . $typeSpecificHtml + . '</tr>' + . '</tbody>' + . '</table>'; + } + + return $pageTitle . '<div class="topic">' . $generalTitle . $generalHtml . '</div>' + . '<div class="topic">' . $loggingTitle . $loggingHtml . '</div>'; + } + + public function getReport() + { + if ($this->error === false) { + return array(sprintf( + mt('setup', 'General configuration has been successfully written to: %s'), + Config::resolvePath('config.ini') + )); + } elseif ($this->error !== null) { + return array( + sprintf( + mt('setup', 'General configuration could not be written to: %s. An error occured:'), + Config::resolvePath('config.ini') + ), + sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)) + ); + } + } +} diff --git a/modules/setup/library/Setup/Steps/ResourceStep.php b/modules/setup/library/Setup/Steps/ResourceStep.php new file mode 100644 index 0000000..d9daf3b --- /dev/null +++ b/modules/setup/library/Setup/Steps/ResourceStep.php @@ -0,0 +1,199 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use Icinga\Application\Config; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; + +class ResourceStep extends Step +{ + protected $data; + + protected $error; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $resourceConfig = array(); + if (isset($this->data['dbResourceConfig'])) { + $dbConfig = $this->data['dbResourceConfig']; + $resourceName = $dbConfig['name']; + unset($dbConfig['name']); + $resourceConfig[$resourceName] = $dbConfig; + } + + if (isset($this->data['ldapResourceConfig'])) { + $ldapConfig = $this->data['ldapResourceConfig']; + $resourceName = $ldapConfig['name']; + unset($ldapConfig['name']); + $resourceConfig[$resourceName] = $ldapConfig; + } + + try { + Config::fromArray($resourceConfig) + ->setConfigFile(Config::resolvePath('resources.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + if (isset($this->data['dbResourceConfig']) && isset($this->data['ldapResourceConfig'])) { + $pageTitle = '<h2>' . mt('setup', 'Resources', 'setup.page.title') . '</h2>'; + } else { + $pageTitle = '<h2>' . mt('setup', 'Resource', 'setup.page.title') . '</h2>'; + } + + if (isset($this->data['dbResourceConfig'])) { + $dbTitle = '<h3>' . mt('setup', 'Database', 'setup.page.title') . '</h3>'; + $dbHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Resource Name') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['name'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Database Type') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['db'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Host') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['host'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Port') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['port'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Database Name') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['dbname'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Username') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['username'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Password') . '</strong></td>' + . '<td>' . str_repeat('*', strlen($this->data['dbResourceConfig']['password'])) . '</td>' + . '</tr>'; + + if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && isset($this->data['resourceConfig']['ssl_do_not_verify_server_cert']) + && $this->data['resourceConfig']['ssl_do_not_verify_server_cert'] + ) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('SSL Do Not Verify Server Certificate') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['ssl_do_not_verify_server_cert'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_key']) && $this->data['dbResourceConfig']['ssl_key']) { + $dbHtml .= '' + .'<tr>' + . '<td><strong>' . t('SSL Key') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_key'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_cert']) && $this->data['dbResourceConfig']['ssl_cert']) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('SSL Cert') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_cert'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_ca']) && $this->data['dbResourceConfig']['ssl_ca']) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('CA') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_ca'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_capath']) && $this->data['dbResourceConfig']['ssl_capath']) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('CA Path') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_capath'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_cipher']) && $this->data['dbResourceConfig']['ssl_cipher']) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('Cipher') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_cipher'] . '</td>' + . '</tr>'; + } + + $dbHtml .= '' + . '</tbody>' + . '</table>'; + } + + if (isset($this->data['ldapResourceConfig'])) { + $ldapTitle = '<h3>LDAP</h3>'; + $ldapHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Resource Name') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['name'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Host') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['hostname'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Port') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['port'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Root DN') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['root_dn'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Bind DN') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['bind_dn'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Bind Password') . '</strong></td>' + . '<td>' . str_repeat('*', strlen($this->data['ldapResourceConfig']['bind_pw'])) . '</td>' + . '</tr>' + . '</tbody>' + . '</table>'; + } + + return $pageTitle . (isset($dbTitle) ? '<div class="topic">' . $dbTitle . $dbHtml . '</div>' : '') + . (isset($ldapTitle) ? '<div class="topic">' . $ldapTitle . $ldapHtml . '</div>' : ''); + } + + public function getReport() + { + if ($this->error === false) { + return array(sprintf( + mt('setup', 'Resource configuration has been successfully written to: %s'), + Config::resolvePath('resources.ini') + )); + } elseif ($this->error !== null) { + return array( + sprintf( + mt('setup', 'Resource configuration could not be written to: %s. An error occured:'), + Config::resolvePath('resources.ini') + ), + sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)) + ); + } + } +} diff --git a/modules/setup/library/Setup/Steps/UserGroupStep.php b/modules/setup/library/Setup/Steps/UserGroupStep.php new file mode 100644 index 0000000..4aab676 --- /dev/null +++ b/modules/setup/library/Setup/Steps/UserGroupStep.php @@ -0,0 +1,213 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use Icinga\Application\Config; +use Icinga\Authentication\UserGroup\DbUserGroupBackend; +use Icinga\Data\ConfigObject; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; + +class UserGroupStep extends Step +{ + protected $data; + + protected $groupError; + + protected $memberError; + + protected $groupIniError; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $success = $this->createGroupsIni(); + if (isset($this->data['resourceConfig'])) { + $success &= $this->createUserGroup(); + if ($success) { + $success &= $this->createMembership(); + } + } + + return $success; + } + + protected function createGroupsIni() + { + $config = array(); + if (isset($this->data['groupConfig'])) { + $backendConfig = $this->data['groupConfig']; + $backendName = $backendConfig['name']; + unset($backendConfig['name']); + $config[$backendName] = $backendConfig; + } else { + $backendConfig = array( + 'backend' => $this->data['backendConfig']['backend'], // "db" or "msldap" + 'resource' => $this->data['resourceName'] + ); + + if ($backendConfig['backend'] === 'msldap') { + $backendConfig['user_backend'] = $this->data['backendConfig']['name']; + } + + $config[$this->data['backendConfig']['name']] = $backendConfig; + } + + try { + Config::fromArray($config) + ->setConfigFile(Config::resolvePath('groups.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->groupIniError = $e; + return false; + } + + $this->groupIniError = false; + return true; + } + + protected function createUserGroup() + { + try { + $backend = new DbUserGroupBackend( + ResourceFactory::createResource(new ConfigObject($this->data['resourceConfig'])) + ); + + $groupName = mt('setup', 'Administrators', 'setup.role.name'); + if ($backend->select()->where('group_name', $groupName)->count() === 0) { + $backend->insert('group', array( + 'group_name' => $groupName + )); + $this->groupError = false; + } + } catch (Exception $e) { + $this->groupError = $e; + return false; + } + + return true; + } + + protected function createMembership() + { + try { + $backend = new DbUserGroupBackend( + ResourceFactory::createResource(new ConfigObject($this->data['resourceConfig'])) + ); + + $groupName = mt('setup', 'Administrators', 'setup.role.name'); + $userName = $this->data['username']; + if ($backend + ->select() + ->from('group_membership') + ->where('group_name', $groupName) + ->where('user_name', $userName) + ->count() === 0 + ) { + $backend->insert('group_membership', array( + 'group_name' => $groupName, + 'user_name' => $userName + )); + $this->memberError = false; + } + } catch (Exception $e) { + $this->memberError = $e; + return false; + } + + return true; + } + + public function getSummary() + { + if (! isset($this->data['groupConfig'])) { + return; // It's not necessary to show the user something he didn't configure.. + } + + $pageTitle = '<h2>' . mt('setup', 'User Groups', 'setup.page.title') . '</h2>'; + $backendTitle = '<h3>' . mt('setup', 'User Group Backend', 'setup.page.title') . '</h3>'; + + $backendHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Backend Name') . '</strong></td>' + . '<td>' . $this->data['groupConfig']['name'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Group Object Class') . '</strong></td>' + . '<td>' . $this->data['groupConfig']['group_class'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Custom Filter') . '</strong></td>' + . '<td>' . (trim($this->data['groupConfig']['group_filter']) ?: t('None', 'auth.ldap.filter')) . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Group Name Attribute') . '</strong></td>' + . '<td>' . $this->data['groupConfig']['group_name_attribute'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Group Member Attribute') . '</strong></td>' + . '<td>' . $this->data['groupConfig']['group_member_attribute'] . '</td>' + . '</tr>' + . '</tbody>' + . '</table>'; + + return $pageTitle . '<div class="topic">' . $backendTitle . $backendHtml . '</div>'; + } + + public function getReport() + { + $report = array(); + + if ($this->groupIniError === false) { + $report[] = sprintf( + mt('setup', 'User Group Backend configuration has been successfully written to: %s'), + Config::resolvePath('groups.ini') + ); + } elseif ($this->groupIniError !== null) { + $report[] = sprintf( + mt('setup', 'User Group Backend configuration could not be written to: %s. An error occured:'), + Config::resolvePath('groups.ini') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->groupIniError)); + } + + if ($this->groupError === false) { + $report[] = sprintf( + mt('setup', 'User Group "%s" has been successfully created.'), + mt('setup', 'Administrators', 'setup.role.name') + ); + } elseif ($this->groupError !== null) { + $report[] = sprintf( + mt('setup', 'Unable to create user group "%s". An error occured:'), + mt('setup', 'Administrators', 'setup.role.name') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->groupError)); + } + + if ($this->memberError === false) { + $report[] = sprintf( + mt('setup', 'Account "%s" has been successfully added as member to user group "%s".'), + $this->data['username'], + mt('setup', 'Administrators', 'setup.role.name') + ); + } elseif ($this->memberError !== null) { + $report[] = sprintf( + mt('setup', 'Unable to add account "%s" as member to user group "%s". An error occured:'), + $this->data['username'], + mt('setup', 'Administrators', 'setup.role.name') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->memberError)); + } + + return $report; + } +} diff --git a/modules/setup/library/Setup/Utils/DbTool.php b/modules/setup/library/Setup/Utils/DbTool.php new file mode 100644 index 0000000..5cf203e --- /dev/null +++ b/modules/setup/library/Setup/Utils/DbTool.php @@ -0,0 +1,943 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Utils; + +use PDO; +use PDOException; +use LogicException; +use Zend_Db_Adapter_Pdo_Mysql; +use Zend_Db_Adapter_Pdo_Pgsql; +use Icinga\Util\File; +use Icinga\Exception\ConfigurationError; + +/** + * Utility class to ease working with databases when setting up Icinga Web 2 or one of its modules + */ +class DbTool +{ + /** + * The PDO database connection + * + * @var PDO + */ + protected $pdoConn; + + /** + * The Zend database adapter + * + * @var Zend_Db_Adapter_Pdo_Abstract + */ + protected $zendConn; + + /** + * The resource configuration + * + * @var array + */ + protected $config; + + /** + * Whether we are connected to the database from the resource configuration + * + * @var bool + */ + protected $dbFromConfig = false; + + /** + * GRANT privilege level identifiers + */ + const GLOBAL_LEVEL = 1; + const PROCEDURE_LEVEL = 2; + const DATABASE_LEVEL = 4; + const TABLE_LEVEL = 8; + const COLUMN_LEVEL = 16; + const FUNCTION_LEVEL = 32; + + /** + * All MySQL GRANT privileges with their respective level identifiers + * + * @var array + */ + protected $mysqlGrantContexts = array( + 'ALL' => 31, + 'ALL PRIVILEGES' => 31, + 'ALTER' => 13, + 'ALTER ROUTINE' => 7, + 'CREATE' => 13, + 'CREATE ROUTINE' => 5, + 'CREATE TEMPORARY TABLES' => 5, + 'CREATE USER' => 1, + 'CREATE VIEW' => 13, + 'DELETE' => 13, + 'DROP' => 13, + 'EXECUTE' => 5, // MySQL reference states this also supports database level, 5.1.73 not though + 'FILE' => 1, + 'GRANT OPTION' => 15, + 'INDEX' => 13, + 'INSERT' => 29, + 'LOCK TABLES' => 5, + 'PROCESS' => 1, + 'REFERENCES' => 12, + 'RELOAD' => 1, + 'REPLICATION CLIENT' => 1, + 'REPLICATION SLAVE' => 1, + 'SELECT' => 29, + 'SHOW DATABASES' => 1, + 'SHOW VIEW' => 13, + 'SHUTDOWN' => 1, + 'SUPER' => 1, + 'UPDATE' => 29 + ); + + /** + * All PostgreSQL GRANT privileges with their respective level identifiers + * + * @var array + */ + protected $pgsqlGrantContexts = array( + 'ALL' => 63, + 'ALL PRIVILEGES' => 63, + 'SELECT' => 24, + 'INSERT' => 24, + 'UPDATE' => 24, + 'DELETE' => 8, + 'TRUNCATE' => 8, + 'REFERENCES' => 24, + 'TRIGGER' => 8, + 'CREATE' => 12, + 'CONNECT' => 4, + 'TEMPORARY' => 4, + 'TEMP' => 4, + 'EXECUTE' => 32, + 'USAGE' => 33, + 'CREATEROLE' => 1 + ); + + /** + * Create a new DbTool + * + * @param array $config The resource configuration to use + */ + public function __construct(array $config) + { + if (! isset($config['port'])) { + // TODO: This is not quite correct, but works as it previously did. Previously empty values were not + // transformed no NULL (now they are) so if the port is now null, it's been the empty string. + $config['port'] = ''; + } + + $this->config = $config; + } + + /** + * Connect to the server + * + * @return $this + */ + public function connectToHost() + { + $this->assertHostAccess(); + + if ($this->config['db'] == 'pgsql') { + // PostgreSQL requires us to specify a database on each connection and will use + // the current user name as default database in cases none is provided. If + // that database doesn't exist (which might be the case here) it will error. + // Therefore, we specify the maintenance database 'postgres' as database, which + // is most probably present and public. (http://stackoverflow.com/q/4483139) + $this->connect('postgres'); + } else { + $this->connect(); + } + + return $this; + } + + /** + * Connect to the database + * + * @return $this + */ + public function connectToDb() + { + $this->assertHostAccess(); + $this->assertDatabaseAccess(); + $this->connect($this->config['dbname']); + return $this; + } + + /** + * Assert that all configuration values exist that are required to connect to a server + * + * @throws ConfigurationError + */ + protected function assertHostAccess() + { + if (! isset($this->config['db'])) { + throw new ConfigurationError('Can\'t connect to database server of unknown type'); + } elseif (! isset($this->config['host'])) { + throw new ConfigurationError('Can\'t connect to database server without a hostname or address'); + } elseif (! isset($this->config['port'])) { + throw new ConfigurationError('Can\'t connect to database server without a port'); + } elseif (! isset($this->config['username'])) { + throw new ConfigurationError('Can\'t connect to database server without a username'); + } elseif (! isset($this->config['password'])) { + throw new ConfigurationError('Can\'t connect to database server without a password'); + } + } + + /** + * Assert that all configuration values exist that are required to connect to a database + * + * @throws ConfigurationError + */ + protected function assertDatabaseAccess() + { + if (! isset($this->config['dbname'])) { + throw new ConfigurationError('Can\'t connect to database without a valid database name'); + } + } + + /** + * Assert that a connection with a database has been established + * + * @throws LogicException + */ + protected function assertConnectedToDb() + { + if ($this->zendConn === null) { + throw new LogicException('Not connected to database'); + } + } + + /** + * Return whether a connection with the server has been established + * + * @return bool + */ + public function isConnected() + { + return $this->pdoConn !== null; + } + + /** + * Establish a connection with the database or just the server by omitting the database name + * + * @param string $dbname The name of the database to connect to + */ + public function connect($dbname = null) + { + $this->pdoConnect($dbname); + if ($dbname !== null) { + $this->zendConnect($dbname); + $this->dbFromConfig = $dbname === $this->config['dbname']; + } + } + + /** + * Reestablish a connection with the database or just the server by omitting the database name + * + * @param string $dbname The name of the database to connect to + */ + public function reconnect($dbname = null) + { + $this->pdoConn = null; + $this->zendConn = null; + $this->connect($dbname); + } + + /** + * Initialize Zend database adapter + * + * @param string $dbname The name of the database to connect with + * + * @throws ConfigurationError In case the resource type is not a supported PDO driver name + */ + private function zendConnect($dbname) + { + if ($this->zendConn !== null) { + return; + } + + $config = array( + 'dbname' => $dbname, + 'host' => $this->config['host'], + 'port' => $this->config['port'], + 'username' => $this->config['username'], + 'password' => $this->config['password'] + ); + + if ($this->config['db'] === 'mysql') { + if (isset($this->config['use_ssl']) && $this->config['use_ssl']) { + $this->config['driver_options'] = array(); + # The presence of these keys as empty strings or null cause non-ssl connections to fail + if ($this->config['ssl_key']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key']; + } + if ($this->config['ssl_cert']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert']; + } + if ($this->config['ssl_ca']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca']; + } + if ($this->config['ssl_capath']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath']; + } + if ($this->config['ssl_cipher']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher']; + } + if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && $this->config['ssl_do_not_verify_server_cert'] + ) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false; + } + } + $this->zendConn = new Zend_Db_Adapter_Pdo_Mysql($config); + } elseif ($this->config['db'] === 'pgsql') { + $this->zendConn = new Zend_Db_Adapter_Pdo_Pgsql($config); + } else { + throw new ConfigurationError( + 'Failed to connect to database. Unsupported PDO driver "%s"', + $this->config['db'] + ); + } + + $this->zendConn->getConnection(); // Force connection attempt + } + + /** + * Initialize PDO connection + * + * @param string $dbname The name of the database to connect with + */ + private function pdoConnect($dbname) + { + if ($this->pdoConn !== null) { + return; + } + + $driverOptions = array( + PDO::ATTR_TIMEOUT => 1, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + + if ($this->config['db'] === 'mysql' + && isset($this->config['use_ssl']) + && $this->config['use_ssl'] + ) { + # The presence of these keys as empty strings or null cause non-ssl connections to fail + if ($this->config['ssl_key']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key']; + } + if ($this->config['ssl_cert']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert']; + } + if ($this->config['ssl_ca']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca']; + } + if ($this->config['ssl_capath']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath']; + } + if ($this->config['ssl_cipher']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher']; + } + if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && $this->config['ssl_do_not_verify_server_cert'] + ) { + $driverOptions[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false; + } + } + + $this->pdoConn = new PDO( + $this->buildDsn($this->config['db'], $dbname), + $this->config['username'], + $this->config['password'], + $driverOptions + ); + } + + /** + * Return a datasource name for the given database type and name + * + * @param string $dbtype + * @param string $dbname + * + * @return string + * + * @throws ConfigurationError In case the passed database type is not supported + */ + protected function buildDsn($dbtype, $dbname = null) + { + if ($dbtype === 'mysql') { + return 'mysql:host=' . $this->config['host'] . ';port=' . $this->config['port'] + . ($dbname !== null ? ';dbname=' . $dbname : ''); + } elseif ($dbtype === 'pgsql') { + return 'pgsql:host=' . $this->config['host'] . ';port=' . $this->config['port'] + . ($dbname !== null ? ';dbname=' . $dbname : ''); + } else { + throw new ConfigurationError( + 'Failed to build data source name. Unsupported PDO driver "%s"', + $dbtype + ); + } + } + + /** + * Try to connect to the server and throw an exception if this fails + * + * @throws PDOException In case an error occurs that does not indicate that authentication failed + */ + public function checkConnectivity() + { + try { + $this->connectToHost(); + } catch (PDOException $e) { + if ($this->config['db'] === 'mysql') { + $code = $e->getCode(); + /* + * 1040 .. Too many connections + * 1045 .. Access denied for user '%s'@'%s' (using password: %s) + * 1698 .. Access denied for user '%s'@'%s' + */ + if ($code !== 1040 && $code !== 1045 && $code !== 1698) { + throw $e; + } + } elseif ($this->config['db'] === 'pgsql') { + if (strpos($e->getMessage(), $this->config['username']) === false) { + throw $e; + } + } + } + } + + /** + * Return the given identifier escaped with backticks + * + * @param string $identifier The identifier to escape + * + * @return string + * + * @throws LogicException In case there is no behaviour implemented for the current PDO driver + */ + public function quoteIdentifier($identifier) + { + if ($this->config['db'] === 'mysql') { + return '`' . str_replace('`', '``', $identifier) . '`'; + } elseif ($this->config['db'] === 'pgsql') { + return '"' . str_replace('"', '""', $identifier) . '"'; + } else { + throw new LogicException('Unable to quote identifier.'); + } + } + + /** + * Return the given table name with all wildcards being escaped + * + * @param string $tableName + * + * @return string + * + * @throws LogicException In case there is no behaviour implemented for the current PDO driver + */ + public function escapeTableWildcards($tableName) + { + if ($this->config['db'] === 'mysql') { + return str_replace(array('_', '%'), array('\_', '\%'), $tableName); + } + + throw new LogicException('Unable to escape table wildcards.'); + } + + /** + * Return the given value escaped as string + * + * @param mixed $value The value to escape + * + * @return string + * + * @throws LogicException In case there is no behaviour implemented for the current PDO driver + */ + public function quote($value) + { + $quoted = $this->pdoConn->quote($value); + if ($quoted === false) { + throw new LogicException(sprintf('Unable to quote value: %s', $value)); + } + + return $quoted; + } + + /** + * Execute a SQL statement and return the affected row count + * + * Use $params to use a prepared statement. + * + * @param string $statement The statement to execute + * @param array $params The params to bind + * + * @return int + */ + public function exec($statement, $params = array()) + { + if (empty($params)) { + return $this->pdoConn->exec($statement); + } + + $stmt = $this->pdoConn->prepare($statement); + $stmt->execute($params); + return $stmt->rowCount(); + } + + /** + * Execute a SQL statement and return the result + * + * Use $params to use a prepared statement. + * + * @param string $statement The statement to execute + * @param array $params The params to bind + * + * @return mixed + */ + public function query($statement, $params = array()) + { + if ($this->zendConn !== null) { + return $this->zendConn->query($statement, $params); + } + + if (empty($params)) { + return $this->pdoConn->query($statement); + } + + $stmt = $this->pdoConn->prepare($statement); + $stmt->execute($params); + return $stmt; + } + + /** + * Return the version of the server currently connected to + * + * @return string|null + */ + public function getServerVersion() + { + if ($this->config['db'] === 'mysql') { + return $this->query('show variables like "version"')->fetchColumn(1) ?: null; + } elseif ($this->config['db'] === 'pgsql') { + return $this->query('show server_version')->fetchColumn() ?: null; + } else { + throw new LogicException( + sprintf('Unable to fetch the server\'s version. Unsupported PDO driver "%s"', $this->config['db']) + ); + } + } + + /** + * Import the given SQL file + * + * @param string $filepath The file to import + */ + public function import($filepath) + { + $file = new File($filepath); + $content = join(PHP_EOL, iterator_to_array($file)); // There is no fread() before PHP 5.5 :( + + foreach (preg_split('@;(?! \\\\)@', $content) as $statement) { + if (($statement = trim($statement)) !== '') { + $this->exec($statement); + } + } + } + + /** + * Return whether the given privileges were granted + * + * @param array $privileges An array of strings with the required privilege names + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The login name for which to check the privileges, + * if NULL the current login is used + * + * @return bool + */ + public function checkPrivileges(array $privileges, array $context = null, $username = null) + { + if ($this->config['db'] === 'mysql') { + return $this->checkMysqlPrivileges($privileges, false, $context, $username); + } elseif ($this->config['db'] === 'pgsql') { + return $this->checkPgsqlPrivileges($privileges, false, $context, $username); + } + } + + /** + * Return whether the given privileges are grantable to other users + * + * @param array $privileges The privileges that should be grantable + * + * @return bool + */ + public function isGrantable($privileges) + { + if ($this->config['db'] === 'mysql') { + return $this->checkMysqlPrivileges($privileges, true); + } elseif ($this->config['db'] === 'pgsql') { + return $this->checkPgsqlPrivileges($privileges, true); + } + } + + /** + * Grant all given privileges to the given user + * + * @param array $privileges The privilege names to grant + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The username to grant the privileges to + */ + public function grantPrivileges(array $privileges, array $context, $username) + { + if ($this->config['db'] === 'mysql') { + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $quotedDbName = $this->quoteIdentifier($this->config['dbname']); + + $grant = 'GRANT %s'; + $on = ' ON %s.%s'; + $to = sprintf( + ' TO %s@%s', + $this->quoteIdentifier($username), + $this->quoteIdentifier($host) + ); + + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->mysqlGrantContexts)) as $privilege) { + if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (! empty($tablePrivileges)) { + $tableGrant = sprintf($grant, join(',', $tablePrivileges)); + foreach ($context as $table) { + $this->exec($tableGrant . sprintf($on, $quotedDbName, $this->quoteIdentifier($table)) . $to); + } + } + + if (! empty($dbPrivileges)) { + $this->exec( + sprintf($grant, join(',', $dbPrivileges)) + . sprintf($on, $this->escapeTableWildcards($quotedDbName), '*') + . $to + ); + } + } elseif ($this->config['db'] === 'pgsql') { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) { + if (! empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (! empty($dbPrivileges)) { + $this->exec(sprintf( + 'GRANT %s ON DATABASE %s TO %s', + join(',', $dbPrivileges), + $this->config['dbname'], + $username + )); + } + + if (! empty($tablePrivileges)) { + foreach ($context as $table) { + $this->exec(sprintf( + 'GRANT %s ON TABLE %s TO %s', + join(',', $tablePrivileges), + $table, + $username + )); + } + } + } + } + + /** + * Return a list of all existing database tables + * + * @return array + */ + public function listTables() + { + $this->assertConnectedToDb(); + return $this->zendConn->listTables(); + } + + /** + * Return whether the given database login exists + * + * @param string $username The username to search + * + * @return bool + */ + public function hasLogin($username) + { + if ($this->config['db'] === 'mysql') { + $queryString = <<<EOD +SELECT 1 + FROM information_schema.user_privileges + WHERE grantee = REPLACE(CONCAT("'", REPLACE(CURRENT_USER(), '@', "'@'"), "'"), :current, :wanted) +EOD; + + $query = $this->query( + $queryString, + array( + ':current' => $this->config['username'], + ':wanted' => $username + ) + ); + return count($query->fetchAll()) > 0; + } elseif ($this->config['db'] === 'pgsql') { + $query = $this->query( + 'SELECT 1 FROM pg_catalog.pg_user WHERE usename = :ident LIMIT 1', + array(':ident' => $username) + ); + return count($query->fetchAll()) === 1; + } + } + + /** + * Add a new database login + * + * @param string $username The username of the new login + * @param string $password The password of the new login + */ + public function addLogin($username, $password) + { + if ($this->config['db'] === 'mysql') { + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $this->exec( + 'CREATE USER :user@:host IDENTIFIED BY :passw', + array(':user' => $username, ':host' => $host, ':passw' => $password) + ); + } elseif ($this->config['db'] === 'pgsql') { + $this->exec(sprintf( + 'CREATE USER %s WITH PASSWORD %s', + $this->quoteIdentifier($username), + $this->quote($password) + )); + } + } + + /** + * Check whether the current user has the given privileges + * + * @param array $privileges The privilege names + * @param bool $requireGrants Only return true when all privileges can be granted to others + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The login name to which the passed privileges need to be granted + * + * @return bool + */ + protected function checkMysqlPrivileges( + array $privileges, + $requireGrants = false, + array $context = null, + $username = null + ) { + $mysqlPrivileges = array_intersect($privileges, array_keys($this->mysqlGrantContexts)); + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $grantee = "'" . ($username === null ? $this->config['username'] : $username) . "'@'" . $host . "'"; + + if (isset($this->config['dbname'])) { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach ($mysqlPrivileges as $privilege) { + if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } + if ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + $dbPrivilegesGranted = true; + $tablePrivilegesGranted = true; + + if (! empty($dbPrivileges)) { + $queryString = 'SELECT COUNT(*) as matches' + . ' FROM information_schema.schema_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND privilege_type IN (%s)' + . ($requireGrants ? " AND is_grantable = 'YES'" : ''); + + $dbAndTableQuery = $this->query( + sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbPrivileges))), + array(':grantee' => $grantee, ':dbname' => $this->escapeTableWildcards($this->config['dbname'])) + ); + $grantedDbAndTablePrivileges = (int) $dbAndTableQuery->fetchObject()->matches; + if ($grantedDbAndTablePrivileges === count($dbPrivileges)) { + $tableExclusivePrivileges = array_diff($tablePrivileges, $dbPrivileges); + if (! empty($tableExclusivePrivileges)) { + $tablePrivileges = $tableExclusivePrivileges; + $tablePrivilegesGranted = false; + } + } else { + $tablePrivilegesGranted = false; + $dbExclusivePrivileges = array_diff($dbPrivileges, $tablePrivileges); + if (! empty($dbExclusivePrivileges)) { + $dbExclusiveQuery = $this->query( + sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbExclusivePrivileges))), + array( + ':grantee' => $grantee, + ':dbname' => $this->escapeTableWildcards($this->config['dbname']) + ) + ); + $dbPrivilegesGranted = (int) $dbExclusiveQuery->fetchObject()->matches === count( + $dbExclusivePrivileges + ); + } + } + } + + if (! $tablePrivilegesGranted && !empty($tablePrivileges)) { + $query = $this->query( + 'SELECT COUNT(*) as matches' + . ' FROM information_schema.table_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND table_name IN (' . join(',', array_map(array($this, 'quote'), $context)) . ')' + . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $tablePrivileges)) . ')' + . ($requireGrants ? " AND is_grantable = 'YES'" : ''), + array(':grantee' => $grantee, ':dbname' => $this->config['dbname']) + ); + $expectedAmountOfMatches = count($context) * count($tablePrivileges); + $tablePrivilegesGranted = (int) $query->fetchObject()->matches === $expectedAmountOfMatches; + } + + if ($dbPrivilegesGranted && $tablePrivilegesGranted) { + return true; + } + } + + $query = $this->query( + 'SELECT COUNT(*) as matches FROM information_schema.user_privileges WHERE grantee = :grantee' + . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $mysqlPrivileges)) . ')' + . ($requireGrants ? " AND is_grantable = 'YES'" : ''), + array(':grantee' => $grantee) + ); + return (int) $query->fetchObject()->matches === count($mysqlPrivileges); + } + + /** + * Check whether the current user has the given privileges + * + * Note that database and table specific privileges (i.e. not SUPER, CREATE and CREATEROLE) are ignored + * in case no connection to the database defined in the resource configuration has been established + * + * @param array $privileges The privilege names + * @param bool $requireGrants Only return true when all privileges can be granted to others + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The login name to which the passed privileges need to be granted + * + * @return bool + */ + public function checkPgsqlPrivileges( + array $privileges, + $requireGrants = false, + array $context = null, + $username = null + ) { + $privilegesGranted = true; + if ($this->dbFromConfig) { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) { + if (! empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } + if ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (! empty($dbPrivileges)) { + $dbExclusivesGranted = true; + foreach ($dbPrivileges as $dbPrivilege) { + $query = $this->query( + 'SELECT has_database_privilege(:user, :dbname, :privilege) AS db_privilege_granted', + array( + ':user' => $username !== null ? $username : $this->config['username'], + ':dbname' => $this->config['dbname'], + ':privilege' => $dbPrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '') + ) + ); + if (! $query->fetchObject()->db_privilege_granted) { + $privilegesGranted = false; + if (! in_array($dbPrivilege, $tablePrivileges)) { + $dbExclusivesGranted = false; + } + } + } + + if ($privilegesGranted) { + // Do not check privileges twice if they are already granted at database level + $tablePrivileges = array_diff($tablePrivileges, $dbPrivileges); + } elseif ($dbExclusivesGranted) { + $privilegesGranted = true; + } + } + + if ($privilegesGranted && !empty($tablePrivileges)) { + foreach (array_intersect($context, $this->listTables()) as $table) { + foreach ($tablePrivileges as $tablePrivilege) { + $query = $this->query( + 'SELECT has_table_privilege(:user, :table, :privilege) AS table_privilege_granted', + array( + ':user' => $username !== null ? $username : $this->config['username'], + ':table' => $table, + ':privilege' => $tablePrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '') + ) + ); + $privilegesGranted &= $query->fetchObject()->table_privilege_granted; + } + } + } + } else { + // In case we cannot check whether the user got the required db-/table-privileges due to not being + // connected to the database defined in the resource configuration it is safe to just ignore them + // as the chances are very high that the database is created later causing the current user being + // the owner with ALL privileges. (Which in turn can be granted to others.) + + if (array_search('CREATE', $privileges, true) !== false) { + $query = $this->query( + 'select rolcreatedb from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted &= $query->fetchColumn() !== false; + } + } + + if (array_search('CREATEROLE', $privileges, true) !== false) { + $query = $this->query( + 'select rolcreaterole from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted &= $query->fetchColumn() !== false; + } + + if (array_search('SUPER', $privileges, true) !== false) { + $query = $this->query( + 'select rolsuper from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted &= $query->fetchColumn() !== false; + } + + return (bool) $privilegesGranted; + } +} diff --git a/modules/setup/library/Setup/Utils/EnableModuleStep.php b/modules/setup/library/Setup/Utils/EnableModuleStep.php new file mode 100644 index 0000000..92af5b7 --- /dev/null +++ b/modules/setup/library/Setup/Utils/EnableModuleStep.php @@ -0,0 +1,77 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Utils; + +use Exception; +use Icinga\Application\Icinga; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; + +class EnableModuleStep extends Step +{ + protected $modulePaths; + + protected $moduleNames; + + protected $errors; + + protected $warnings; + + public function __construct(array $moduleNames) + { + $this->moduleNames = $moduleNames; + + $this->modulePaths = array(); + if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) { + $this->modulePaths[] = $appModulePath; + } + } + + public function apply() + { + $moduleManager = Icinga::app()->getModuleManager(); + $moduleManager->detectInstalledModules($this->modulePaths); + + $success = true; + foreach ($this->moduleNames as $moduleName) { + try { + $moduleManager->enableModule($moduleName); + } catch (ConfigurationError $e) { + $this->warnings[$moduleName] = $e; + } catch (Exception $e) { + $this->errors[$moduleName] = $e; + $success = false; + } + } + + return $success; + } + + public function getSummary() + { + // Enabling a module is like a implicit action, which does not need to be shown to the user... + } + + public function getReport() + { + $okMessage = mt('setup', 'Module "%s" has been successfully enabled.'); + $failMessage = mt('setup', 'Module "%s" could not be enabled. An error occured:'); + + $report = array(); + foreach ($this->moduleNames as $moduleName) { + if (isset($this->errors[$moduleName])) { + $report[] = sprintf($failMessage, $moduleName); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->errors[$moduleName])); + } elseif (isset($this->warnings[$moduleName])) { + $report[] = sprintf($failMessage, $moduleName); + $report[] = sprintf(mt('setup', 'WARNING: %s'), $this->warnings[$moduleName]->getMessage()); + } else { + $report[] = sprintf($okMessage, $moduleName); + } + } + + return $report; + } +} diff --git a/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php b/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php new file mode 100644 index 0000000..a3f218b --- /dev/null +++ b/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php @@ -0,0 +1,73 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Web\Form\Validator; + +use Exception; +use Zend_Validate_Abstract; +use Icinga\Util\File; + +/** + * Validator that checks if a token matches with the contents of a corresponding token-file + */ +class TokenValidator extends Zend_Validate_Abstract +{ + /** + * The path to the token file + * + * @var string + */ + protected $tokenPath; + + /** + * Create a new TokenValidator + * + * @param string $tokenPath The path to the token-file + */ + public function __construct($tokenPath) + { + $this->tokenPath = $tokenPath; + $this->_messageTemplates = array( + 'TOKEN_FILE_ERROR' => sprintf( + mt('setup', 'Cannot validate token: %s (%s)'), + $tokenPath, + '%value%' + ), + 'TOKEN_FILE_EMPTY' => sprintf( + mt('setup', 'Cannot validate token, file "%s" is empty. Please define a token.'), + $tokenPath + ), + 'TOKEN_INVALID' => mt('setup', 'Invalid token supplied.') + ); + } + + /** + * Validate the given token with the one in the token-file + * + * @param string $value The token to validate + * @param null $context The form context (ignored) + * + * @return bool + */ + public function isValid($value, $context = null) + { + try { + $file = new File($this->tokenPath); + $expectedToken = trim($file->fgets()); + } catch (Exception $e) { + $msg = $e->getMessage(); + $this->_error('TOKEN_FILE_ERROR', substr($msg, strpos($msg, ']: ') + 3)); + return false; + } + + if (empty($expectedToken)) { + $this->_error('TOKEN_FILE_EMPTY'); + return false; + } elseif ($value !== $expectedToken) { + $this->_error('TOKEN_INVALID'); + return false; + } + + return true; + } +} diff --git a/modules/setup/library/Setup/WebWizard.php b/modules/setup/library/Setup/WebWizard.php new file mode 100644 index 0000000..f25be55 --- /dev/null +++ b/modules/setup/library/Setup/WebWizard.php @@ -0,0 +1,752 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement\SetRequirement; +use Icinga\Module\Setup\Requirement\WebLibraryRequirement; +use PDOException; +use Icinga\Web\Form; +use Icinga\Web\Wizard; +use Icinga\Web\Request; +use Icinga\Application\Config; +use Icinga\Application\Icinga; +use Icinga\Module\Setup\Forms\ModulePage; +use Icinga\Module\Setup\Forms\WelcomePage; +use Icinga\Module\Setup\Forms\SummaryPage; +use Icinga\Module\Setup\Forms\DbResourcePage; +use Icinga\Module\Setup\Forms\AuthBackendPage; +use Icinga\Module\Setup\Forms\AdminAccountPage; +use Icinga\Module\Setup\Forms\LdapDiscoveryPage; +//use Icinga\Module\Setup\Forms\LdapDiscoveryConfirmPage; +use Icinga\Module\Setup\Forms\LdapResourcePage; +use Icinga\Module\Setup\Forms\RequirementsPage; +use Icinga\Module\Setup\Forms\GeneralConfigPage; +use Icinga\Module\Setup\Forms\AuthenticationPage; +use Icinga\Module\Setup\Forms\DatabaseCreationPage; +use Icinga\Module\Setup\Forms\UserGroupBackendPage; +use Icinga\Module\Setup\Steps\DatabaseStep; +use Icinga\Module\Setup\Steps\GeneralConfigStep; +use Icinga\Module\Setup\Steps\ResourceStep; +use Icinga\Module\Setup\Steps\AuthenticationStep; +use Icinga\Module\Setup\Steps\UserGroupStep; +use Icinga\Module\Setup\Utils\EnableModuleStep; +use Icinga\Module\Setup\Utils\DbTool; +use Icinga\Module\Setup\Requirement\OSRequirement; +use Icinga\Module\Setup\Requirement\PhpModuleRequirement; +use Icinga\Module\Setup\Requirement\PhpVersionRequirement; +use Icinga\Module\Setup\Requirement\ConfigDirectoryRequirement; +use Icinga\Module\Monitoring\Forms\Config\Transport\ApiTransportForm; + +/** + * Icinga Web 2 Setup Wizard + */ +class WebWizard extends Wizard implements SetupWizard +{ + /** + * The privileges required by Icinga Web 2 to create the database and a login + * + * @var array + */ + protected $databaseCreationPrivileges = array( + 'CREATE', + 'CREATE USER', // MySQL + 'CREATEROLE' // PostgreSQL + ); + + /** + * The privileges required by Icinga Web 2 to setup the database + * + * @var array + */ + protected $databaseSetupPrivileges = array( + 'CREATE', + 'ALTER', // MySQL only + 'REFERENCES' + ); + + /** + * The privileges required by Icinga Web 2 to operate the database + * + * @var array + */ + protected $databaseUsagePrivileges = array( + 'SELECT', + 'INSERT', + 'UPDATE', + 'DELETE', + 'EXECUTE', + 'TEMPORARY', // PostgreSql + 'CREATE TEMPORARY TABLES' // MySQL + ); + + /** + * The database tables operated by Icinga Web 2 + * + * @var array + */ + protected $databaseTables = array( + 'icingaweb_group', + 'icingaweb_group_membership', + 'icingaweb_user', + 'icingaweb_user_preference', + 'icingaweb_rememberme' + ); + + /** + * Register all pages and module wizards for this wizard + */ + protected function init() + { + $this->addPage(new WelcomePage()); + $this->addPage(new ModulePage()); + $this->addPage(new RequirementsPage()); + $this->addPage(new AuthenticationPage()); + $this->addPage(new DbResourcePage(array('name' => 'setup_auth_db_resource'))); + $this->addPage(new DatabaseCreationPage(array('name' => 'setup_auth_db_creation'))); + $this->addPage(new LdapDiscoveryPage()); + //$this->addPage(new LdapDiscoveryConfirmPage()); + $this->addPage(new LdapResourcePage()); + $this->addPage(new AuthBackendPage()); + $this->addPage(new UserGroupBackendPage()); + $this->addPage(new AdminAccountPage()); + $this->addPage(new GeneralConfigPage()); + $this->addPage(new DbResourcePage(array('name' => 'setup_config_db_resource'))); + $this->addPage(new DatabaseCreationPage(array('name' => 'setup_config_db_creation'))); + $this->addPage(new SummaryPage(array('name' => 'setup_summary'))); + + if (($modulePageData = $this->getPageData('setup_modules')) !== null) { + $modulePage = $this->getPage('setup_modules')->populate($modulePageData); + foreach ($modulePage->getModuleWizards() as $moduleWizard) { + $this->addPage($moduleWizard); + } + } + } + + /** + * Setup the given page that is either going to be displayed or validated + * + * @param Form $page The page to setup + * @param Request $request The current request + */ + public function setupPage(Form $page, Request $request) + { + if ($page->getName() === 'setup_requirements') { + $page->setWizard($this); + } elseif ($page->getName() === 'setup_authentication_backend') { + /** @var AuthBackendPage $page */ + + $authData = $this->getPageData('setup_authentication_type'); + if ($authData['type'] === 'db') { + $page->setResourceConfig($this->getPageData('setup_auth_db_resource')); + } elseif ($authData['type'] === 'ldap') { + $page->setResourceConfig($this->getPageData('setup_ldap_resource')); + + $suggestions = $this->getPageData('setup_ldap_discovery'); + if (isset($suggestions['backend'])) { + $page->setSuggestions($suggestions['backend']); + } + + if ($this->getDirection() === static::FORWARD) { + $backendConfig = $this->getPageData('setup_authentication_backend'); + if ($backendConfig !== null && $request->getPost('name') !== $backendConfig['name']) { + $pageData = & $this->getPageData(); + unset($pageData['setup_usergroup_backend']); + } + } + } + + if ($this->getDirection() === static::FORWARD) { + $backendConfig = $this->getPageData('setup_authentication_backend'); + if ($backendConfig !== null && $request->getPost('backend') !== $backendConfig['backend']) { + $pageData = & $this->getPageData(); + unset($pageData['setup_usergroup_backend']); + } + } + /*} elseif ($page->getName() === 'setup_ldap_discovery_confirm') { + $page->setResourceConfig($this->getPageData('setup_ldap_discovery'));*/ + } elseif ($page->getName() === 'setup_auth_db_resource') { + $page->addDescription(mt( + 'setup', + 'Now please configure the database resource where to store users and user groups.' + )); + $page->addDescription(mt( + 'setup', + 'Note that the database itself does not need to exist at this time as' + . ' it is going to be created once the wizard is about to be finished.' + )); + } elseif ($page->getName() === 'setup_usergroup_backend') { + $page->setResourceConfig($this->getPageData('setup_ldap_resource')); + $page->setBackendConfig($this->getPageData('setup_authentication_backend')); + } elseif ($page->getName() === 'setup_admin_account') { + $page->setBackendConfig($this->getPageData('setup_authentication_backend')); + $page->setGroupConfig($this->getPageData('setup_usergroup_backend')); + $authData = $this->getPageData('setup_authentication_type'); + if ($authData['type'] === 'db') { + $page->setResourceConfig($this->getPageData('setup_auth_db_resource')); + } elseif ($authData['type'] === 'ldap') { + $page->setResourceConfig($this->getPageData('setup_ldap_resource')); + } + } elseif ($page->getName() === 'setup_auth_db_creation' || $page->getName() === 'setup_config_db_creation') { + $page->setDatabaseSetupPrivileges( + array_unique(array_merge($this->databaseCreationPrivileges, $this->databaseSetupPrivileges)) + ); + $page->setDatabaseUsagePrivileges($this->databaseUsagePrivileges); + $page->setResourceConfig( + $this->getPageData('setup_auth_db_resource') ?: $this->getPageData('setup_config_db_resource') + ); + } elseif ($page->getName() === 'setup_summary') { + $page->setSubjectTitle('Icinga Web 2'); + $page->setSummary($this->getSetup()->getSummary()); + } elseif ($page->getName() === 'setup_config_db_resource') { + $page->addDescription(mt( + 'setup', + 'Now please configure the database resource where to store user preferences.' + )); + $page->addDescription(mt( + 'setup', + 'Note that the database itself does not need to exist at this time as' + . ' it is going to be created once the wizard is about to be finished.' + )); + + $ldapData = $this->getPageData('setup_ldap_resource'); + if ($ldapData !== null && $request->getPost('name') === $ldapData['name']) { + $page->error( + mt('setup', 'The given resource name must be unique and is already in use by the LDAP resource') + ); + } + } elseif ($page->getName() === 'setup_ldap_resource') { + $suggestion = $this->getPageData('setup_ldap_discovery'); + if (isset($suggestion['resource'])) { + $page->populate($suggestion['resource']); + } + + if ($this->getDirection() === static::FORWARD) { + $resourceConfig = $this->getPageData('setup_ldap_resource'); + if ($resourceConfig !== null && $request->getPost('name') !== $resourceConfig['name']) { + $pageData = & $this->getPageData(); + unset($pageData['setup_usergroup_backend']); + } + } + } elseif ($page->getName() === 'setup_authentication_type') { + $authData = $this->getPageData($page->getName()); + $pageData = & $this->getPageData(); + + if ($authData !== null && $request->getPost('type') !== $authData['type']) { + // Drop any existing page data in case the authentication type has changed, + // otherwise it will conflict with other forms that depend on this one + unset($pageData['setup_admin_account']); + unset($pageData['setup_authentication_backend']); + + if ($authData['type'] === 'db') { + unset($pageData['setup_auth_db_resource']); + unset($pageData['setup_auth_db_creation']); + } elseif ($request->getPost('type') === 'db') { + unset($pageData['setup_config_db_resource']); + unset($pageData['setup_config_db_creation']); + } + } elseif (isset($authData['type']) && $authData['type'] == 'external') { + // If you choose the authentication type external and validate the database and then come + // back to change the authentication type but do not change it, you will get an database configuration + // related error message on the next page. To avoid this error, the 'setup_config_db_resource' + // page must be unset. + + unset($pageData['setup_config_db_resource']); + } + } + } + + /** + * Return the new page to set as current page + * + * {@inheritdoc} Runs additional checks related to some registered pages. + * + * @param string $requestedPage The name of the requested page + * @param Form $originPage The origin page + * + * @return Form The new page + * + * @throws InvalidArgumentException In case the requested page does not exist or is not permitted yet + */ + protected function getNewPage($requestedPage, Form $originPage) + { + $skip = false; + $newPage = parent::getNewPage($requestedPage, $originPage); + if ($newPage->getName() === 'setup_auth_db_resource') { + $authData = $this->getPageData('setup_authentication_type'); + $skip = $authData['type'] !== 'db'; + } elseif ($newPage->getname() === 'setup_ldap_discovery') { + $authData = $this->getPageData('setup_authentication_type'); + $skip = $authData['type'] !== 'ldap'; + /*} elseif ($newPage->getName() === 'setup_ldap_discovery_confirm') { + $skip = false === $this->hasPageData('setup_ldap_discovery');*/ + } elseif ($newPage->getName() === 'setup_ldap_resource') { + $authData = $this->getPageData('setup_authentication_type'); + $skip = $authData['type'] !== 'ldap'; + } elseif ($newPage->getName() === 'setup_usergroup_backend') { + $backendConfig = $this->getPageData('setup_authentication_backend'); + $skip = $backendConfig['backend'] !== 'ldap'; + } elseif ($newPage->getName() === 'setup_config_db_resource') { + $authData = $this->getPageData('setup_authentication_type'); + $skip = $authData['type'] === 'db'; + } elseif (in_array($newPage->getName(), array('setup_auth_db_creation', 'setup_config_db_creation'))) { + if (($newPage->getName() === 'setup_auth_db_creation' || $this->hasPageData('setup_config_db_resource')) + && (($config = $this->getPageData('setup_auth_db_resource')) !== null + || ($config = $this->getPageData('setup_config_db_resource')) !== null) + && !$config['skip_validation'] && $this->getDirection() == static::FORWARD + ) { + // Execute this code only if the direction is forward. + // Otherwise, an error will be output when you go back. + $db = new DbTool($config); + + try { + $db->connectToDb(); // Are we able to login on the database? + + if (array_search(reset($this->databaseTables), $db->listTables(), true) === false) { + // In case the database schema does not yet exist the + // user needs the privileges to setup the database + $skip = $db->checkPrivileges($this->databaseSetupPrivileges, $this->databaseTables); + } else { + // In case the database schema exists the user needs the required privileges + // to operate the database, if those are missing we ask for another user + $skip = $db->checkPrivileges($this->databaseUsagePrivileges, $this->databaseTables); + } + } catch (PDOException $_) { + try { + $db->connectToHost(); // Are we able to login on the server? + // It is not possible to reliably determine whether a database exists or not if a user can't + // log in to the database, so we just require the user to be able to create the database + $skip = $db->checkPrivileges( + array_unique( + array_merge($this->databaseCreationPrivileges, $this->databaseSetupPrivileges) + ), + $this->databaseTables + ); + } catch (PDOException $_) { + // We are NOT able to login on the server.. + } + } + } else { + $skip = true; + } + } + + return $skip ? $this->skipPage($newPage) : $newPage; + } + + /** + * Add buttons to the given page based on its position in the page-chain + * + * @param Form $page The page to add the buttons to + */ + protected function addButtons(Form $page) + { + parent::addButtons($page); + + $pages = $this->getPages(); + $index = array_search($page, $pages, true); + if ($index === 0) { + $page->getElement(static::BTN_NEXT)->setLabel( + mt('setup', 'Start', 'setup.welcome.btn.next') + ); + } elseif ($index === count($pages) - 1) { + $page->getElement(static::BTN_NEXT)->setLabel( + mt('setup', 'Setup Icinga Web 2', 'setup.summary.btn.finish') + ); + } + + $authData = $this->getPageData('setup_authentication_type'); + $veto = $page->getName() === 'setup_authentication_backend' && $authData['type'] === 'db'; + if (! $veto && in_array($page->getName(), array( + 'setup_authentication_backend', + 'setup_auth_db_resource', + 'setup_config_db_resource', + 'setup_ldap_resource', + 'setup_monitoring_ido', + 'setup_icingadb_resource', + 'setup_icingadb_redis', + 'setup_icingadb_api_transport' + ))) { + $page->addElement( + 'submit', + 'backend_validation', + array( + 'ignore' => true, + 'label' => t('Validate Configuration'), + 'data-progress-label' => t('Validation In Progress'), + 'decorators' => array('ViewHelper'), + 'formnovalidate' => 'formnovalidate' + ) + ); + $page->getDisplayGroup('buttons')->addElement($page->getElement('backend_validation')); + } + + if ($page->getName() === 'setup_command_transport') { + if ($page->getSubForm('transport_form')->getSubForm('transport_form') instanceof ApiTransportForm) { + $page->addElement( + 'submit', + 'transport_validation', + array( + 'ignore' => true, + 'label' => t('Validate Configuration'), + 'data-progress-label' => t('Validation In Progress'), + 'decorators' => array('ViewHelper'), + 'formnovalidate' => 'formnovalidate' + ) + ); + $page->getDisplayGroup('buttons')->addElement($page->getElement('transport_validation')); + } + } + } + + /** + * Clear the session being used by this wizard + * + * @param bool $removeToken If true, the setup token will be removed + */ + public function clearSession($removeToken = true) + { + parent::clearSession(); + + if ($removeToken) { + $tokenPath = Config::resolvePath('setup.token'); + if (file_exists($tokenPath)) { + @unlink($tokenPath); + } + } + } + + /** + * Return the setup for this wizard + * + * @return Setup + */ + public function getSetup() + { + $pageData = $this->getPageData(); + $setup = new Setup(); + + if (isset($pageData['setup_auth_db_resource']) + && !$pageData['setup_auth_db_resource']['skip_validation'] + && (! isset($pageData['setup_auth_db_creation']) + || !$pageData['setup_auth_db_creation']['skip_validation'] + ) + ) { + $setup->addStep( + new DatabaseStep(array( + 'tables' => $this->databaseTables, + 'privileges' => $this->databaseUsagePrivileges, + 'resourceConfig' => $pageData['setup_auth_db_resource'], + 'adminName' => isset($pageData['setup_auth_db_creation']['username']) + ? $pageData['setup_auth_db_creation']['username'] + : null, + 'adminPassword' => isset($pageData['setup_auth_db_creation']['password']) + ? $pageData['setup_auth_db_creation']['password'] + : null, + 'schemaPath' => Config::module('setup') + ->get('schema', 'path', Icinga::app()->getBaseDir('schema')) + )) + ); + } elseif (isset($pageData['setup_config_db_resource']) + && !$pageData['setup_config_db_resource']['skip_validation'] + && (! isset($pageData['setup_config_db_creation']) + || !$pageData['setup_config_db_creation']['skip_validation'] + ) + ) { + $setup->addStep( + new DatabaseStep(array( + 'tables' => $this->databaseTables, + 'privileges' => $this->databaseUsagePrivileges, + 'resourceConfig' => $pageData['setup_config_db_resource'], + 'adminName' => isset($pageData['setup_config_db_creation']['username']) + ? $pageData['setup_config_db_creation']['username'] + : null, + 'adminPassword' => isset($pageData['setup_config_db_creation']['password']) + ? $pageData['setup_config_db_creation']['password'] + : null, + 'schemaPath' => Config::module('setup') + ->get('schema', 'path', Icinga::app()->getBaseDir('schema')) + )) + ); + } + + $setup->addStep( + new GeneralConfigStep(array( + 'generalConfig' => $pageData['setup_general_config'], + 'resourceName' => isset($pageData['setup_auth_db_resource']['name']) + ? $pageData['setup_auth_db_resource']['name'] + : (isset($pageData['setup_config_db_resource']['name']) + ? $pageData['setup_config_db_resource']['name'] + : null + ) + )) + ); + + $adminAccountType = $pageData['setup_admin_account']['user_type']; + if ($adminAccountType === 'user_group') { + $adminAccountData = array('groupname' => $pageData['setup_admin_account'][$adminAccountType]); + } else { + $adminAccountData = array('username' => $pageData['setup_admin_account'][$adminAccountType]); + if ($adminAccountType === 'new_user' && !$pageData['setup_auth_db_resource']['skip_validation'] + && (! isset($pageData['setup_auth_db_creation']) + || !$pageData['setup_auth_db_creation']['skip_validation'] + ) + ) { + $adminAccountData['resourceConfig'] = $pageData['setup_auth_db_resource']; + $adminAccountData['password'] = $pageData['setup_admin_account']['new_user_password']; + } + } + $authType = $pageData['setup_authentication_type']['type']; + $setup->addStep( + new AuthenticationStep(array( + 'adminAccountData' => $adminAccountData, + 'backendConfig' => $pageData['setup_authentication_backend'], + 'resourceName' => $authType === 'db' ? $pageData['setup_auth_db_resource']['name'] : ( + $authType === 'ldap' ? $pageData['setup_ldap_resource']['name'] : null + ) + )) + ); + + if ($authType !== 'external') { + $setup->addStep( + new UserGroupStep(array( + 'backendConfig' => $pageData['setup_authentication_backend'], + 'groupConfig' => isset($pageData['setup_usergroup_backend']) + ? $pageData['setup_usergroup_backend'] + : null, + 'resourceName' => $authType === 'db' + ? $pageData['setup_auth_db_resource']['name'] + : $pageData['setup_ldap_resource']['name'], + 'resourceConfig' => $authType === 'db' + ? $pageData['setup_auth_db_resource'] + : null, + 'username' => $authType === 'db' + ? $pageData['setup_admin_account'][$adminAccountType] + : null + )) + ); + } + + if (isset($pageData['setup_auth_db_resource']) + || isset($pageData['setup_config_db_resource']) + || isset($pageData['setup_ldap_resource']) + ) { + $setup->addStep( + new ResourceStep(array( + 'dbResourceConfig' => isset($pageData['setup_auth_db_resource']) + ? array_diff_key($pageData['setup_auth_db_resource'], array('skip_validation' => null)) + : (isset($pageData['setup_config_db_resource']) + ? array_diff_key($pageData['setup_config_db_resource'], array('skip_validation' => null)) + : null + ), + 'ldapResourceConfig' => isset($pageData['setup_ldap_resource']) + ? array_diff_key($pageData['setup_ldap_resource'], array('skip_validation' => null)) + : null + )) + ); + } + + foreach ($this->getWizards() as $wizard) { + if ($wizard->isComplete()) { + $setup->addSteps($wizard->getSetup()->getSteps()); + } + } + + $setup->addStep(new EnableModuleStep(array_keys($this->getPage('setup_modules')->getCheckedModules()))); + + return $setup; + } + + /** + * Return the requirements of this wizard + * + * @return RequirementSet + */ + public function getRequirements($skipModules = false) + { + $set = new RequirementSet(); + + $set->add(new PhpVersionRequirement(array( + 'condition' => array('>=', '7.2'), + 'description' => sprintf(mt( + 'setup', + 'Running Icinga Web 2 requires PHP version %s.' + ), '7.2') + ))); + + $set->add(new OSRequirement(array( + 'optional' => true, + 'condition' => 'linux', + 'description' => mt( + 'setup', + 'Icinga Web 2 is developed for and tested on Linux. While we cannot' + . ' guarantee they will, other platforms may also perform as well.' + ) + ))); + + $set->add(new WebLibraryRequirement(array( + 'condition' => ['icinga-php-library', '>=', '0.9.0'], + 'alias' => 'Icinga PHP library', + 'description' => mt( + 'setup', + 'The Icinga PHP library (IPL) is required for Icinga Web 2 and modules' + ) + ))); + + $set->add(new WebLibraryRequirement(array( + 'condition' => ['icinga-php-thirdparty', '>=', '0.11.0'], + 'alias' => 'Icinga PHP Thirdparty', + 'description' => mt( + 'setup', + 'The Icinga PHP Thirdparty library is required for Icinga Web 2 and modules' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'OpenSSL', + 'description' => mt( + 'setup', + 'The PHP module for OpenSSL is required to generate cryptographically safe password salts.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'XML', + 'description' => mt( + 'setup', + 'The XML module for PHP is required for Markdown and custom HTML annotations.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'JSON', + 'description' => mt( + 'setup', + 'The JSON module for PHP is required for various export functionalities as well as APIs.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'gettext', + 'description' => mt( + 'setup', + 'For message localization, the gettext module for PHP is required.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'INTL', + 'description' => mt( + 'setup', + 'For language, timezone and date/time format negotiation, the INTL module for PHP is required.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'DOM', + 'description' => mt( + 'setup', + 'For charts and exports of views and reports to PDF, the DOM module for PHP is required.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'LDAP', + 'description' => mt( + 'setup', + 'If you\'d like to authenticate users using LDAP the corresponding PHP module is required.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'mbstring', + 'description' => mt( + 'setup', + 'In case you want views being exported to PDF, you\'ll need the mbstring extension for PHP.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'GD', + 'description' => mt( + 'setup', + 'In case you want views being exported to PDF, you\'ll need the GD extension for PHP.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'Imagick', + 'description' => mt( + 'setup', + 'In case you want graphs being exported to PDF as well, you\'ll need the ImageMagick extension for PHP.' + ) + ))); + + $dbSet = new RequirementSet(false, RequirementSet::MODE_OR); + $dbSet->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'pdo_mysql', + 'alias' => 'PDO-MySQL', + 'description' => mt( + 'setup', + 'To store users or preferences in a MySQL database the PDO-MySQL module for PHP is required.' + ) + ))); + $dbSet->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'pdo_pgsql', + 'alias' => 'PDO-PostgreSQL', + 'description' => mt( + 'setup', + 'To store users or preferences in a PostgreSQL database the PDO-PostgreSQL module for PHP is required.' + ) + ))); + $set->merge($dbSet); + + $dbRequire = (new SetRequirement(array( + 'optional' => false, + 'condition' => $dbSet, + 'title' =>'Database', + 'alias' => 'PDO-MySQL OR PDO-PostgreSQL', + 'description' => mt( + 'setup', + 'A database is mandatory, therefore at least one module ' + . 'PDO-MySQL or PDO-PostgreSQL for PHP is required.' + ) + ))); + + $set->add($dbRequire); + + $set->add(new ConfigDirectoryRequirement(array( + 'condition' => Icinga::app()->getStorageDir(), + 'title' => mt('setup', 'Read- and writable storage directory'), + 'description' => mt( + 'setup', + 'The Icinga Web 2 storage directory defaults to "/var/lib/icingaweb2", if' . + ' not explicitly set in the environment variable "ICINGAWEB_STORAGEDIR".' + ) + ))); + + $set->add(new ConfigDirectoryRequirement(array( + 'condition' => Icinga::app()->getConfigDir(), + 'description' => mt( + 'setup', + 'The Icinga Web 2 configuration directory defaults to "/etc/icingaweb2", if' . + ' not explicitly set in the environment variable "ICINGAWEB_CONFIGDIR".' + ) + ))); + + if (! $skipModules) { + foreach ($this->getWizards() as $wizard) { + $set->merge($wizard->getRequirements()); + } + } + + return $set; + } +} diff --git a/modules/setup/library/Setup/Webserver.php b/modules/setup/library/Setup/Webserver.php new file mode 100644 index 0000000..2251ba3 --- /dev/null +++ b/modules/setup/library/Setup/Webserver.php @@ -0,0 +1,233 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use Icinga\Application\Icinga; +use Icinga\Exception\ProgrammingError; + +/** + * Base class for generating webserver configuration + */ +abstract class Webserver +{ + /** + * Document root + * + * @var string + */ + protected $documentRoot; + + /** + * URL path of Icinga Web 2 + * + * @var string + */ + protected $urlPath = '/icingaweb2'; + + /** + * Path to Icinga Web 2's configuration files + * + * @var string + */ + protected $configDir; + + /** + * Address or path where to pass requests to FPM + * + * @var string + */ + protected $fpmUri; + + /** + * Enable to pass requests to FPM + * + * @var bool + */ + protected $enableFpm = false; + + /** + * Create instance by type name + * + * @param string $type + * + * @return WebServer + * + * @throws ProgrammingError + */ + public static function createInstance($type) + { + $class = __NAMESPACE__ . '\\Webserver\\' . ucfirst($type); + if (class_exists($class)) { + return new $class(); + } + throw new ProgrammingError('Class "%s" does not exist', $class); + } + + /** + * Generate configuration + * + * @return string + */ + public function generate() + { + $template = $this->getTemplate(); + + $searchTokens = array( + '{urlPath}', + '{documentRoot}', + '{aliasDocumentRoot}', + '{configDir}', + '{fpmUri}' + ); + $replaceTokens = array( + $this->getUrlPath(), + $this->getDocumentRoot(), + preg_match('~/$~', $this->getUrlPath()) ? $this->getDocumentRoot() . '/' : $this->getDocumentRoot(), + $this->getConfigDir(), + $this->getFpmUri() + ); + $template = str_replace($searchTokens, $replaceTokens, $template); + return $template; + } + + /** + * Specific template + * + * @return string + */ + abstract protected function getTemplate(); + + /** + * Set the URL path of Icinga Web 2 + * + * @param string $urlPath + * + * @return $this + */ + public function setUrlPath($urlPath) + { + $this->urlPath = '/' . ltrim(trim((string) $urlPath), '/'); + return $this; + } + + /** + * Get the URL path of Icinga Web 2 + * + * @return string + */ + public function getUrlPath() + { + return $this->urlPath; + } + + /** + * Set the document root + * + * @param string $documentRoot + * + * @return $this + */ + public function setDocumentRoot($documentRoot) + { + $this->documentRoot = trim((string) $documentRoot); + return $this; + } + + /** + * Detect the document root + * + * @return string + */ + public function detectDocumentRoot() + { + return Icinga::app()->getBaseDir('public'); + } + + /** + * Get the document root + * + * @return string + */ + public function getDocumentRoot() + { + if ($this->documentRoot === null) { + $this->documentRoot = $this->detectDocumentRoot(); + } + return $this->documentRoot; + } + + /** + * Set the configuration directory + * + * @param string $configDir + * + * @return $this + */ + public function setConfigDir($configDir) + { + $this->configDir = (string) $configDir; + return $this; + } + + /** + * Get the configuration directory + * + * @return string + */ + public function getConfigDir() + { + if ($this->configDir === null) { + return Icinga::app()->getConfigDir(); + } + return $this->configDir; + } + + /** + * Get whether FPM is enabled + * + * @return bool + */ + public function getEnableFpm() + { + return $this->enableFpm; + } + + /** + * Set FPM enabled + * + * @param bool $flag + * + * @return $this + */ + public function setEnableFpm($flag) + { + $this->enableFpm = (bool) $flag; + + return $this; + } + + /** + * Get the address or path where to pass requests to FPM + * + * @return string + */ + public function getFpmUri() + { + return $this->fpmUri; + } + + /** + * Set the address or path where to pass requests to FPM + * + * @param string $uri + * + * @return $this + */ + public function setFpmUri($uri) + { + $this->fpmUri = (string) $uri; + + return $this; + } +} diff --git a/modules/setup/library/Setup/Webserver/Apache.php b/modules/setup/library/Setup/Webserver/Apache.php new file mode 100644 index 0000000..fdb367f --- /dev/null +++ b/modules/setup/library/Setup/Webserver/Apache.php @@ -0,0 +1,142 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Webserver; + +use Icinga\Module\Setup\Webserver; + +/** + * Generate Apache 2.x configuration + */ +class Apache extends Webserver +{ + protected $fpmUri = '127.0.0.1:9000'; + + protected function getTemplate() + { + if (! $this->enableFpm) { + return <<<'EOD' +Alias {urlPath} "{aliasDocumentRoot}" + +# Remove comments if you want to use PHP FPM and your Apache version is older than 2.4 +#<IfVersion < 2.4> +# # Forward PHP requests to FPM +# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 +# <LocationMatch "^{urlPath}/(.*\.php)$"> +# ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1" +# </LocationMatch> +#</IfVersion> + +<Directory "{documentRoot}"> + Options SymLinksIfOwnerMatch + AllowOverride None + + DirectoryIndex index.php + + <IfModule mod_authz_core.c> + # Apache 2.4 + <RequireAll> + Require all granted + </RequireAll> + </IfModule> + + <IfModule !mod_authz_core.c> + # Apache 2.2 + Order allow,deny + Allow from all + </IfModule> + + SetEnv ICINGAWEB_CONFIGDIR "{configDir}" + + EnableSendfile Off + + <IfModule mod_rewrite.c> + RewriteEngine on + RewriteBase {urlPath}/ + RewriteCond %{REQUEST_FILENAME} -s [OR] + RewriteCond %{REQUEST_FILENAME} -l [OR] + RewriteCond %{REQUEST_FILENAME} -d + RewriteRule ^.*$ - [NC,L] + RewriteRule ^.*$ index.php [NC,L] + </IfModule> + + <IfModule !mod_rewrite.c> + DirectoryIndex error_norewrite.html + ErrorDocument 404 {urlPath}/error_norewrite.html + </IfModule> + +# Remove comments if you want to use PHP FPM and your Apache version +# is greater than or equal to 2.4 +# <IfVersion >= 2.4> +# # Forward PHP requests to FPM +# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 +# <FilesMatch "\.php$"> +# SetHandler "proxy:fcgi://{fpmUri}" +# ErrorDocument 503 {urlPath}/error_unavailable.html +# </FilesMatch> +# </IfVersion> +</Directory> +EOD; + } else { + return <<<'EOD' +Alias {urlPath} "{aliasDocumentRoot}" + +<IfVersion < 2.4> + # Forward PHP requests to FPM + SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 + <LocationMatch "^{urlPath}/(.*\.php)$"> + ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1" + </LocationMatch> +</IfVersion> + +<Directory "{documentRoot}"> + Options SymLinksIfOwnerMatch + AllowOverride None + + DirectoryIndex index.php + + <IfModule mod_authz_core.c> + # Apache 2.4 + <RequireAll> + Require all granted + </RequireAll> + </IfModule> + + <IfModule !mod_authz_core.c> + # Apache 2.2 + Order allow,deny + Allow from all + </IfModule> + + SetEnv ICINGAWEB_CONFIGDIR "{configDir}" + + EnableSendfile Off + + <IfModule mod_rewrite.c> + RewriteEngine on + RewriteBase {urlPath}/ + RewriteCond %{REQUEST_FILENAME} -s [OR] + RewriteCond %{REQUEST_FILENAME} -l [OR] + RewriteCond %{REQUEST_FILENAME} -d + RewriteRule ^.*$ - [NC,L] + RewriteRule ^.*$ index.php [NC,L] + </IfModule> + + <IfModule !mod_rewrite.c> + DirectoryIndex error_norewrite.html + ErrorDocument 404 {urlPath}/error_norewrite.html + </IfModule> + + <IfVersion >= 2.4> + # Forward PHP requests to FPM + SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 + <FilesMatch "\.php$"> + SetHandler "proxy:fcgi://{fpmUri}" + ErrorDocument 503 {urlPath}/error_unavailable.html + </FilesMatch> + </IfVersion> +</Directory> +EOD; + } + } +} diff --git a/modules/setup/library/Setup/Webserver/Nginx.php b/modules/setup/library/Setup/Webserver/Nginx.php new file mode 100644 index 0000000..c7ae716 --- /dev/null +++ b/modules/setup/library/Setup/Webserver/Nginx.php @@ -0,0 +1,36 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Webserver; + +use Icinga\Module\Setup\Webserver; + +/** + * Generate nginx configuration + */ +class Nginx extends Webserver +{ + protected $fpmUri = '127.0.0.1:9000'; + + protected $enableFpm = true; + + protected function getTemplate() + { + return <<<'EOD' +location ~ ^{urlPath}/index\.php(.*)$ { + fastcgi_pass {fpmUri}; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME {documentRoot}/index.php; + fastcgi_param ICINGAWEB_CONFIGDIR {configDir}; + fastcgi_param REMOTE_USER $remote_user; +} + +location ~ ^{urlPath}(.+)? { + alias {documentRoot}; + index index.php; + try_files $1 $uri $uri/ {urlPath}/index.php$is_args$args; +} +EOD; + } +} |